[python]celery笔记

1 celery的部署

1.1 什么是celery?

celery 大致有两种应用场景,一种是异步任务,一种是定时任务。

比如说在一个接口请求中,某个函数执行所需的时间过长,而前端页面并不是立刻需要在接口中获取处理结果,可以将这个函数作为异步任务,先返回给前端处理中的信息,在后台单独运行这个函数,这就是异步任务。

另一个比如说某个函数需要每天晚上运行一遍,不可能人天天守着后台手动执行一遍这个函数,那么就可以用 celery 来实现这个定时的周期任务。

接下来介绍一下 celery 的组成:

task

这个任务就是我们前面举的例子的异步任务或者是定时任务,即为 task,我们可以定义这些任务,然后发送到 broker

broker

broker 可以理解成消息中间件,用于获取异步或者定时任务,形成一个或多个消息队列,然后发送给 worker 处理这些消息

broker 的形式可以是 Redis,RabbitMQ 或者其他,这里我们使用 Redis 作为消息中间件

worker

worker 是处理消息的程序,获取 broker 中的消息,然后在 worker 中执行,然后根据配置决定将处理结果发送到 backend

result_backend

在 worker 处理完消息之后会有 return 或者没有返回结果,都会根据配置将结果发送出来,可以配置成发送到 redis 中,也可以将之存储到 database 中

beat

主要用于调用定时任务,根据设定好的定时任务,比如每天晚上十点执行某个函数,beat 则会在相应的时间将这个 task 发送给 broker,然后 worker 获取任务进行处理

定时任务除了说的每天晚上十点这种周期任务,也可以是间隔任务,比如说每隔多少秒,多少分钟执行一次

注意:异步任务的发送是不经过 beat 处理,直接发送给 broker 的

在上面的结构中,broker 需要将相应的服务比如 redis 运行起来,而 worker 和 beat 需要在手动用程序运行,而且每次更改了定时策略之后需要重新启动 beat 和 worker 才能生效。

1.2 celery 准备

接下来我们实现一个最简单的异步任务,在执行异步任务前,我们做如下的准备工作

1.2.1 安装依赖

激活虚拟环境,创建一个py312的虚拟环境,激活环境

python3 -m venv new_project/venv
source new_project/venv/bin/activate

我们需要安装一下 celery 和 redis 的依赖

pip3 install celery==5.1.2 -i https://mirrors.aliyun.com/pypi/simple/
pip3 install redis==3.5.3 -i https://mirrors.aliyun.com/pypi/simple/

1.2.2 消息中间件

这里我们用到的消息中间件是 redis,可以去官网下载一个 redis 启动,也可以使用 docker 来执行安装。

使用docker来获取redis镜像

docker pull redis

拉取失败的话要换镜像源:https://www.cnblogs.com/harrylearn/p/18845568

1.2.3.异步任务准备

我们准备一个最简单的 add() 函数,放在 tasks.py 文件中:

# tasks.py
from celery import Celery

app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/1')

@app.task
def add(x, y): 
    return x + y

在这段代码里,我们引入 Celery 模块,并将其实例化为 app,且配置了 broker 参数,表示消息队列都会被放在 redis 的第一个数据库下

指定的 backend 参数则表示函数运行的结果被放在 redis 的第二个数据库下

然后用 @app.task 修饰 add 函数,表示它是 app 下的 task 任务

以上,我们的准备工作就完成了,接下来尝试运行这个异步任务

1.2.4 celery 启动和异步任务的运行

说是 celery 的启动,其实是 worker 的启动,中间件是 redis,已经在前面的步骤中启动了。

我们在 tasks.py 所在的文件夹下执行下面的命令:

celery -A tasks worker -l INFO

在这里,tasks 是我们任务所在的文件名,worker 表示启动的是 worker 程序

-l INFO 则会在控制台打印出 worker 接收到的消息详情,如果不执行,则信息流不会被打印出来

执行了上面的程序后,可以看到控制台会输出下面这种信息:

(venv) hanwang@k8s-master-node:~/learn_celery$ celery -A tasks worker -l INFO
/home/hanwang/learn_celery/new_project/venv/lib/python3.12/site-packages/celery/bin/celery.py:11: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import iter_entry_points
 
 -------------- celery@k8s-master-node v5.1.2 (sun-harmonics)
