Celery任务队列怎么配置才不丢任务

文章导读
Celery 丢任务,多数情况并不是消息被“吃掉”了,而是在几个交接环节上没人接手:消息进入 broker 前没有持久化、Worker 取到消息后进程被杀、任务失败后没有重试或记录、可见性超时把还在执行的任务又投递了一次。要减少丢任务,通常要把“至少投递一次 + 任务可重复执行”当成一个组合来做,而不是只打开某一个开关。先确认三件事:broker 用的是 RabbitMQ 还是 Redis、队列和
📋 目录
  1. 壹 先确认当前场景和目标
  2. 贰 按这个顺序处理
  3. 叁 改完后怎么验证
  4. 肆 容易忽略的边界
A A

Celery 丢任务,多数情况并不是消息被“吃掉”了,而是在几个交接环节上没人接手:消息进入 broker 前没有持久化、Worker 取到消息后进程被杀、任务失败后没有重试或记录、可见性超时把还在执行的任务又投递了一次。要减少丢任务,通常要把“至少投递一次 + 任务可重复执行”当成一个组合来做,而不是只打开某一个开关。先确认三件事:broker 用的是 RabbitMQ 还是 Redis、队列和消息是否持久化、业务任务重复执行一次会不会出问题。第三点决定你能不能使用 late ack。

要让 Celery 尽量不丢任务,通常需要同时做三件事:让 broker 侧持久化生效;把关键任务设为 late ack,并在 Worker 异常退出时把消息放回队列;业务侧把任务写成可重复执行。只改 acks_late 而不做幂等,超时重投会产生重复执行;只做幂等而不改 ack 时机,被 kill 的任务仍然会直接消失。验证方式是杀掉 Worker 进程后看队列是否回补、任务是否重跑,不能只看日志里出现过 SUCCESS。

先确认当前场景和目标

“不丢任务”在不同环境里的含义不一样,先把判断条件列清楚再动手,可以避免改出一堆副作用:

  • broker 类型:RabbitMQ 的持久化由队列 durable 和消息 delivery mode 决定;Redis 作为 broker 依赖持久化配置和可见性超时机制,两者语义有差别,处理方式不能互相照搬。
  • 任务能否重复执行:发短信、扣款、写流水这类不可重入的操作,必须先有幂等键或去重记录,否则不要急着开 late ack。
  • 任务最长执行时间:它直接决定可见性超时该设多大。执行时间超过超时值,消息会被重复投递。
  • 失败后要不要人工介入:失败任务直接丢弃,还是落到重试队列或失败记录表,这决定是否需要死信或补投机制。
  • 现状配置:先查看现有的 task_acks_late、task_reject_on_worker_lost、worker_prefetch_multiplier 是否已被改过,避免叠加冲突。

如果任务是可重复的、允许重跑,就可以采用“至少一次投递 + 幂等”的路线;如果任务本身不可重入,又暂时加不上幂等,只能保守处理:先保证 broker 持久化,接受极端情况下少量丢失,而不是用不完整的 late ack 换来大量重复单。

按这个顺序处理

建议按“先 broker、再 Worker 的 ack 策略、再任务代码、最后是兜底记录”的顺序推进。每一步改动范围小,出问题时也好回退。

Celery任务队列怎么配置才不丢任务

第一步:确认 broker 侧真的把消息落盘了

RabbitMQ 需要队列声明为 durable、消息以 persistent 模式投递,否则 broker 重启后队列和消息都会消失。Redis 作为 broker 时,需要开启 AOF,并确认内存淘汰策略不会把队列键驱逐掉;如果淘汰策略配成随机或 LRU 类,队列键可能被清掉,这种丢失在应用日志里看不出来。下面这段配置只是占位示例,连接串和超时值需要按自己的环境替换:

# celeryconfig.py,键名请按自己安装的版本核对
broker_url = "amqp://user:pass@broker-host:5672//"
result_backend = "redis://backend-host:6379/1"

task_default_queue = "default"
task_default_delivery_mode = "persistent"   # 持久化投递

broker_transport_options = {
    "visibility_timeout": 3600,   # 必须大于任务最长执行时间
    "max_retries": 3,
}

第二步:调整 ack 时机和预取数量

默认情况下,Worker 取到消息后就会 ack,进程一旦被 kill 或机器重启,这条消息就没有了。改成执行完再 ack,并允许 Worker 异常退出时把消息放回队列:

