目录
[基于 APScheduler 的可恢复调度系统:子进程隔离、心跳监控与任务热更新](#基于 APScheduler 的可恢复调度系统:子进程隔离、心跳监控与任务热更新)
[二、数据结构:Scheduler 心跳表](#二、数据结构:Scheduler 心跳表)
[1. 启动调度器](#1. 启动调度器)
[2. 心跳写入(每 x 秒)](#2. 心跳写入(每 x 秒))
[3. 自动刷新调度任务(每 x 秒)](#3. 自动刷新调度任务(每 x 秒))
[1. 启动调度子进程](#1. 启动调度子进程)
[2. 每 x 秒检查子进程状态](#2. 每 x 秒检查子进程状态)
[3. 通过心跳判断是否卡死](#3. 通过心跳判断是否卡死)
[4. 自动重启调度器](#4. 自动重启调度器)
[五、无需数据库的简化版 sync_jobs_with_db 示例](#五、无需数据库的简化版 sync_jobs_with_db 示例)
[1. 子进程隔离](#1. 子进程隔离)
[2. 自动重启](#2. 自动重启)
[3. 任务热更新](#3. 任务热更新)
[4. 可观测性强](#4. 可观测性强)
[5. 易扩展](#5. 易扩展)
基于 APScheduler 的可恢复调度系统:子进程隔离、心跳监控与任务热更新
在许多后端系统中,任务调度器(Scheduler)扮演着重要角色:定时清理数据、同步外部系统、执行周期任务等。然而,一旦调度器发生进程异常、任务丢失或无法自恢复,就可能导致严重隐患。
本文介绍一种可恢复的调度器架构:APScheduler + 子进程隔离 + 心跳监控 + 任务热更新。
将基于三个核心文件深入讲解实现方式,并提供一个简化版 sync_jobs_with_db 示例,帮助快速理解系统运行机制。
一、系统设计目标
这个调度系统的设计目标是:
- 调度器与业务主进程隔离(通过独立子进程)
- 主进程对调度子进程进行健康监控
- 调度器自动刷新任务列表,实现任务热更新
- 通过心跳机制检测子进程存活状态
- 出现异常可自动重启调度子进程
系统核心结构如下:
主进程 子进程(APScheduler)
--------------------------------------------------------
| 启动 Scheduler 子进程 | scheduler.start() |
| 每 x 秒检查子进程是否存活 | 每 x 秒写一次心跳 |
| 检查 DB 中的心跳记录 | 每 x 秒刷新调度任务(热更新) |
| 发现异常 → 自动重启 | 异常自动退出主进程捕获 |
--------------------------------------------------------
二、数据结构:Scheduler 心跳表
文件:models.py
调度器使用一个简单的数据表来记录心跳:
from typing import Optional
from datetime import datetime
from sqlmodel import SQLModel, Field
from app.core.base_model import CommonModel
class SchedulerHeartBeat(CommonModel, table=True):
__tablename__ = "scheduler_heartbeat"
id: Optional[int] = Field(default=None, primary_key=True)
last_beat: datetime = Field(default=datetime.utcnow)
scheduler_pid: Optional[int] = Field(nullable=True)
意义:
- scheduler_pid 用于保存子进程 PID,只在启动时写入一次。
- last_beat 每次心跳更新,用于主进程判断子进程是否存活。
三、子进程逻辑:调度器执行体
文件:crud.py
子进程需要完成 3 件事:
- 启动 APScheduler
- 定期写入心跳
- 定期刷新调度任务
1. 启动调度器
import asyncio
from datetime import datetime
from sqlalchemy import update
from sqlalchemy.future import select
from app.core.database import get_db_session
from app.db_manage.scheduler import sync_jobs_with_db, scheduler
from app.scheduler.models import SchedulerHeartBeat
from xu_box.logger_utils import xu_logger
REFRESH_INTERVAL = 15 # 任务刷新周期(降低数据库压力)
HEARTBEAT_INTERVAL = 60 # 心跳周期(无须太频繁)
async def write_scheduler_pid(pid: int):
"""
主进程启动时写入 PID(只执行一次)
采用 merge() 避免额外 select,提高性能
"""
async with get_db_session() as db:
row = SchedulerHeartBeat(id=1, scheduler_pid=pid, last_beat=datetime.utcnow())
await db.merge(row)
await db.commit()
async def update_heartbeat():
"""
子进程心跳(仅更新 last_beat)
采用直接 UPDATE,提高性能,无需 SELECT
"""
try:
async with get_db_session() as db:
stmt = (
update(SchedulerHeartBeat)
.where(SchedulerHeartBeat.id == 1)
.values(last_beat=datetime.utcnow())
)
await db.execute(stmt)
await db.commit()
xu_logger.debug("[Heartbeat] 心跳更新成功")
except Exception as e:
xu_logger.error(f"[Heartbeat] 心跳更新失败: {e}")
async def heartbeat_worker():
"""每隔 HEARTBEAT_INTERVAL 执行一次心跳"""
while True:
await update_heartbeat()
await asyncio.sleep(HEARTBEAT_INTERVAL)
async def auto_refresh_jobs():
"""
自动刷新 APScheduler 的任务列表
周期可以不必太短,避免 DB 查询压力
"""
while True:
try:
async with get_db_session() as db:
await sync_jobs_with_db(db, reload=True, log=False)
except Exception as e:
xu_logger.error(f"[AutoRefresh] 刷新任务失败: {e}")
await asyncio.sleep(REFRESH_INTERVAL)
async def scheduler_worker():
"""APScheduler 子进程主逻辑"""
# 1. 初次同步任务
try:
async with get_db_session() as db:
await sync_jobs_with_db(db, reload=True, log=False)
except Exception as e:
xu_logger.error(f"[Boot] 初次加载任务失败: {e}")
# 2. 启动调度器
if not scheduler.running:
scheduler.start()
xu_logger.info("APScheduler 子进程调度器已启动")
# 3. 启动后台 worker
tasks = [
asyncio.create_task(auto_refresh_jobs()),
asyncio.create_task(heartbeat_worker()),
]
# 4. 永久运行
await asyncio.gather(*tasks)
def run_scheduler_process():
"""APScheduler 子进程入口"""
try:
asyncio.run(scheduler_worker())
except Exception as e:
xu_logger.error(f"APScheduler 子进程异常退出: {e}")
这是 APScheduler 的基本启动过程。
2. 心跳写入(每 x 秒)
async def heartbeat_worker():
while True:
await update_heartbeat()
await asyncio.sleep(HEARTBEAT_INTERVAL)
通过更新 last_beat,主进程便可以实时判断子进程是否正常运行。
3. 自动刷新调度任务(每 x 秒)
async def auto_refresh_jobs():
while True:
async with get_db_session() as db:
await sync_jobs_with_db(db, reload=True, log=False)
await asyncio.sleep(REFRESH_INTERVAL)
当数据库更新任务表时,调度器能自动同步最新任务,做到热更新。
效果示例:

四、主进程:子进程健康监控系统
文件:control.py
主进程负责启动调度子进程并持续监控。
1. 启动调度子进程
import asyncio
from multiprocessing import Process
import time
from datetime import datetime, timedelta
from sqlalchemy.future import select
from app.core.database import get_db_session
from app.scheduler.models import SchedulerHeartBeat
from app.scheduler.crud import run_scheduler_process, write_scheduler_pid, HEARTBEAT_INTERVAL
from xu_box.logger_utils import xu_logger
def scheduler_health_monitor(scheduler_proc: Process):
"""主进程监控 scheduler 子进程健康状态"""
async def check():
async with get_db_session() as db:
stmt = select(SchedulerHeartBeat).where(SchedulerHeartBeat.id == 1)
result = await db.execute(stmt)
beat = result.scalar_one_or_none()
if not beat:
return False
# 10s 内无心跳 → 判定死亡
if datetime.utcnow() - beat.last_beat > timedelta(10):
return False
return True
# 每 x 秒检查子进程是否健康
while True:
time.sleep(HEARTBEAT_INTERVAL)
if not scheduler_proc.is_alive():
xu_logger.error("APScheduler 子进程意外退出,即将重启...")
return False # 主进程重新启动
# 心跳异常
import asyncio
alive = asyncio.run(check())
if not alive:
xu_logger.error("APScheduler 心跳丢失,将重启子进程...")
scheduler_proc.terminate()
return False
def start_scheduler_supervised():
"""启动 scheduler 并进行自动重启"""
while True:
proc = Process(target=run_scheduler_process, name="APSchedulerProcess", daemon=True)
proc.start()
xu_logger.info(f"APScheduler 子进程已启动,PID={proc.pid}")
asyncio.run(write_scheduler_pid(proc.pid))
alive = scheduler_health_monitor(proc)
if not alive:
xu_logger.warning("正在重启 APScheduler 子进程")
continue
完全隔离调度器,使其错误不会影响业务 API。
2. 每 x 秒检查子进程状态
if not scheduler_proc.is_alive():
return False
但仅检查进程存活是不够的,因为进程可能挂死却仍在运行。
3. 通过心跳判断是否卡死
if datetime.utcnow() - beat.last_beat > timedelta(10):
return False
如果 10 秒没有心跳,则判断子进程失效。
4. 自动重启调度器
当 scheduler_health_monitor() 返回 False 时,主进程会:
xu_logger.warning("正在重启 APScheduler 子进程")
continue
不断循环启动新的子进程,当旧子进程卡死或退出时能自动恢复。
五、无需数据库的简化版 sync_jobs_with_db 示例
实际项目中,sync_jobs_with_db 通常从数据库读取任务表,并与 APScheduler 调度器同步。本示例提供一个无需数据库的极简版本,便于理解:
# 简化版 sync_jobs_with_db,不访问数据库
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
# 假设我们要同步固定任务列表
def example_job():
print("执行示例任务")
async def sync_jobs_with_db(db=None, reload=True, log=False):
"""
极简版任务同步:每次只确保 scheduler 内存在一个固定任务。
"""
job_id = "example_job"
# 如果任务不存在,则添加
if not scheduler.get_job(job_id):
scheduler.add_job(example_job, "interval", seconds=10, id=job_id)
if log:
print("已添加 example_job")
# 如果 reload=True,可实现修改任务逻辑,这里仅作示例
if reload:
# 假设我们想每次刷新时"重置任务"
scheduler.remove_job(job_id)
scheduler.add_job(example_job, "interval", seconds=10, id=job_id)
if log:
print("任务已刷新")
运行效果:
- 每次调度器刷新(auto_refresh_jobs)时,上述任务会被重置。
- 真实环境中,你可以把任务列表从数据库读出来并同步。
六、系统优势总结
这种调度架构具备以下优势:
1. 子进程隔离
调度器崩溃不会拖垮主服务。
2. 自动重启
主进程通过心跳监控调度器运行状态,异常则重启。
3. 任务热更新
调度器能自动同步数据库中的任务列表。
4. 可观测性强
通过心跳表,可以扩展监控面板或报警系统。
5. 易扩展
调度逻辑统一收敛于 sync_jobs_with_db,统一管理所有任务。
七、适用场景
- 大型服务端项目的周期任务管理
- 需要强健调度能力(自动恢复、热更新)
- 调度任务较多且随时变化
- 多进程环境下的任务调度
结语
通过主进程监控 + 子进程隔离 + APScheduler 的组合,构建一个可自恢复、可热更新、可扩展的可靠调度系统。