Files
ProductionDataBaseSync_Data…/docs/plan-health-check-fastapi.md
Misaka_Company 03d7bcea39 feat(health): 被动式健康检查 API(FastAPI,覆盖服务健康+数据健康)
在同步进程内内置 FastAPI/uvicorn HTTP endpoint (GET /health),返回结构化
健康快照,便于运维/监控被动探活:

- 服务健康:cycle_lag(核心指标,超过 3×poll_interval 判 unhealthy)、
  队列状态(pending/error/dead/cleaned 实时查)、capture 累计(enqueued/
  deferred/aged_out)、最近 cycle 时间/耗时/错误。
- 数据健康:解析最近一次定时 compare_ids 报告(不跑全量 compare 太重)、
  实时查 SyncLogArchive 异常痕迹(AgedOut/降级)、SyncQueue dead 行样本。

status 三态:healthy(200) / degraded(503, 有dead/error/aged_out/compare
不一致) / unhealthy(503, 主循环停滞)。状态码映射便于按码告警。

线程模型:uvicorn 占主线程,capture/apply/cleanup 循环跑后台 daemon 线程,
NSSM 停服务时主线程退出、daemon 自动终止。health.enabled=false 时退化为
旧行为(同步循环占主线程)。Web 层(routes/)与业务逻辑(HealthChecker)
解耦,后期加运维接口(compare触发/死信管理/metrics)零结构改动。

零 SQL/零 schema 改动。新增依赖 fastapi/uvicorn[standard]。
2026-08-05 12:13:14 +08:00

8.3 KiB
Raw Blame History

方案 v2FastAPI 健康检查(面向多接口扩展)

基于"后期会引入更多运维接口"的前提,采用 FastAPI。 与 v1标准库 http.server的核心差异在部署模型——FastAPI/uvicorn 阻塞主线程,必须重新设计进程结构。本方案先解决这个架构问题,再展开实现。


一、核心架构决策:同步循环放哪个线程?

FastAPI 的标准运行方式 uvicorn.run(app)阻塞主线程。而现有项目里 service.run()(同步循环)是主线程。两者都要"常驻",必须有一个让出主线程。这是用 FastAPI 唯一的硬约束,两条路径:

方案 AFastAPI 主线程 + 同步循环后台线程( 推荐)

