一、问题现象
线上 Python 定时服务采用 APScheduler 做周期任务调度,业务逻辑简单:数据清洗、数据库同步、日志上报。
但服务长期运行出现明确异常:
- 任务执行耗时越来越长;
- 后台任务不断堆积,出现大量并行任务;
- 内存单向上涨,无回落;
- 无报错、无崩溃,属于典型隐性故障。
重启服务后立刻恢复,很多开发者会简单归因为“Python内存通病”,实则是APScheduler使用方式错误。
阅读定位:只解决一个问题——定时任务越跑越卡、堆积、内存泄漏。 适用人群:使用APScheduler、Celery定时、常驻脚本定时任务开发。
二、错误代码(90%开发者的写法)
先贴出线上事故原始代码,也是网上最普遍的教程写法:
from apscheduler.schedulers.blocking import BlockingScheduler
import time
scheduler = BlockingScheduler()
# 每分钟执行一次
@scheduler.scheduled_job("cron", minute="*")
def task():
# 模拟IO耗时任务
time.sleep(3)
print("执行同步任务")
if __name__ == "__main__":
scheduler.start()
三、问题根源深度剖析
3.1 核心原因:任务未执行完成,下一轮任务直接触发
上述代码存在致命问题:APScheduler 默认不做任务互斥。
当前任务还未结束,下一分钟定时时间到达,新任务直接并行启动。任务不断叠加、线程越开越多,造成:
- 线程池无限膨胀;
- 数据库连接不释放;
- 上下文对象常驻内存;
- 内存只升不降、任务堆积拥堵。
3.2 第二大坑:默认线程池无上限 + 资源不回收
BlockingScheduler 默认使用ThreadPoolExecutor,不限制最大线程数。
每一次叠加任务都会创建新线程,线程不销毁、连接不释放,长期运行直接拖垮服务。
3.3 隐形坑:异常中断导致任务残留
任务内部抛出异常时,APScheduler 不会主动关闭本次任务上下文,部分连接、句柄、引用无法被GC回收,日积月累形成内存泄漏。
四、生产级正确修复方案(可直接上线)
4.1 方案一:任务互斥(单任务串行执行)
同一任务不允许并行,上一次未结束直接跳过本次执行,这是定时任务最通用、最安全的逻辑。
from apscheduler.schedulers.blocking import BlockingScheduler
import time
import threading
scheduler = BlockingScheduler()
# 任务锁
lock = threading.Lock()
@scheduler.scheduled_job("cron", minute="*")
def task():
# 加锁:保证同一时刻只有一个任务执行
if not lock.acquire(blocking=False):
print("任务正在执行,跳过本次")
return
try:
time.sleep(3)
print("执行同步任务")
finally:
lock.release()
if __name__ == "__main__":
scheduler.start()
4.2 方案二:手动限制线程池,防止无限膨胀
手动指定最大线程数,杜绝线程无限创建,避免系统资源耗尽。
from apscheduler.schedulers.blocking import BlockingScheduler
from concurrent.futures import ThreadPoolExecutor
# 限制最大线程为5
executor = ThreadPoolExecutor(max_workers=5)
scheduler = BlockingScheduler(executor=executor)
4.3 方案三:任务异常兜底 + 资源强制回收
所有定时任务必须写 try-finally,强制关闭连接、释放资源,杜绝隐性内存泄漏。
def task():
conn = None
try:
# 获取数据库连接
# ...业务逻辑
pass
except Exception as e:
print("任务异常", e)
finally:
# 强制回收资源
if conn:
conn.close()
4.4 终极生产方案:推荐使用 AsyncScheduler
IO密集型定时任务,不要用阻塞调度器,使用异步调度器减少线程开销,内存更稳定。
from apscheduler.schedulers.asyncio import AsyncScheduler
五、修复前后对比
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 任务并行数 | 持续叠加、无限上涨 | 始终保持单任务执行 |
| 内存状态 | 单向上涨、不回落 | 稳定波动、正常回收 |
| 线程数量 | 持续膨胀 | 固定可控 |
六、总结:定时任务四大硬性规范
本次线上故障完全由不规范使用定时调度器导致,总结四条生产铁律:
- 周期性任务必须加任务锁,禁止任务叠加;
- 手动限制线程池大小,不要使用默认无限线程池;
- 任何连接必须finally关闭,禁止依赖自动回收;
- IO任务优先异步调度器,减少线程切换开销。
七、结语
很多人误以为 Python 定时任务简单,随手复制网上代码上线。APScheduler 本身没有Bug,错误的使用方式才是线上隐患的根源。 大家可以评论区一起交流讨论讨论!