我的踩坑实录: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机制,对于这种单机多任务的场景,它能以更低的资源占用完成调度。不需要消息队列的中间环节,任务直接从内存触发到执行器,延迟更低,排查问题也更方便。

代码写起来也很顺手,大概长这样:

```python
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.jobstores.memory import MemoryJobStore

scheduler = AsyncIOScheduler(jobstores={
'default': MemoryJobStore()
})

模拟一个每小时执行的数据同步任务

scheduler.add_job(
func=sync_inventory_data,
trigger=CronTrigger(hour='*', minute=0),
id='inventory_sync',
max_instances=1,
coalesce=True
)
scheduler.start()
```

这段代码看着挺干净,但我当时觉得这样就行,结果后面还是栽了跟头。

跑起来才发现的坑

项目上线第一周,我就收到了运维的报警。说是同步服务的CPU占用率偶尔会飙到90%以上,而且日志里出现了大量重复执行的记录。

我一开始以为是网络波动导致任务超时,于是手动去服务器上查进程,结果发现同一个任务在同一时刻竟然有两个实例在跑。这就很离谱了,明明我在add_job的时候设置了max_instances=1,怎么还会重叠?

后来翻遍了APScheduler的文档,结合源码看了一遍,才发现问题出在coalesce和触发器精度上。我们的某些任务用的是IntervalTrigger,间隔设的300秒,但由于服务器负载高,任务执行时间超过了间隔时间。这时候如果没配好next_run_time的计算逻辑,调度器可能会认为之前的任务已经“错过”了,于是直接再塞一个进去。

坑死了,这玩意儿在官方文档的FAQ里藏得可太深了。

排查过程其实挺折磨人的。我先是在任务函数里加了分布式锁,试图用Redis锁来防重。但这属于治标不治本,万一锁过期了或者网络抖动,还是会有并发。后来我把方案改成了纯靠APScheduler自身的配置来规避。

关键配置调整如下:

```python
scheduler.add_job(
func=heavy_data_sync,
trigger=IntervalTrigger(seconds=300),
id='heavy_sync',
max_instances=1,
coalesce=True,
misfire_grace_time=60
)
```

这里misfire_grace_time设成60秒,意思是如果任务因为服务器忙碌错过了执行时间,只要在60秒内补跑就算正常。超过这个时间窗口的,直接丢弃,不再堆积。coalesce=True则保证如果期间有多个触发改发生,只执行一次最新的任务,避免重复劳动。

有意思的是,APScheduler 3.10.x在持久化这块也有讲究。起初我想用内存JobStore,觉得轻量。但有一次服务器重启后,所有正在跑的定时任务全部丢了,业务方直接炸毛。后来我换成了RedisJobStore,记得要从新版的独立模块里引:

```python
from apscheduler.jobstores.redis import RedisJobStore
from apscheduler.jobstores.memory import MemoryJobStore

jobstores = {
'redis': RedisJobStore(host='localhost', port=6379, db=0),
'default': MemoryJobStore()
}
```

虽然引入了Redis,但相比Celery那种完整的消息队列架构,APScheduler的Redis只是用来存Job状态,连接池开销几乎可以忽略不计。

折腾完这一轮,300多个任务终于稳稳当当地跑起来了。没有复杂的Worker节点,没有Broker消息积压,服务器资源占用也控制在了一个很舒适的范围内。

这次选型经历让我明白,技术方案没有绝对的好坏,只有适不适合。Celery很强,但对于这种单机、轻量级、任务执行时间短的场景,它就是杀鸡用牛刀。APScheduler胜在简单直接,配合现代异步框架用起来非常丝滑。

本文基于实际项目经验整理,欢迎在评论区交流技术问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红信鸽科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值