如何在 Uvicorn 多进程部署中确保 FastAPI 的定时任务仅执行一次

1次阅读

如何在 Uvicorn 多进程部署中确保 FastAPI 的定时任务仅执行一次

本文介绍在 fastapi + uvicorn 多工作进程(workers > 1)部署场景下,避免生命周期(lifespan)中启动的定时任务被重复执行的核心方案:将调度器移至主进程初始化,并选用 backgroundscheduler 替代 asyncioscheduler。

本文介绍在 fastapi + uvicorn 多工作进程(workers > 1)部署场景下,避免生命周期(lifespan)中启动的定时任务被重复执行的核心方案:将调度器移至主进程初始化,并选用 backgroundscheduler 替代 asyncioscheduler。

在使用 Uvicorn 以多进程模式(如 workers=2)运行 fastapi 应用时,每个工作进程都会独立执行 lifespan 事件——这意味着若在 lifespan 中初始化并启动 AsyncIOScheduler,所有工作进程都会各自运行一套调度器,导致定时任务(如每秒执行一次的 test())被多次并发触发,违背单例预期。

根本原因在于:Uvicorn 的多进程模型采用 fork 派生unix)或 spawn 启动windows),每个 worker 是完全独立的 Python 进程,拥有各自的事件循环、内存空间和调度器实例。lifespan 在每个 worker 内部被单独调用,因此无法天然实现“仅一个进程执行”的语义。

✅ 正确解法是:将调度器脱离 FastAPI 生命周期管理,提升至主进程(parent process)中初始化和启停。这确保全局仅存在一个调度器实例,且其生命周期与 Uvicorn 主进程对齐。

推荐使用 APScheduler 的 BackgroundScheduler(非异步版),因其基于线程而非事件循环,更适合作为主进程中的后台守护服务:

from fastapi import FastAPI from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler import uvicorn  def test():     print(f"Test scheduler {datetime.now()}")  app = FastAPI()  if __name__ == "__main__":     # ✅ 在主进程(parent)中创建并启动调度器     scheduler = BackgroundScheduler()     scheduler.add_job(         test,         trigger="cron",         second="0-30",  # 每分钟第 0–30 秒各触发一次(即每秒一次)         coalesce=True,   # 防止因延迟导致的任务积         max_instances=1  # 确保同一任务不会并发执行     )     scheduler.start()      # ⚠️ 注意:必须在 uvicorn.run() 之后、程序退出前显式 shutdown     try:         uvicorn.run("main:app", workers=2, host="127.0.0.1", port=5000)     finally:         scheduler.shutdown(wait=True)  # 安全等待正在运行的任务完成

? 关键要点说明:

  • 不使用 lifespan:避免在每个 worker 中重复初始化调度器;
  • BackgroundScheduler vs AsyncIOScheduler:前者基于 threading.Timer,可安全运行于主线程;后者依赖 asyncio 事件循环,而主进程无活跃 Event loop,且多 worker 下仍会重复;
  • coalesce=True 和 max_instances=1:防止因系统负载导致任务堆积或并发执行,强化单次语义;
  • scheduler.shutdown(wait=True):确保应用退出前完成清理,避免僵尸线程或资源泄漏;
  • 信号处理补充(进阶):如需优雅响应 SIGTERM(例如 docker 停止),可结合 signal 模块注册钩子,但上述 try/finally 已覆盖绝大多数场景。

? 补充建议:若业务逻辑涉及数据库写入、文件操作或外部 API 调用等有状态行为,还需考虑分布式锁(如 redis Lock)作为兜底方案,但对纯定时触发、无共享状态的轻量任务,主进程单例调度器已足够可靠。

综上,通过将调度器上提至主进程并选用线程安全的 BackgroundScheduler,即可在保持 Uvicorn 多进程高并发能力的同时,严格保障定时任务的全局唯一性与执行确定性。

text=ZqhQzanResources