8核实例docker-compose部署下Dramatiq同一任务被多进程重复执行问题
故障日志示例:
"INFO | 10/06/2021 10:30:09 | 139698903181056 | app.orders.tasks | tasks | tasks.py | 22 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:09 | 139698894788352 | app.orders.tasks | tasks | tasks.py | 22 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:08 | 139698911573760 | app.orders.tasks | tasks | tasks.py | 18 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:07 | 139699123865344 | app.orders.tasks | tasks | tasks.py | 19 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:03 | 139698903181056 | app.orders.tasks | tasks | tasks.py | 21 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:02 | 139698374702848 | app.orders.tasks | tasks | tasks.py | 16 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:01 | 139698903181056 | app.orders.tasks | tasks | tasks.py | 19 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:01 | 139698903181056 | app.orders.tasks | tasks | tasks.py | 21 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:01 | 139698307593984 | app.orders.tasks | tasks | tasks.py | 21 | 39 | send_menu | Sending menu #87..." "INFO | 10/06/2021 10:30:00 | 139698861217536 | app.orders.tasks | tasks | tasks.py | 22 | 39 | send_menu | Sending menu #87..."
故障根因
- 调度器多实例重复触发:
BackgroundScheduler的初始化逻辑嵌入在了dramatiq worker进程会加载的公共代码中,dramatiq默认会启动和CPU核心数(本次为8核)匹配的工作进程,每个进程都会独立启动一个调度器实例。即使数据库中仅存1条任务配置,多个调度器也会各自独立触发任务投递,这是重复执行的核心原因。 - 消息消费重投:如果RabbitMQ的消费者确认机制配置不合理,比如dramatiq worker拉取消息后未及时提交ack就触发超时,消息会被RabbitMQ重新投递给其他worker,进一步放大重复执行的问题。
- 不同进程出现相同线程ID属于操作系统正常逻辑:线程ID为进程内唯一标识,跨进程不做全局去重,不属于故障范畴。
修复方案
1. 调度层修复(优先级最高)
- 单独拆分调度服务:在
docker-compose.yml中新增独立的调度服务,仅在该服务中初始化并启动BackgroundScheduler,所有dramatiq worker的代码中移除调度器启动逻辑,从根源避免多进程调度重复触发任务。 - 若暂时不拆分服务,可新增调度锁逻辑:在任务触发入口增加基于PostgreSQL的分布式锁,同一时间仅允许一个进程触发该任务,或执行任务前先校验对应业务记录的状态,若已触发/已执行则直接终止流程。
2. 消息队列配置优化
- 启动dramatiq worker时添加
--prefetch-count 1参数,限制单个worker预取消息数量,避免消息堆积在worker本地触发超时重投。 - 给
send_menu任务调整重试配置:添加@dramatiq.actor(max_retries=0)完全关闭重试,或根据业务场景调整重试间隔与次数。 - 若
send_menu任务执行时长超过30秒,调整RabbitMQ的消费者超时参数,避免未执行完成就被判定为消费失败。
3. 业务层兜底
给send_menu任务增加幂等校验逻辑,以菜单ID作为唯一标识,执行前先查询发送记录,若已发送成功直接返回,避免重复推送对用户造成干扰。
内容的提问来源于stack exchange,提问作者Rodrigo Suarez
相关产品推荐
相关产品推荐