--- ***** ----- 
-- ******* ---- Linux-6.8.0-62-generic-x86_64-with-glibc2.39 2025-06-21 11:56:49
- *** --- * --- 
- ** ---------- [config]
- ** ---------- .> app:         tasks:0x70f29f6f4380
- ** ---------- .> transport:   redis://localhost:6379/0
- ** ---------- .> results:     redis://localhost/1
- *** --- * --- .> concurrency: 16 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** ----- 
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery
                

[tasks]
  . tasks.add

[2025-06-21 11:56:49,308: INFO/MainProcess] Connected to redis://localhost:6379/0
[2025-06-21 11:56:49,309: INFO/MainProcess] mingle: searching for neighbors
[2025-06-21 11:56:50,317: INFO/MainProcess] mingle: all alone
[2025-06-21 11:56:50,325: INFO/MainProcess] celery@k8s-master-node ready.

则表示 worker 启动成功

1.2.5 执行异步任务

在另一个 shell 窗口,进入 python 的交互界面,输入以下命令:

from tasks import add
res = add.delay(1,2)

add 是我们需要执行的异步任务的函数名

delay 是异步任务执行的特有方法,这个其实是 apply_async() 函数的简便写法,不带任何参数,apply_async() 除了可以实现异步任务的功能,还可以指定多少时间后执行,比如说二十秒后执行,这个在后面的笔记中我们再介绍。

而异步任务的返回我们这里用 res 来定义,它是一个包含了这个任务所有执行信息对象,有任务状态(是否执行成功),有返回结果(add() 函数的return),还有这个 task 特有的标识 id等信息

(venv) hanwang@k8s-master-node:~/learn_celery$ python
Python 3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> from tasks import add
>>> res = add.delay(1,2)
>>> print(res)
671d5719-2040-406e-8e33-d8faf94cf188
>>> print(res.result)
3
>>> print(res.id)
671d5719-2040-406e-8e33-d8faf94cf188

2 建立celery项目、配置及几种加载方式

创建一个 celery 项目,文件夹及目录如下:

pro/__init__.py
    /celery.py
    /tasks1.py
    /tasks2.py

2.1 项目内容介绍

celery.py
其中 celery.py 内容为 celery 实例化以及一些基础配置,文件内容如下:

from celery import Celery

app = Celery('pro',
             broker='redis://localhost/0',
             backend='redis://localhost/1',
             include=['proj.tasks1', 'proj.tasks2'])

app.conf.update(
    result_expires=60
)

if __name__ == '__main__':
    app.start()

在这里,我们还是对 celery 进行了实例化的操作,但是在这里项目名称改为了 pro,也就是这个文件的上一级文件夹名称

broker、backend 还是对应的配置

但是因为对应的任务我们是单独以文件的形式引入,所以,在这里引入的方式是通过 include 的方式来实现的

app.conf.update() 是对 celery 配置的补充,这里只加了一个参数,表示是对系统对结果的留存时间。

另外几种配置方式我们放在下面详讲。

接着 app.start() 就是项目的启动。
tasks.py

在这里我们将 task 作为两部分拆解,分别放在两个文件下,内容分别如下

#tasks1.py
from .celery import app

@app.task
def add(x, y):
    return x + y

#tasks2.py
from .celery import app

@app.task
def mul(x, y):
    return x * y

启动服务,在 pro 所在的文件夹,执行下面的命令:

celery -A pro worker -l INFO

运行延时任务

在 pro 同级的文件夹下执行 python,进入 python 的交互界面:

from pro.tasks1 import add
add.delay(1, 2)

2.2 celery配置的几种方式

在上面的 celery 的配置方式,是一部分写在 celery 的实例化过程中,另一部分是通过 app.conf.update() 的方式写入

这里介绍一下,celery 的配置还可以通过类的方式来写入,或者把所有变量都写入一个文件,通过引入文件的形式来引入变量。

不管是以类的方式还是文件的形式都会使用到一个方法:config_from_object(),参数为需要引入的变量

类的方式加载配置

下面是一个类的方式引入的示例:

# celery.py
from celery import Celery

app = Celery()

class Config:
    include = ['pro.tasks1', 'pro.tasks2']
    broker_url = 'redis://localhost:6379/0'
    result_backend = 'redis://localhost:6379/1'
    
