根本原因是Django崩溃导致任务状态未同步,Broker中任务仍为PENDING/RECEIVED但无Worker消费;必须显式配置task_time_limit、task_soft_time_limit和broker_transport_options.visibility_timeout三项超时参数,并结合主动扫描清理与幂等设计防重复执行。

为什么Django崩溃后Celery任务会变僵尸?
根本原因是任务状态未同步:Django进程意外退出(如OOM、SIGKILL、gunicorn worker被杀)时,apply_async()已发出去的任务在Broker(如Redis/RabbitMQ)里仍为PENDING或RECEIVED,但Worker进程已消失,无人消费。这些任务不会自动超时或失败,卡在队列里,形成“僵尸”。
Celery配置必须加的三项超时参数
光靠默认配置扛不住Django崩溃——必须显式约束任务生命周期:
-
task_time_limit:硬性中断任务执行(SIGTERM后强制SIGKILL),单位秒。设为300(5分钟)较稳妥,避免长任务无限挂起 -
task_soft_time_limit:软超时,触发SoftTimeLimitExceeded异常,留给任务自己清理资源。建议比硬限低30秒,如270 -
broker_transport_options中配visibility_timeout(Redis)或message_ttl(RabbitMQ):控制任务从Broker取出后多久未ack就自动重回队列。Redis默认1小时,必须调低到略大于task_time_limit,比如360
示例(celery.py):
app.conf.update(
task_time_limit=300,
task_soft_time_limit=270,
broker_transport_options={'visibility_timeout': 360},
)
如何主动识别并清理残留僵尸任务?
不能只靠超时——Django崩溃可能瞬间压垮多个Worker,得有兜底扫描机制:
立即学习“Python免费学习笔记(深入)”;
- 用
celery inspect active_queues确认当前活跃队列,再用redis-cli -h HOST -p PORT llen celery查队列长度是否异常堆积 - 对Redis Broker,直接用
redis-cli keys "celery-task-meta-*"扫所有任务元数据,结合hgetall查status字段,过滤出PENDING超过2小时的旧任务(用hget取date_done或created_at字段判断) - 清理脚本别直接
DEL——先hget确认状态,再HDEL元数据+LREM队列里的原始消息(需解析Redis list结构,推荐用celery control cancel_task <task_id>更安全)
重启后如何防止任务重复执行?
僵尸任务被清理后,新Worker启动可能重复拉取同一任务(尤其Redis visibility_timeout设置不当)。关键在幂等设计:
- 任务函数开头立刻写数据库标记
task_id为PROCESSING,用select_for_update()或唯一索引防并发 - 不要依赖
task_id做业务主键——它只是调度ID;业务实体必须有独立、确定的ID(如订单号) - 所有外部调用(HTTP/API/DB写入)加
if not exists或ON CONFLICT DO NOTHING,容忍重复提交
最易忽略的一点:task_id在Django崩溃前可能已生成但未落库,重启后新任务用相同task_id重试,此时旧元数据若未清干净,AsyncResult(task_id).state可能返回错误状态。务必确保元数据清理和业务状态更新原子化。


