task_acks_late = True                   # 任务执行完再确认
task_reject_on_worker_lost = True       # 子进程被强杀时把消息退回队列
task_acks_on_failure_or_timeout = True  # 明确失败也 ack,由重试机制接手
worker_prefetch_multiplier = 1          # 长任务减少一次性预取
worker_max_tasks_per_child = 200        # 定期回收子进程,降低内存泄漏影响

prefetch_multiplier 调小之后,单个 Worker 手里暂存的消息变少,进程被杀时“取走但未执行”的消息也相应减少。这里会牺牲一点吞吐,需要结合任务耗时决定。

Celery任务队列怎么配置才不丢任务

第三步:把任务写成可重复执行

late ack 的代价是可能重复执行,所以任务本身要有幂等保护。下面的骨架是通用写法,存储部分要换成你自己的数据库或缓存:

@app.task(
    bind=True,
    acks_late=True,
    autoretry_for=(Exception,),
    retry_backoff=True,
    retry_kwargs={"max_retries": 5},
)
def settle_order(self, order_id):
    # 幂等检查:已处理过就直接返回,避免重投造成重复记账
    if is_settled(order_id):
        return "skipped"
    do_settle(order_id)      # 业务操作
    mark_settled(order_id)   # 与业务操作尽量放在同一事务或补偿逻辑里
    return "done"

注意业务写入和幂等标记之间的顺序:先写标记再执行、或先执行再写标记,都会在异常中断时留下不一致。通常建议用唯一约束或状态机来保证同一笔业务只会生效一次。

Celery任务队列怎么配置才不丢任务

第四步:给失败和超时留出口

重试次数耗尽的任务不要只停留在日志里。可以把它写入一张失败任务表,或者路由到一个单独的低优先级队列,方便人工补跑。这一步和“不丢任务”直接相关:没有出口的失败,本质上等同于丢失。

改完后怎么验证

验证要模拟真实故障,而不是看任务有没有正常跑完。可以先用一条能识别的测试任务(例如往一张测试表写一条带唯一标记的记录)走完整条链路:

  1. 投递任务后,在 broker 侧查看队列积压:RabbitMQ 可以用队列列表命令,Redis 可以用 LLEN 查看对应键长度。先确认消息确实进入了队列。
  2. 在 Worker 执行过程中执行 kill -9 杀掉子进程,模拟异常退出。
  3. 再次查看队列长度和 Worker 的 reserved 状态,预期是消息数量回补或任务重新出现在 reserved 中,而不是就此归零。
  4. 重启 Worker 后,查看任务是否被重新执行,同时检查测试表里是否只有一条记录,以确认幂等生效。
  5. 把 visibility_timeout 临时设置为小于任务耗时的值,观察超时后是否出现重复投递,以确认这个参数必须按最长耗时来设置。

预期信号是:Worker 被杀不会导致任务凭空消失、任务重新执行但业务结果不重复、失败任务能在失败表或死信队列里查到。若杀进程后队列长度直接归零,说明 ack 时机或 broker 持久化还有一项没生效。

容易忽略的边界

  • 不可重入任务:扣款、发券这类操作如果还没做幂等,不要一开始就在全局开启 late ack,可以只对可重复的任务单独声明 acks_late=True。
  • 可见性超时:设置得比任务执行时间短,会持续产生重复投递;设置得过大,Worker 崩溃后消息要等很久才回补,恢复时间会变长。需要按任务实际耗时留足余量。
  • Redis 作为 broker:可见性机制和 ack 语义与 RabbitMQ 不同,长任务要结合超时值单独评估;内存淘汰策略和持久化配置不当造成的丢失,不在应用层可见。
  • 结果后端丢失:result backend 丢数据只影响状态回查,不影响任务执行本身,排查时不要把它和任务丢失混为一谈。
  • 监控中的未确认状态:开启 late ack 后,执行中的任务会停留在 unacked 状态,看板上的“积压”数值会偏高,需要区分是正在执行还是真的堆积。
  • 定时调度:beat 侧的调度记录和队列任务不是一回事,调度器重启可能造成漏触发,需要单独检查。
  • 版本差异:不同大版本的配置项名称和默认值有变化,改之前先按自己安装的版本核对键名,避免写了不生效的配置还以为已经改好。
  • 回退建议:如果开启 late ack 后出现大量重复执行,先关掉 task_reject_on_worker_lost 止血,再补幂等逻辑;不要用加大 prefetch 或延长超时来掩盖重复问题。