app.config_from_object(Config)

if __name__ == '__main__':
    app.start()

这里需要注意的是在 Config 中的 broker_url 和 result_backend 与直接实例化 Celery 时写入的参数名称是不一样的

文件的形式加载配置

接下来我们在 pro 的文件夹中新建一个文件 celeryconfig.py,那么 pro 文件夹下的文件配置则如下:

pro/__init__.py
    /celery.py
    /celeryconfig.py
    /tasks1.py
    /tasks2.py

配置文件内容:

# celeryconfig.py
broker_url = 'redis://localhost/0'
result_backend = 'redis://localhost/1'
include = ['pro.tasks1', 'pro.tasks2']

celery 文件内容:

# celery.py
from celery import Celery
from . import celeryconfig


app = Celery()
app.config_from_object(celeryconfig)

if __name__ == '__main__':
    app.start()

无论是把配置写入 Config 类中,还是写入文件,这里的思想都是将配置集中处理,在一处管理所有的配置内容。

注意:
在这里我们引入配置的方式都是 config_from_object(),我们之前还介绍过一个更新配置的方式是 app.conf.update()

这里要说明的是,只要运行了 config_from_object() 函数,在此之前的变量都会被覆盖失效,如果我们要新增 celeryconfig 之外的配置,则需要在调用 config_from_object() 函数之后调用,比如:


app = Celery()

app.conf.update(result_expires=60) # 在 config_from_config() 之前调用,会失效

app.config_from_object(celeryconfig)

app.conf.update(result_expires=30)  # 这个配置会生效

还有一种引入配置的方式是在 Django 系统中,将 Celery 相关的变量都写入 settings.py,然后通过 namespace 的方式引入到 Celery 中进行实例化处理。

这个方式在后面celery 与 Django 结合的时候再做记录

2.3 一些基本配置

设置时区

比如我们设置北京时间:

app.conf.update(
    enable_utc=False,
    timezone='Asia/Shanghai',
)

broker 和 result_backend 设置

设置 broker 和 result_backend 的地址:

app.conf.update(
    broker_url = 'redis://localhost:6379/0'
    result_backend = 'redis://localhost:6379/1'
)

如果是 broker 和 backend 加密码的配置,则是如下:

app.conf.update(
    broker_url = 'redis://:123456@localhost:6380/0'
    result_backend = 'redis://:123456@localhost:6380/1'
)

其中,123456 是密码。

如果是用 docker 启动一个带密码的 redis,命令如下:

docker run -d --name redis_pwd -p6380:6379 redis:latest --requirepass 123456

3 celery中task和task的调用

3.1 基础的 task 定义方式

最简单的定义方式,使用 @app.task 作为装饰器:

@app.task
def add(x, y):
    return x + y

如果是在 Django 系统中使用 celery,需要定义一个延时任务或者周期定时任务,可以使用 @shared_task 来修饰

from celery import shared_task

@shared_task
def add(x, y):
    return x + y

多个装饰器

如果是 celery 的任务和其他装饰器一起联用,记得将 celery 的装饰器放在最后使用,也就是列表的最前面:

@app.task
@decorator1
@decorator2
def add(x, y):
    return x + y

task名称

每个 task 都有一个唯一的名称用来标识这个 task,如果我们在定义的时候不指定,系统会为我们默认一个名称,这些名称会在 celery 的 worker 启动的时候被系统扫描然后输出一个列表展示。

还是上一篇笔记中我们定义的两个 task,我们给其中一个指定 name:

#tasks1.py
from .celery import app


@app.task(name="tasks1.add")
def add(x, y):
    return x + y

可以观察在 celery 的 worker 启动的时候,会有一个输出:

[tasks]
  . pro.tasks2.mul
  . tasks1.add

可以看到这个地方,系统就会使用我们定义的 name 了。

3.2 日志处理

我们可以在启动 worker 的时候指定日志的输出,定义格式如下:

celery -A pro worker -l INFO --logfile=/home/hw/celery_new/celery_log/celery.log

在 task 中的定义可以使用 celery 中方法:

from celery.utils.log import get_task_logger

logger = get_task_logger(__name__)

也可以直接使用 logging 模块:

import logging

logger1 = logging.getLogger(__name__)

直接在 task 中输出:

