fastapi+backgroundscheduler处理skipped: maximum number of running instances reached (1)


问题原因

  1. 并发实例限制触发‌
    APScheduler默认max_instances=1,当任务执行时间超过间隔时间(如30秒)时,新触发的实例会被拒绝。‌

  2. 任务执行时间过长‌

    当前任务可能包含阻塞操作(如网络请求、复杂计算等),导致无法在30秒内完成

通俗的讲:
就是当同一个job(同一job_id)上一次执行的程序还没有执行完,下一次的trigger就来了,导致不能并行的执行多次,从而阻塞


解决方案分析:

这里和两个参数有关

参数1:max_instances

max_instances 这个参数可以允许同一job在同时并行,就算上次的没结束,也可以新开一个线程去处理新来的请求

max_instances控制的是‌同一任务的并发实例数每个实例会占用线程池中的一个线程

#即使线程池有空闲线程,该任务最多同时运行2个实例
scheduler.add_job(func, 'interval', seconds=30, max_instances=2)

#两个任务实例数独立统计,不会互相限制

scheduler.add_job(func, 'interval', id='job1', seconds=10)

scheduler.add_job(func, 'interval', id='job2', seconds=20) 

参数2:max_works   

max_works 这个参数是可以开的最大线程数,控制线程池容量。有任务(无论是否属于同一 job_id)共享该线程池资源。即使某个任务设置 max_instances=3,若线程池仅允许同时运行2个线程,实际最多只能并行执行2个该任务的实例

若未显式指定 max_workers,其默认值遵循以下规则:

default_max_workers = min(32, os.cpu_count() + 4)
其中:
os.cpu_count():获取当前系统的 CPU 核心数
32 是默认上限,即使 CPU 核心数较多(如服务器场景),最大线程数也不会超过此值

设置线程池

executor = ThreadPoolExecutor(max_workers=2)

那么如何设置max_works呢

        方案1  根据总并发需求设置 max_workers,建议公式: max_workers = SUM(各任务 max_instances) + 缓冲线程数

         方案2 I/O 密集型:max_workers = CPU核心数 × 2

import os

cpu_cores = os.cpu_count()
io_max_workers = cpu_cores * 2  # I/O 密集型推荐值

        方案3 CPU 密集型max_workers = CPU核心数

import os

cpu_cores = os.cpu_count()
cpu_max_workers = cpu_cores     # CPU 密集型推荐值  

 参数对比 

参数 作用层级 限制对象 优先级关系
max_workers 线程池级 全局并行线程总数 若线程池空闲线程不足,即使 max_instances 允许,实例也无法执行
max_instances 任务级 单个任务的并发实例数 仅在有空闲线程时生效,不能突破线程池限制
条件组合 结果
max_instances≥可用线程数 实际并行度=可用线程数
max_instances<可用线程数 实际并行度=max_instances

自动释放机制 

那么不人工干预,是否可以主动释放呢,结论如下

主动释放机制‌

  • ‌实例级释放‌:已运行的实例在‌执行完成后自动释放计数‌,调度器会重新允许新实例触发。
  • ‌等待逻辑‌:若因 max_instances 或线程池资源不足导致实例被跳过,后续触发需‌等待正在运行的实例结束释放资源‌,而非直接抛弃或自动重置
  • 限制类型 释放触发条件 恢复时间点
    max_instances 当前运行实例完成 下一周期触发时重新检查
    线程池资源 线程归还至线程池 任务结束后立即释放
场景1:单任务实例堆积

scheduler.add_job( long_task, # 耗时任务(例如持续30秒) 'interval', seconds=10, max_instances=1 # 默认配置 )

  • ‌现象‌:
    第1次触发后任务运行中,第2、3次触发时因 max_instances=1 被跳过(报错)
  • ‌恢复过程‌:
    任务执行完成后(30秒后),第4次触发(40秒时)可正常执行。
场景2:全局线程池耗尽

executor = ThreadPoolExecutor(max_workers=2) # 全局仅2线程 scheduler.add_job(task1, 'interval', seconds=5, max_instances=3) scheduler.add_job(task2, 'interval', seconds=5, max_instances=3)

  • ‌现象‌:
    当两个任务同时占用所有线程时,新实例被阻塞
  • ‌恢复过程‌:
    任一任务完成并释放线程后,新实例可立即获取资源运行38

解决:

1 调整任务参数‌

  • 增大 max_instances 或延长任务间隔(seconds/minutes)以适应任务执行时间
  • 设置 misfire_grace_time 容忍短暂延迟,避免高频触发导致堆

scheduler.add_job(
    task,
    'interval',
    seconds=10,
    max_instances=3,
    misfire_grace_time=60  # 允许延迟60秒内补执行
)
 

  2 扩展执行资源

    提升线程池容量 max_workers 以匹配并发需求

  • 盲目增大 max_workers 可能导致线程争抢资源,反而降低吞吐量。
  • 建议通过压力测试确定最佳线程数

from concurrent.futures import ThreadPoolExecutor

# 手动设置 max_workers=200(需评估系统资源)
executor = ThreadPoolExecutor(max_workers=200)
 

你可能感兴趣的:(fastapi)