NSSM 启动 → main.py incremental --loop
   ├─ 主线程: uvicorn.run(app)         ← FastAPI 常驻
   └─ 后台线程(daemon): service cycle  ← 同步循环搬到后台
  • 优点FastAPI 在主线程signal handling、uvicorn 内部机制都按官方推荐姿势跑,最稳;未来加接口、加中间件、接 Prometheus 都顺畅。
  • 代价:同步循环从主线程移到后台线程。但 cycle() 本身是纯函数式的(每轮独立、用完即关 writer搬到后台线程风险可控——它本来就是为"被反复调用"设计的。

方案 B同步循环主线程 + FastAPI 后台线程( 不推荐)

  • 缺点uvicorn 官方明确不推荐嵌入非主线程signal handler、事件循环绑定有边角问题且 NSSM 的进程身份含糊(管的是"同步服务"还是"Web服务"?)。

采用方案 A。 下面所有实现都基于 A。


二、进程结构与生命周期

flowchart TD
    NSSM[NSSM 启动 main.py incremental --loop] --> MAIN[主线程]
    MAIN -->|"启动顺序"| S1[1. setup_logging]
    S1 --> S2[2. 创建 ServiceState 全局状态]
    S2 --> S3[3. 启动同步后台线程 daemon]
    S3 --> S4[4. uvicorn.run 主线程阻塞]
    S3 -.->|daemon 线程| CYC[cycle 循环<br/>每轮更新 ServiceState]
    S4 -.->|HTTP 请求到来| APP[FastAPI app]
    APP -->|读| S2
    APP -->|实时查 SQL| DB[(SQL Server)]
    CYC -->|写| S2
    CYC -->|读写| AC[(Access)] & DB

    style S4 fill:#e3f2fd,stroke:#1976d2
    style CYC fill:#fff3e0,stroke:#f57c00
    style APP fill:#e8f5e9,stroke:#388e3c

关键点:

  • ServiceState 是两线程间的唯一桥梁同步线程写、HTTP 线程读。用 threading.Lock 保护(或用简单的不可变快照替换,避免锁)。
  • daemon 线程:同步循环线程设 daemon=True,主进程退出时自动终止,不留孤儿。
  • uvicorn 退出即进程退出NSSM stop → uvicorn 收到信号 → 主线程结束 → daemon 同步线程随之终止。

三、健康检查覆盖的两层(同 v1

服务健康Service Health

指标 来源
status 综合判定 healthy/degraded/unhealthy
pid, started_at, uptime_seconds 进程
last_cycle_at, last_cycle_duration_s 内存状态(同步线程每轮更新)
cycle_lag_seconds now - last_cycle_at核心指标
queue 实时查 SyncQueuepending/error/dead/cleaned
capture_since_start 内存累计enqueued/deferred/aged_out

数据健康Data Health

指标 来源
last_compare 解析最近 compare_ids_*.log 报告头
drift_tables 实时查 SyncLogArchiveAgedOut、Original≠Processed
dead_rows_sample 实时查 SyncQueue dead 行样本

数据健康不跑全量 compare(太重),靠"最近定时 compare 结果 + 异常痕迹实时查"。全量漂移仍由每日 05:08 compare 兜底。


四、status 综合判定(同 v1

unhealthy : cycle_lag_seconds > 3 × poll_interval   (主循环停滞)
degraded  : queue.dead > 0 或 queue.error > 0
           或 capture.aged_out > 0
           或 last_compare.mismatch
healthy   : 其余

unhealthy/degraded 时 HTTP 返回 503healthy 返回 200便于按状态码告警。


五、FastAPI 应用结构(为后期扩展铺路)

src/sync/
  web/
    __init__.py
    app.py            # FastAPI 实例 + 全局依赖state/cfg 注入)
    deps.py           # Depends(): 取 ServiceState / SyncConfig
    schemas.py        # Pydantic 响应模型HealthResponse 等)
    routes/
      __init__.py
      health.py       # GET /health本次实现
      # 后期: compare.py (触发/查询核对)、queue.py (死信管理)、metrics.py...
  • 后期加接口只需在 routes/ 加文件 + 在 app.py include_router,结构清晰。
  • 用 FastAPI 的 Depends 注入共享的 ServiceState,避免全局变量。
  • 响应用 Pydantic 模型(schemas.py),自动生成 /docs 给运维查阅。

本次只实现 routes/health.py,但目录结构一步到位,后期加接口零结构改动。


六、配置config.py 新增)

class HealthConfig(BaseModel):
    enabled: bool = True
    host: str = "0.0.0.0"     # 监听地址
    port: int = 8421           # 健康检查端口
    # uvicorn 运行参数
    log_level: str = "warning" # uvicorn 自身日志级别(避免刷屏)

class SyncConfig(BaseModel):
    # ... 既有字段 ...
    health: HealthConfig = HealthConfig()  # 默认开启,老 config.yaml 无需改

七、service.py 改造(线程模型变更)

run() 从"主线程跑循环"变为"启动后台同步线程 + 主线程跑 uvicorn"

def run(cfg):
    setup_logging(cfg.logging)
    state = ServiceState(started_at=datetime.now(), pid=os.getpid())
    # 后台同步线程
    sync_thread = threading.Thread(
        target=_sync_loop, args=(cfg, state), daemon=True, name="sync-cycle"
    )
    sync_thread.start()
    # 主线程跑 FastAPI阻塞
    if cfg.health.enabled:
        from sync.web.app import create_app
        import uvicorn
        app = create_app(state, cfg)
        uvicorn.run(app, host=cfg.health.host, port=cfg.health.port,
                    log_level=cfg.health.log_level)
    else:
        # 健康检查关闭:主线程直接跑同步循环(兼容旧行为)
        _sync_loop(cfg, state)

def _sync_loop(cfg, state):
    """同步循环(原 run() 的 while True 主体,抽出供后台线程调用)。"""
    idle_since = None
    while True:
        t0 = time.monotonic()
        active = cycle(cfg, state)   # cycle 增加 state 参数用于更新状态
        state.update_cycle(active, duration=time.monotonic()-t0)  # 新增
        # idle heartbeat 逻辑保留...
        time.sleep(cfg.runtime.poll_interval_seconds)

cycle(cfg, state) 末尾新增 state.update_cycle(...),并把每轮 CaptureStats 累计进 state。


八、依赖更新

requirements.txt 增加:

fastapi>=0.110.0
uvicorn[standard]>=0.27.0

uvicorn[standard] 含 uvloop/httptools性能更好纯 Windows 下 uvloop 不可用会自动降级,无影响。)

测试依赖dev不入 requirementshttpxFastAPI 测试用 TestClient 依赖它)。


九、改动文件清单

文件 改动
src/sync/web/app.py 新增 create_app + FastAPI 实例
src/sync/web/deps.py 新增 Depends 注入
src/sync/web/schemas.py 新增 Pydantic 响应模型
src/sync/web/routes/health.py 新增 GET /health
src/sync/health.py 新增 ServiceState + HealthChecker业务逻辑与 Web 解耦)
src/sync/service.py 改 run() 线程模型 + cycle() 更新 state
src/sync/config.py 新增 HealthConfig
requirements.txt 加 fastapi / uvicorn
config.example.yaml 补 health 段示例
tests/test_health.py 新增 覆盖 status 判定/snapshot/TestClient

零 SQL 改动、零 schema 改动。Web 层与业务逻辑HealthChecker解耦后期加接口只动 web/routes/。


十、待确认决策点

  1. 端口 8421
  2. unhealthy/degraded 返回 503(按码告警友好)确认?
  3. host 0.0.0.0(内网+FRP 可访问)确认?
  4. 同步循环放后台 daemon 线程方案A确认这是与 v1 最大的结构差异。
  5. drift_tables 查询范围:启动至今 vs 最近 N 小时?倾向启动至今。

确认后实施。