先把当时的情况摆出来。Celery 5.3 配了 4 个 worker 消费同一个队列,一条 40 秒的录音转写任务在日志里跑了 4 遍,broker 是 Redis,visibility_timeout 还是默认的 3600。

排查方向其实有限,先把可能性排一排。任务没开 acks_late,worker 崩溃后消息被重新投递。还有一种是broker_transport_options 里的 visibility_timeout 与任务时长不匹配。

另外,多个 worker 共用队列名但没有做幂等。少数情况下是prefetch_multiplier 默认 4,一个 worker 先抓走四条。

第一件事是把关键配置打印出来核对

python -c "from celery import Celery;app=Celery('t');app.conf.update(task_acks_late=True,worker_prefetch_multiplier=1);print(app.conf.task_acks_late,app.conf.worker_prefetch_multiplier)"

给任务加一个 Redis 幂等锁

redis-cli SET lock:job:8821 1 NX EX 600

然后把 visibility_timeout 调到大于最长任务时长

redis-cli CONFIG SET visibility_timeout 7200

等前面都确认过,再重启 worker 后观察同一条任务的出现次数

celery -A tasks inspect active

中间停过一次,把已经改的东西回滚干净,再重新来一遍。

恢复的判定其实很直接。同一条任务在日志里只出现一次,Redis 里的 lock 键在任务结束后被释放,worker 重启也不再触发重投,重复率降为零。

补一句。Redis 做 broker 时消息的可见性靠超时兜底,任务比超时还长就一定会被别的 worker 再拿到,幂等锁比调参数更可靠。这类问题最好一次改到位,反复小改反而容易把原因搅混。

作者 9823189