1. Celery 介绍
Celery 的分布式表明多个客户端可同时访问;它的异步性体现在:Celery 可以将生产完毕的任务放入消息队列,待消费端空闲后可自行去取,而不必阻塞生产端的运行。
Celery 本身不提供消息队列的功能,通常通过中间件(broker)实现,常用中间件有 RabbitMQ 和 Redis 等。同时为了便于查看任务执行过程中的状态,还需要存储器来保存每次任务的执行结果,可以使用各类数据库以及 Redis 等。
2. 基本使用
结合上一节的内容,首先定义一个 Celery 对象,中间件和存储(backend)均使用 Redis。
broker = 'redis://127.0.0.1:6379/1'
backend = 'redis://127.0.0.1:6379/2'
celery = Celery(main=__name__, broker=broker, backend=backend)
初始化 Celery 对象后,通常使用装饰器指定可被 Celery 调度的任务。
@celery.task()
def add(x, y):
return x + y
然后,通过命令行启动 Celery 服务。
celery worker -A tasks -l info
其中,-A 用于指定 Celery 实例、worker 启动 Celery 服务、-l 指定日志等级。
$ celery -A main worker -l info
-------------- celery@${machinename} v5.2.7 (dawn-chorus)
--- ***** -----
-- ******* ---- Linux-5.15.0-41-generic-x86_64-with-debian-bullseye-sid 2022-07-31 10:38:07
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: main:0x7f9ab8843f90
- ** ---------- .> transport: redis://127.0.0.1:6379/1
- ** ---------- .> results: redis://127.0.0.1:6379/2
- *** --- * --- .> concurrency: 16 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. main.add
[2022-07-31 10:38:08,160: INFO/MainProcess] Connected to redis://127.0.0.1:6379/1
[2022-07-31 10:38:08,161: INFO/MainProcess] mingle: searching for neighbors
[2022-07-31 10:38:09,168: INFO/MainProcess] mingle: all alone
[2022-07-31 10:38:09,177: INFO/MainProcess] celery@cn0014010240l ready.
借由 Celery 提供的分布式功能,当有多个应用程序调用任务时,为了保证调用不出错,Celery 可指定队列名来确定应用程序操作指定任务。在上述启动 Celery 时,选项 [queues] 表明默认使用的队列是 celery,在启动时也可通过 -Q 命令指定监听的队列。
在输出的 [tasks] 选项可看到当前可调用任务为 main.add。然后在应用程序中通过 delay 或 apply_async 调用指定任务。
>>> from main import add
>>> r = add.delay(2, 6)
>>> r.ready() # 判断当前任务是否执行完毕
True
>>> r.get() # 获取任务执行结果
8
其他的,可使用 r.state 或 r.status 获取任务状态。
3. 进阶使用
3.1 跨机通信
Celery 的一大特点是异步性,通过中间件,生产端和客户端可以实现独立运作。
首先在客户端发送任务到消息队列,其中包含任务中函数的名字以及对应的参数,后续生产端去拿函数来执行即可(这里的生产端指函数实现端、消费端指调用端)。为了使生产端和客户端跨机通信,我们放弃使用 from xx import xx
的方式导入任务函数。
要实现上述功能,首先在客户端一侧的 app_client.py 定义一个任务函数,其中不实现任何功能。
from celery import Celery
celery_app = Celery(main=__name__,
backend='redis://127.0.0.1:6379/0',
broker='redis://127.0.0.1:6379/1')
# 为了实现跨机通信,使用 name 字段将生产端和客户端指定为相同的名字
# name 的优先级高于 报名.文件名.add 的形式
@celery_app.task(name="csdn.blog.add")
def add(*args, **kwargs):
pass
然后,在客户端 client.py 中往消息队列发送任务。
from app_client import add
if __name__ == "__main__":
ii, jj = [2, 3, 4, 5, 6, 7, 8, 9, 10], [7, 8, 9, 10, 11, 2, 3, 4, 5]
t1res = []
for i in range(9):
# 或使用 delay
res = add.apply_async((ii[i], jj[i]))
上述代码通过循环往消息队列中发送 9 条数据,运行后打开 redis-cli 查看。
# 前面使用 6379:1 作为中间件,首先切换到 1
127.0.0.1:6379> select 1
OK
# 使用 keys 查看所有键值,可以看到还没被 Celery 取走的默认队列 celery
127.0.0.1:6379[1]> keys *
1) "celery"
2) xxx
# 使用 type 查看键类型
127.0.0.1:6379[1]> type celery
list
# 其类型是列表,查看它的长度,和刚才我们发送的请求数一致
127.0.0.1:6379[1]> llen list
(integer) 9
# 使用 lindex celery 0 查看索引为 0 的内容,其他类似
127.0.0.1:6379[1]> lindex celery 0
xxx # 内容太多了,这里不贴了,其中 argsrepr 项表明当前项参数为 (10, 5)
现在,消息队列里有 9 个刚才发送进去而待取出的任务函数,现在编写生产端(函数实现)app_server.py 代码。这里,将 app_client.py 里面的代码拷贝过来,然后实现函数即可。
from celery import Celery
celery_app = Celery(main=__name__,
backend='redis://127.0.0.1:6379/0',
broker='redis://127.0.0.1:6379/1')
# 为了实现跨机通信,使用 name 字段将生产端和客户端指定为相同的名字
@celery_app.task(name="csdn.blog.add")
def add(x, y):
print(f'x: {x}, y: {y}\n')
return x + y
然后通过命令运行 Celery,从打印内容可以看到在启动后,Celery 会自动去消息队列拿函数来执行。
celery worker -A app_server.celery_app -l info
现在去 redis-cli 查看键值,发现 celery 已经不存在,表明其中内容已全部执行完毕。
3.2 定时任务
接着上一节的内容,在跨机间实现任务定时功能,首先始终开启生产端的执行。
celery worker -A app_server.celery_app -l info
然后在 app_client.py 中加入定时属性。
from celery import Celery
from datetime import timedelta
from celery.schedules import crontab
celery_app = Celery(main=__name__,
backend='redis://127.0.0.1:6379/0',
broker='redis://127.0.0.1:6379/1')
celery_app.conf.update(
# 如果设置某时某分执行任务,需要统一到北京时间
timezone='Asia/Shanghai',
beat_schedule={
# 任务列表,可设置多个任务
"task1": {
# 任务函数,和生产端的同名
"task": "csdn.blog.add",
# 时间属性,使用 timedelta 设置执行时间间隔、crontab 设置执行时间点
# 每隔 10 秒执行一次
"schedule": timedelta(seconds=10),
# 每周一的上午 6:00 执行一次
# "schedule": crontab(hours=6, minutes=0, day_of_week=1)
# 任务函数的执行参数
"args": (2, 3)
},})
然后在生产端启动 Celery 的定时任务。
celery beat -A app_client.celery_app -l info
从打印结果可以看到,每隔 10 秒向消息队列写入任务函数及其参数,生成端也接收到了任务。
celery beat v4.4.0 (cliffs) is starting.
__ - ... __ - _
LocalTime -> 2022-08-20 10:19:01
Configuration ->
. broker -> redis://127.0.0.1:6379/1
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2022-08-20 10:19:01,157: INFO/MainProcess] beat: Starting...
[2022-08-20 10:19:01,451: INFO/MainProcess] Scheduler: Sending due task task1 (csdn.blog.add)
[2022-08-20 10:19:11,428: INFO/MainProcess] Scheduler: Sending due task task1 (csdn.blog.add)
[2022-08-20 10:19:21,429: INFO/MainProcess] Scheduler: Sending due task task1 (csdn.blog.add)
其他的可以设置更复杂的定时任务,如在 beat_schedule 选项中加入更多任务、在任务中加入更多参数等。
4. 总结
本文介绍了 Celery 的安装和基本使用方法,官方文档:https://docs.celeryq.dev/en/stable/index.html