@app.task(name="tasks1.add")
def add(x, y):
    logger.info("this is from logger")
    return x + y

然后在 worker 启动时指定的日志文件就会有我们打印出的日志内容:

[2025-06-21 14:48:44,005: INFO/MainProcess] Task tasks1.add[1b4c6a18-5c46-4f3b-a1a3-78b0763e0e5a] received
[2025-06-21 14:48:44,006: INFO/ForkPoolWorker-16] this is from logger
[2025-06-21 14:48:44,008: INFO/ForkPoolWorker-16] Task tasks1.add[1b4c6a18-5c46-4f3b-a1a3-78b0763e0e5a] succeeded in 0.002332591997401323s: 3

3.3 任务重试

对于一个 task,我们可以对其设置 retry 参数来指定其在任务执行失败后会重试几次,以及隔多长时间重试。

比如对于下面的 div() 函数,我们来输入除数为 0 的情况查看重试的功能。

当然,这里我们是故意输入参数错误,在实际的项目中可能会是其他的原因造成任务失败,比如数据库连接失败等

任务重试的参数也都在 @app.task() 中定义:

#tasks1.py
from .celery import app
import logging

logger1 = logging.getLogger(__name__)


@app.task(autoretry_for=(Exception, ),  
          default_retry_delay=10, retry_kwargs={'max_retries': 5}, name="tasks1.add")
def div(x, y):
    logger1.info("this is from logger")
    return x / y

在这里,autoretry_for 表示的是某种报错情况下重试,我们定义的 Exception 表示任何错误都重试。

如果只是想在某种特定的 exception 情况下重试,将那种 exception 的值替换 Exception 即可。

default_retry_delay 表示重试间隔时长,默认值是 3 * 60s,即三分钟,是以秒为单位,这里我们设置的是 10s。

retry_kwargs 是一个 dict,其中有一个 max_retries 参数,表示的是最大重试次数,我们定为 5

然后可以尝试调用这个延时任务:

from pro.tasks1 import div
div.delay(1, 0)

然后可以看到在日志文件会有如下输出:

[2025-06-21 15:02:33,585: INFO/MainProcess] Task tasks1.add[ba58cb79-c3d9-420f-9287-635cabe974e9] received
[2025-06-21 15:02:33,586: INFO/ForkPoolWorker-16] this is from logger
[2025-06-21 15:02:33,591: INFO/MainProcess] Task tasks1.add[ba58cb79-c3d9-420f-9287-635cabe974e9] received
[2025-06-21 15:02:33,592: INFO/ForkPoolWorker-16] Task tasks1.add[ba58cb79-c3d9-420f-9287-635cabe974e9] retry: Retry in 10s: ZeroDivisionError('division by zero')
[2025-06-21 15:02:43,589: INFO/ForkPoolWorker-16] this is from logger
[2025-06-21 15:02:43,592: INFO/MainProcess] Task tasks1.add[ba58cb79-c3d9-420f-9287-635cabe974e9] received
[2025-06-21 15:02:43,593: INFO/ForkPoolWorker-16] Task tasks1.add[ba58cb79-c3d9-420f-9287-635cabe974e9] retry: Retry in 10s: ZeroDivisionError('division by zero')

且每隔 10s 执行一次,一共执行 5 次,5次之后还是不成功则会报错。
retry_backoff 和 retry_backoff_max

还有一个 retry_backoff 和 retry_backoff_max 参数,这两个参数是用于这种情况:如果你的 task 依赖另一个 service 服务,比如会调用其他系统的 API,然后这两个参数可以用于避免请求过多的占用服务。

retry_backoff 参数可以设置成一个 布尔型数据,为 True 的话,自动重试的时间间隔会成倍的增长

第一次重试是 1 s后
第二次是 2s 后
第三次是 4s 后
第四次是 8s 后
...

如果 retry_backoff 参数是一个数字,比如是 3,那么后续的间隔时间则是 3 的倍数增长

第一次重试 3s 后
第二次是 6s 后
第三次是 12s 后
第四次是 24s 后

retry_backoff_max 是重试的最大的间隔时间,比如重试次数设置的很大,retry_backoff 的间隔时间重复达到了这个值之后就不再增大了。

这个值默认是 600s,也就是 10分钟。

我们看一下下面这个例子:

# tasks1.py

