踩坑实录:解决 APScheduler 定时任务越跑越卡、内存堆积、卡死问题

0 阅读4分钟

一、问题现象

线上 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

五、修复前后对比

指标修复前修复后
任务并行数持续叠加、无限上涨始终保持单任务执行
内存状态单向上涨、不回落稳定波动、正常回收
线程数量持续膨胀固定可控

六、总结:定时任务四大硬性规范

本次线上故障完全由不规范使用定时调度器导致,总结四条生产铁律:

  1. 周期性任务必须加任务锁,禁止任务叠加;
  2. 手动限制线程池大小,不要使用默认无限线程池;
  3. 任何连接必须finally关闭,禁止依赖自动回收;
  4. IO任务优先异步调度器,减少线程切换开销。

七、结语

很多人误以为 Python 定时任务简单,随手复制网上代码上线。APScheduler 本身没有Bug,错误的使用方式才是线上隐患的根源。 大家可以评论区一起交流讨论讨论!