Celery 分布式任务队列:我差点被这行代码坑死
那一天,我跟往常一样,对着屏幕上的代码烦躁不已。一个看似简单的任务调度突然死活不工作,排查了一整天,最后发现是 Celery 的一个配置项让我栽了跟头。如果你正准备或者已经开始使用 Celery,那么接下来的内容你肯定不想错过,因为它们可能会在你某个不眠之夜,成为那盏救你于水火的明灯。
事情是这样的,我在一个项目中需要处理大量图片上传后的处理任务,比如图片压缩、格式转换等。这些任务显然不适合在主线程中处理,那样会阻塞用户的交互,于是我想起了 Celery,一个基于 Python 的分布式任务调度工具。在我认为,这事儿肯定手到擒来,结果第一个配置就让我碰了一鼻子灰。
python
# celery.py
from celery import Celery
# 这里是关键,如果你的配置出了问题,后面的努力可能都是徒劳
app = Celery('tasks', broker='amqp://guest@localhost//', backend='rpc://')
我在本地测试时,一切正常,但一到生产环境,任务就挂在那里不动了。检查 RabbitMQ 的连接,没有问题;检查 Redis 的状态,也没问题。最后,我注意到 broker 和 backend 的配置,它们在生产环境和本地环境中的配置是不同的。本地使用的是 redis 作为 broker 和 backend,而生产环境却换成了 RabbitMQ 和 RPC。这个问题一度让我困扰不已,直到我找到了一篇老外写的博客,他提到 RPC 作为 backend 时,可能会有某些不明显的问题。于是,我果断将 backend 改为了 redis,问题迎刃而解。
python
# 修改后的 celery.py
from celery import Celery
app = Celery('tasks', broker='amqp://guest@localhost//', backend='redis://localhost:6379/0')
这个小插曲让我深刻意识到,Celery 的配置虽然简单,但一旦出错,排查起来可是相当麻烦的。为了避免大家走我走过的弯路,下面我将分享一些核心的 Celery 语法和常用示例,希望能帮你在任务调度的道路上少踩几个坑。
为什么选择 Celery?
在选择任务调度工具时,Celery 不是唯一的选择,但它的灵活性和社区支持确实让它在很多场景中大放异彩。Celery 支持多种消息代理(如 RabbitMQ、Redis、AWS SQS 等),可以轻松地与 Django、Flask 等框架集成,而且它的配置和使用也非常直观。但就如同我刚开始提到的,灵活性也意味着更多的配置选项,这需要你多加小心。
安装 Celery
安装 Celery 非常简单,如果你的环境中已经安装了 Python,那么只需要一行命令:
bash
pip install celery
接下来,根据你选择的消息代理,安装相应的库。例如,使用 RabbitMQ 作为消息代理:
bash
pip install celery[rabbitmq]
或者使用 Redis:
bash
pip install celery[redis]
基本配置
配置 Celery 时,最核心的部分是设置消息代理(broker)和结果后端(backend)。消息代理用于任务队列的管理,结果后端用于存储任务的执行结果。以下是一个最基本的配置示例:
python
# celery.py
from celery import Celery
app = Celery('tasks', broker='amqp://guest@localhost//', backend='redis://localhost:6379/0')
# 配置文件
app.config_from_object('django.conf:settings', namespace='CELERY')
这里的关键是 app.config_from_object,它告诉 Celery 从 Django 的配置文件中读取以 CELERY_ 开头的配置项。如果你的项目不是 Django,可以将配置项直接写在 celery.py 中:
python
# celery.py
from celery import Celery
app = Celery('tasks', broker='amqp://guest@localhost//', backend='redis://localhost:6379/0')
app.conf.update(
result_expires=3600, # 任务结果过期时间
worker_max_tasks_per_child=100, # 每个 worker 处理的任务数
task_serializer='json', # 任务序列化方式
accept_content=['json'], # 接受的内容类型
result_serializer='json', # 结果序列化方式
enable_utc=True # 启用 UTC 时间
)
定义任务
定义任务非常简单,只需要使用 @app.task 装饰器即可。以下是一个简单的任务示例:
python
# tasks.py
from .celery import app
@app.task
def add(x, y):
return x + y
这个 add 任务接收两个参数 x 和 y,返回它们的和。你可以通过 add.delay(x, y) 来异步调用这个任务。如果你需要获取任务的执行结果,可以使用 AsyncResult:
python
# 调用任务
result = add.delay(4, 5)
# 获取任务结果
print(result.get(timeout=1)) # 输出 9
任务队列
在 Celery 中,任务队列是非常重要的概念。你可以通过 queue 参数来指定任务应该发送到哪个队列中:
python
@app.task(queue='high-priority')
def high_priority_task():
print("这是高优先级任务")
@app.task(queue='low-priority')
def low_priority_task():
print("这是低优先级任务")
这里的 high-priority 和 low-priority 是两个不同的队列。你可以在启动 worker 时指定它应该从哪个队列中获取任务:
bash
celery -A your_project worker -l info -Q high-priority
这样,high-priority 队列中的任务将优先被处理。如果你启动的 worker 没有指定队列,它会默认处理所有的队列。
定时任务
Celery 的定时任务是通过 beat 来实现的。beat 是一个调度器,它会定期将任务发送到消息代理中。以下是一个简单的定时任务示例:
python
# celery.py
from celery import Celery
from celery.schedules import crontab
app = Celery('tasks', broker='amqp://guest@localhost//', backend='redis://localhost:6379/0')
app.conf.update(
beat_schedule={
'every-15-seconds': {
'task': 'your_project.tasks.check_status',
'schedule': 15.0,
'args': (),
},
'run-every-morning': {
'task': 'your_project.tasks.send_email',
'schedule': crontab(hour=7, minute=30, day_of_week=1),
'args': ('hello@example.com',),
},
},
)
在这个配置中,check_status 任务每 15 秒执行一次,而 send_email 任务则在每周一的早上 7:30 执行。启动 beat 时,使用以下命令:
bash
celery -A your_project beat -l info
任务链与组
在实际项目中,任务之间往往存在依赖关系,或者需要并行执行多个任务。Celery 提供了 chain 和 group 来处理这样的情况。
任务链 chain 可以让你定义一系列任务,前一个任务的输出将成为下一个任务的输入:
python
# tasks.py
from .celery import app
@app.task
def add(x, y):
return x + y
@app.task
def mul(x, y):
return x * y
@app.task
def sub(x, y):
return x - y
# 使用任务链
result = (add.s(4, 5) | mul.s(2) | sub.s(1)).delay()
print(result.get()) # 输出 (4 + 5) * 2 - 1 = 17
任务组 group 则可以让你并行执行多个任务,最后将所有任务的结果汇总:
python
# 使用任务组
from celery import group
result = group([add.s(i, i) for i in range(10)]).delay()
print(result.get()) # 输出 [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
任务重试
在处理任务时,可能会遇到一些意外的错误,比如网络问题、数据库连接问题等。Celery 提供了任务重试机制,让你的任务更健壮。以下是一个示例:
python
@app.task(bind=True, max_retries=3, default_retry_delay=30)
def risky_task(self, x):
try:
# 这里是可能会出错的代码
1 / x
except ZeroDivisionError as exc:
# 任务重试
self.retry(exc=exc)
在这个示例中,risky_task 任务如果遇到 ZeroDivisionError,将会自动重试最多 3 次,每次重试之间间隔 30 秒。bind=True 是为了将任务实例绑定到 self 参数上,这样你就可以在任务内部调用 self.retry。
任务超时
有时候,任务的执行时间可能会超出预期,这时候你可以设置任务的超时时间。如果任务超时,Celery 会自动终止任务并返回一个超时错误。以下是一个示例:
python
@app.task(time_limit=30, soft_time_limit=25)
def long_running_task(x):
import time
time.sleep(x)
return x
在这个示例中,long_running_task 任务的硬超时时间是 30 秒,软超时时间是 25 秒。硬超时意味着任务的执行时间不能超过 30 秒,否则任务将被强制终止。软超时则会在任务执行时间超过 25 秒时,发送一个超时警告,但不会立即终止任务。
监控与日志
在生产环境中,监控和日志是非常重要的。Celery 提供了多种方式来监控任务和记录日志。最简单的方式是通过 celery -A your_project worker -l info 命令启动 worker,这样会将日志输出到控制台。你还可以将日志输出到文件中:
bash
celery -A your_project worker -l info --logfile=celery.log
此外,Celery 还支持通过 Flower 来进行可视化监控。安装 Flower:
bash
pip install flower
启动 Flower:
bash
celery -A your_project flower --port=5555
然后在浏览器中访问 http://localhost:5555,你就可以看到任务的执行情况、worker 的状态等信息。
踩坑经验分享
在使用 Celery 的过程中,我遇到了不少坑,这里分享几个常见的问题和解决方法:
-
任务挂起 :如果你的任务在某个 worker 上挂起,可能是由于任务的执行时间过长,或者 worker 处理任务的数量达到上限。你可以通过设置
worker_max_tasks_per_child来限制每个 worker 处理的任务数,这样可以避免 worker 长时间占用资源。 -
任务丢失:如果你发现有些任务发送了但没有被执行,可能是由于消息代理的配置问题。确保你的消息代理(如 RabbitMQ、Redis)已经正确配置,并且 Celery 能够连接到它们。
-
结果后端问题 :我之前遇到的问题就是典型的例子。如果你使用的是
RPC作为结果后端,可能会遇到一些不明显的问题,建议使用Redis或MySQL作为结果后端。 -
任务重试:在任务重试时,确保你的任务是幂等的,即多次执行同一个任务不会产生不同的结果。否则,可能会导致数据不一致的问题。
-
任务状态 :Celery 提供了多种任务状态,如
PENDING、STARTED、SUCCESS、FAILURE等。你可以在任务中使用self.update_state来更新任务状态,这样可以在 Flower 中更直观地看到任务的执行情况。
结合 Django 使用
如果你的项目是 Django 项目,Celery 的集成将会更加方便。以下是一个 Django 项目的 Celery 配置示例:
python
# your_project/celery.py
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
# 设置 Django 配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings')
app = Celery('your_project')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))
python
# your_project/settings.py
CELERY_BROKER_URL = 'amqp://guest@localhost//'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_ENABLE_UTC = True
在 Django 项目中,你只需要导入 os 模块并设置 DJANGO_SETTINGS_MODULE 环境变量,然后使用 app.autodiscover_tasks() 自动发现项目中的任务。
实战技巧
-
使用虚拟环境:在开发和部署 Celery 时,建议使用虚拟环境来管理依赖,避免环境冲突。
-
测试任务 :在开发过程中,可以通过
apply_async的link和link_error参数来测试任务的成功和失败处理逻辑。 -
任务监控:使用 Flower 来监控任务的状态,可以帮助你快速发现和解决问题。
-
任务跟踪:在任务中记录更多的日志信息,比如任务的输入参数、执行时间等,方便调试和排查问题。
推荐工具:Hey Cron
在处理定时任务时,Cron 表达式是一个非常重要的概念。但很多时候,编写 Cron 表达式并不是一件容易的事,尤其是在需要精确到分钟甚至秒的情况下。这里我要推荐一个非常实用的工具------Hey Cron。
Hey Cron 提供了一个非常友好的 Cron 表达式生成器,你只需要输入中文描述,它就能自动转换成 Cron 表达式。例如,输入"每分钟执行",它会生成 * * * * *。
此外,Hey Cron 还提供了其他实用工具,如正则表达式生成器、中英互译、JSON 格式化、Base64 编码解码、时间戳转换、JWT 解析等。这些工具在日常开发中非常有用,特别是当你需要快速生成或解析某些格式的数据时。
结语
通过这次踩坑经历,我深刻地认识到了配置和调试在任务调度中的重要性。希望我的这些经验和示例能帮助你在使用 Celery 时少走弯路。如果你在使用过程中遇到任何问题,不妨先查查配置,很多时候问题就出在这里。
如果你还在为编写 Cron 表达式而头疼,不妨试试 Hey Cron,它会是你的好帮手。祝你开发顺利,少踩几个坑!