@app.task(autoretry_for=(Exception, ), retry_backoff=2, retry_backoff_max=40, retry_kwargs={'max_retries': 8})
def div(x, y):
    return x / y

关于重试的机制,理论上应该是按照我们前面列出来的重试时间间隔进行重试,但是如果我们这样直接运行 div.delay(),得出的间隔时间是不定的,是在 0 到 最大值之间得出的一个随机值。

这样产生的原因是因为还有一个 retry_jitter 参数,这个参数默认是 True,所以时间间隔会是一个随机值。

如果需要任务延时的间隔值是按照 retry_backoff 和 retry_backoff_max 两个设定值来运行,那么则需要将 retry_jitter 值设为 False。

# tasks1.py

@app.task(autoretry_for=(Exception, ), retry_backoff=2, retry_backoff_max=40, retry_jitter=False, retry_kwargs={'max_retries': 8})
def div(x, y):
    return x / y

然后运行 div 的延时任务,就可以看到延时任务按照规律的间隔时间重试了,以下是日志:

[2025-06-21 15:07:49,183: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:07:49,188: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:07:49,189: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 2s: ZeroDivisionError('division by zero')
[2025-06-21 15:07:51,188: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:07:51,189: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 4s: ZeroDivisionError('division by zero')
[2025-06-21 15:07:55,191: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:07:55,191: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 8s: ZeroDivisionError('division by zero')
[2025-06-21 15:08:03,194: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:08:03,195: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 16s: ZeroDivisionError('division by zero')
[2025-06-21 15:08:19,197: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:08:19,198: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 32s: ZeroDivisionError('division by zero')
[2025-06-21 15:08:51,200: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:08:51,200: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 40s: ZeroDivisionError('division by zero')
[2025-06-21 15:09:31,203: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:09:31,203: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 40s: ZeroDivisionError('division by zero')
[2025-06-21 15:10:11,205: INFO/MainProcess] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] received
[2025-06-21 15:10:11,206: INFO/ForkPoolWorker-16] Task pro.tasks1.div[af98a5a9-cfce-483d-a304-a25fa3e0f26a] retry: Retry in 40s: ZeroDivisionError('division by zero')

因为我们设置的重试间隔时间最大为 40s,所以这个地方延时间隔时间到了 40 之后,就不再往上继续增长了。

3.4 忽略任务运行结果

有时候延时任务的结果我们并不想保存,但是我们配置了 result_backend 参数,这个时候我们有三种方式不保存运行结果。

1.ignore_result=True 不保存任务运行的结果

@app.task(ignore_result=True)
def add(x, y):
    return x + y

2.app.conf 配置

也可以通过 app.conf 的配置来禁用结果的保存:

app.conf.update(
    task_ignore_result=True
)

3.执行单个任务的时候禁用

from pro.tasks1 import add
add.apply_async((1, 2), ignore_result=True)

apply_async() 函数的作用相当于是带参数的 delay(),或者 delay() 是简化版的 apply_async(),这个我们下面会介绍。

3.5 task 的调用

前面简单两个简单的调用方法,一个是 apply_async(),一个是 delay()。

简单来说就是 delay() 是不带参数执行的 apply_async()。

以下用 add() 函数为例介绍一下他们的用法:

delay()
纯粹的延时任务,只能如下操作:

add.delay(1, 2)

apply_async()
带参数的用法,add() 函数的参数用 () 包起来:

add.apply_async((1, 2))

也可以带其他参数,比如上面介绍的不保存运行结果:

add.apply_async((1, 2), ignore_result=True)

这个函数还可以指定延时的时间:
countdown参数
现在开始 10s 后开始运行:

add.apply_async((1, 2), countdown=10)

eta参数
也可以用 eta 参数来指定 10s 后运行:

from datetime import datetime, timedelta

now = datetime.now()
add.apply_async((1, 2), eta=now + timedelta(seconds=10))

expires参数

这个是用来设置过期的参数:

add.apply_async((1, 2), countdown=60, expires=120)

上面的参数表示,距现在60秒后开始执行,两分钟后过期

4 在 Django 系统中使用 celery

4.1 文件配置

4.2 task定义

4.3 运行worker

源码分析

posted @ 2025-06-20 09:20  harrylearn66666  阅读(60)  评论(0)    收藏  举报