公司动态

我的踩坑实录:300个数据同步任务,我为什么弃用Celery

📅 2026/7/21 15:31:51
我的踩坑实录:300个数据同步任务,我为什么弃用Celery
我的踩坑实录300个数据同步任务我为什么弃用Celery前不久接了个物流公司的内部系统改造项目客户不想明说具体名字我就含糊带过吧。他们有几套老旧的ERP和仓储平台每天要往新上的数据中台灌入库存、订单、物流轨迹这些基础数据。说实话这活儿听着简单但一旦量上来就头大——光是定时同步任务就有300多个而且每个任务的时间窗口不一样有的每小时跑一次有的凌晨批量跑。项目规模不大服务器就两台一台跑业务一台专门跑同步服务。方案选型别被架构复杂度劝退刚开始做技术选型的时候我脑子里第一个蹦出来的就是Celery。毕竟这玩意儿在Python圈子里名气太大分布式、异步、消息队列感觉啥都能干。Celery 5.4.x版本早就加了健康检查和改进的心跳机制文档里写得那叫一个漂亮。我甚至已经开始想怎么部署Redis做Broker、怎么用Django-Celery-Beat配CRON了。但是试了一圈发现这项目真的用不着这么重的家伙。我们的300多个任务虽然多但本质上都是“拉数据、清洗、入库”这种标准流程单个任务执行时间基本控制在5分钟以内。服务器就两台不存在跨机器调度需求。如果硬上Celery为了维持一个任务队列我得额外拉起Redis、Worker、Beat光运维成本就得翻一番。这时候我注意到了APScheduler 3.10.x的最新动态。说实话这个库我一直以为是个小众玩具结果3.10.0版本做了不少硬核改动。它把Redis和MongoDB的JobStore从核心模块移到了独立的apscheduler.jobstores包里依赖关系清爽了很多。更重要的是它对AsyncIOScheduler的支持明显增强非常适合我们这种基于FastAPI搭建的现代应用。我当时纠结了两天方案A是用Celery Redis方案B是APScheduler PostgreSQL。最终选了B原因很简单APScheduler自带Trigger机制对于这种单机多任务的场景它能以更低的资源占用完成调度。不需要消息队列的中间环节任务直接从内存触发到执行器延迟更低排查问题也更方便。代码写起来也很顺手大概长这样pythonfrom apscheduler.schedulers.asyncio import AsyncIOSchedulerfrom apscheduler.triggers.cron import CronTriggerfrom apscheduler.jobstores.memory import MemoryJobStorescheduler AsyncIOScheduler(jobstores{default: MemoryJobStore()})模拟一个每小时执行的数据同步任务scheduler.add_job(funcsync_inventory_data,triggerCronTrigger(hour*, minute0),idinventory_sync,max_instances1,coalesceTrue)scheduler.start()这段代码看着挺干净但我当时觉得这样就行结果后面还是栽了跟头。跑起来才发现的坑项目上线第一周我就收到了运维的报警。说是同步服务的CPU占用率偶尔会飙到90%以上而且日志里出现了大量重复执行的记录。我一开始以为是网络波动导致任务超时于是手动去服务器上查进程结果发现同一个任务在同一时刻竟然有两个实例在跑。这就很离谱了明明我在add_job的时候设置了max_instances1怎么还会重叠后来翻遍了APScheduler的文档结合源码看了一遍才发现问题出在coalesce和触发器精度上。我们的某些任务用的是IntervalTrigger间隔设的300秒但由于服务器负载高任务执行时间超过了间隔时间。这时候如果没配好next_run_time的计算逻辑调度器可能会认为之前的任务已经“错过”了于是直接再塞一个进去。坑死了这玩意儿在官方文档的FAQ里藏得可太深了。排查过程其实挺折磨人的。我先是在任务函数里加了分布式锁试图用Redis锁来防重。但这属于治标不治本万一锁过期了或者网络抖动还是会有并发。后来我把方案改成了纯靠APScheduler自身的配置来规避。关键配置调整如下pythonscheduler.add_job(funcheavy_data_sync,triggerIntervalTrigger(seconds300),idheavy_sync,max_instances1,coalesceTrue,misfire_grace_time60)这里misfire_grace_time设成60秒意思是如果任务因为服务器忙碌错过了执行时间只要在60秒内补跑就算正常。超过这个时间窗口的直接丢弃不再堆积。coalesceTrue则保证如果期间有多个触发改发生只执行一次最新的任务避免重复劳动。有意思的是APScheduler 3.10.x在持久化这块也有讲究。起初我想用内存JobStore觉得轻量。但有一次服务器重启后所有正在跑的定时任务全部丢了业务方直接炸毛。后来我换成了RedisJobStore记得要从新版的独立模块里引pythonfrom apscheduler.jobstores.redis import RedisJobStorefrom apscheduler.jobstores.memory import MemoryJobStorejobstores {redis: RedisJobStore(hostlocalhost, port6379, db0),default: MemoryJobStore()}虽然引入了Redis但相比Celery那种完整的消息队列架构APScheduler的Redis只是用来存Job状态连接池开销几乎可以忽略不计。折腾完这一轮300多个任务终于稳稳当当地跑起来了。没有复杂的Worker节点没有Broker消息积压服务器资源占用也控制在了一个很舒适的范围内。这次选型经历让我明白技术方案没有绝对的好坏只有适不适合。Celery很强但对于这种单机、轻量级、任务执行时间短的场景它就是杀鸡用牛刀。APScheduler胜在简单直接配合现代异步框架用起来非常丝滑。本文基于实际项目经验整理欢迎在评论区交流技术问题。