ApScheduler
APScheduler(Advanced Python Scheduler)是一个用于在 Python 应用程序中执行定时任务的库。它提供了一种简单而强大的方式,允许你在指定的时间间隔、日期或特定事件触发时执行任务。
问题背景
最近公司项目中遇到了个 ApScheduler 不执行的奇葩问题,这个项目有两个环境,一个是外网的开发环境,一个是部署到客户现场的内网环境,最近现场反馈好几个定时任务最近到时间都不会执行,而且很随机,有时候就会执行,有时候又不会执行,和抽风了一样。
解决方案
最后定位到是由于现场创建的定时任务太多了(八十多个),APScheduler 默认的调度配置忙不过来了。
最开始的代码写的比较简单,都是基于默认的配置:
... jobstores = { 'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite') } back_scheduler = BackgroundScheduler(timezone="Asia/Shanghai", jobstores=jobstores) ...
翻阅官方文档可以看到,BackgroundScheduler 默认的执行器是 10 个容量的线程执行器。注意这个 10!从下面的文档可以看到。
APScheduler Documentation - Configuring the scheduler
定时任务没有执行可能有两个原因:
- 定时任务要执行的时间正好发生了停机/重启。这种情况可能性比较小,除非你真的运气不好,恰好赶在了那个时间。
- **Executor 如果是默认的 10 个容量的线程池,恰好 10 个线程都在忙,恰好又有一个任务该执行了,由于没有空闲线程来处理,这个任务将被抛弃。**这个问题就比较常见了,也是我遇到问题的罪魁祸首。如果是因为没有线程处理导致的定时任务不执行,那么会输出日志:
Run time of job "xxx (trigger: cron[year='*', month='*', day='*', day_of_week='*', hour='*', minute='*', second='*'], next run at: 2024-01-05 11:17:37 CST)" was missed by 0:00:05.255671
,抓住关键词:was missed by
,那么基本上就是这个问题了。
这两个原因都可以从官网中找到:
APScheduler Documentation - Missed job executions and coalescing
最后修改代码,适当加大线程池大小,并在创建任务的时候加上 misfire_grace_time 参数。
... jobstores = { 'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite') } job_defaults = { 'coalesce': True, 'misfire_grace_time': None } # 加大线程池大小 executors = { 'default': ThreadPoolExecutor(30) } back_scheduler = BackgroundScheduler(timezone="Asia/Shanghai", jobstores=jobstores, job_defaults=job_defaults, executors=executors) ...
也可以在创建任务的时候为每个任务单独指定:
... # 当任务被唤起时,如果在 misfire_grace_time 时间差内,依然运行。 back_scheduler.add_job(... , misfire_grace_time=30) ...
每个任务都有一个 misfire_grace_time,单位:秒,默认是 0 秒。意思是那些错过的任务在有条件执行时(有线程空闲出来/服务已恢复),如果还没有超过 misfire_grace_time,就会被再次执行。如果 misfire_grace_time=None,就是不论任务错过了多长时间,都会再次执行。