[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秒后开始执行,两分钟后过期

浙公网安备 33010602011771号