为什么Celery任务会静默丢失?
Celery是Python生态最流行的分布式任务队列,但不少新手在刚上手时都遇到过任务"神秘消失"的情况:日志没报错,任务却始终没执行。这往往不是Celery本身的问题,而是我们忽略了它的ack机制 和消息持久化的默认行为。
Celery的Worker从Broker(比如Redis)取走任务后,会先进入内存队列,然后才开始执行。默认情况下,Worker在任务执行完成后 才向Broker发送ack确认(即task_acks_late=True时)。如果Worker在执行过程中被强杀(比如OOM、手动kill),任务就被认为未确认,Redis会重新投递。但如果你的配置是task_acks_late=False(默认值),Worker一取走任务就立即确认,此时任务执行失败或Worker崩溃,消息就永远消失了。
最小可运行示例:Celery + Redis Stream
为了彻底搞懂,我们构建一个最小示例:用Celery生产任务,用Redis Stream做Broker,并模拟Worker崩溃来观察行为。
首先,安装依赖:
pip install celery redis
创建tasks.py:
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379/0', backend='redis://localhost:6379/1')
@app.task(bind=True, max_retries=3)
def add(self, x, y):
if x == 1 and y == 1:
raise ValueError('故意出错')
return x + y
启动Worker(注意先启动Redis):
celery -A tasks worker --loglevel=info --concurrency=1
在另一个终端调用:
python -c "from tasks import add; result = add.delay(1, 1); print(result.id)"
如果默认配置,Worker会立即ack,任务执行抛错后不会重试,任务状态变为FAILURE,但不会消失。但如果Worker在任务执行到一半时被kill -9,默认配置下任务就永久丢失了。让我们改为晚确认:
app.conf.task_acks_late = True
app.conf.worker_prefetch_multiplier = 1 # 每次只取一个任务
重启Worker,再执行任务,然后立刻强杀Worker,你会发现任务被Redis重新投递,Worker重启后任务会再次执行。
深入原理:Redis Stream的消费组机制
Redis Stream是Redis 5.0引入的消息队列,Celery 5.0+支持它作为Broker。Stream提供了消费组(Consumer Group),每个消息只能被组内一个消费者消费,且消费后需要显式发送XACK确认。这正好对应Celery的ack机制。
在Celery中,每个Worker进程相当于一个消费者,它从Stream的pending队列读取消息,执行任务,然后发送XACK。如果Worker崩溃,pending列表中的消息不会被删除,会被重新分配给其他消费者(使用XCLAIM命令)。这正是我们上面实验看到的现象。
实战建议:如何避免任务丢失
- 开启task_acks_late:确保任务执行成功后才确认,否则Worker崩溃任务可重投。
- 使用Redis Stream并合理配置 :
task_acks_on_failure_or_timeout设为False,这样失败任务也会重新投递(配合重试)。 - 持久化Redis:配置AOF和RDB,防止Redis重启导致消息丢失。
- 监控pending队列 :定期用
XINFO GROUPS查看pending数量,发现积压及时排查。
最后,给出一个完整的检查命令:
redis-cli XINFO GROUPS celery # 查看消费组和pending数
redis-cli XPENDING celery my_group # 查看未确认消息详情
理解这些机制后,你的任务队列就会稳健许多。欢迎在评论区分享你遇到的"丢任务"案例。