Files
ProductionDataBaseSync_Data…/docs/plan-health-check-api.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

7.6 KiB
Raw Blame History

方案:被动式健康检查 API数据健康 + 服务健康)

目标:在增量同步进程内内置一个轻量 HTTP endpoint对外暴露结构化健康快照覆盖「服务健康」与「数据健康」两层。 形态:单进程内置(不另起服务、不新增 NSSM 配置HTTP server 跑在后台线程,主同步循环零侵入。


一、设计原则

  1. 单进程内置HTTP server 用标准库 http.server + 后台线程,跑在同步进程内。不另起进程、不新增 NSSM 服务、不引入 Flask 等第三方依赖。
  2. 主循环零侵入:健康检查线程只读共享状态、只读 SQL不触碰 capture/apply/cleanup 任何路径。HTTP server 崩溃不影响同步。
  3. 数据实时查:服务健康(最后 cycle 时间/队列状态从进程内存的状态对象读数据健康Access↔SQL 漂移、dead 行)实时查 SQL Server 和最近 compare 报告。
  4. 配置可选:通过 config.yamlhealth 段控制开关/端口/路径,默认开启但端口可配,关闭时不起线程。

二、健康检查覆盖的两层

服务健康Service Health

回答:"同步进程在跑吗?最近正常工作吗?队列有没有卡死?"

指标 来源 含义
status 综合判定 healthy / degraded / unhealthy
pid, started_at, uptime_seconds 进程 进程存活与运行时长
last_cycle_at, last_cycle_duration_s 内存状态 最近一次 cycle 时间与耗时
cycle_lag_seconds 实时计算 = now - last_cycle_at 核心指标:距上次 cycle 间隔超过阈值→unhealthy
last_cycle_active 内存状态 上轮是否处理了变更
queue 实时查 SyncQueue pending/error/dead/cleaned 计数dead>0→degraded
capture_since_start 内存状态累计 服务启动至今的 deferred/aged_out/enqueued 累计aged_out>0→degraded

数据健康Data Health

回答:"Access 和 SQL 数据一致吗?最近核对结果如何?"

指标 来源 含义
last_compare 解析最近的 compare_ids_*.log 报告头 最近一次 compare 的时间、mismatch 表数、是否一致
drift_tables 实时查 SyncLogArchive 启动至今出现过的降级/异常记录涉及的表AgedOut、Original≠Processed
dead_rows_sample 实时查 SyncQueue dead 行 卡死行的样本(表名/RecordID/错误),辅助定位

注:数据健康在每次请求时跑全量 compare太重而是读"最近一次定时 compare 的结果" + "归档表/队列里的异常痕迹"。全量漂移检测仍由每日 05:08 的 compare_ids 计划任务兜底。


三、status 综合判定逻辑

unhealthy  : cycle_lag_seconds > 3 × poll_interval  (主循环停滞)
           或 进程内存状态长时间未更新(疑似卡死)

degraded   : queue.dead > 0          (有放弃的变更,数据可能缺)
           或 queue.error > 0        (有失败待重试)
           或 capture.aged_out > 0   (有读不到的 Insert 被判死)
           或 last_compare.mismatch  (最近核对不一致)

healthy    : 其余情况

cycle_lag3 × poll_interval(默认 poll=10s → 30s作阈值正常空闲 cycle 间隔就是 10s 左右,超过 3 倍说明主循环被卡(比如 Access 锁等待)。


四、接口契约

请求GET /health(也可配 GET / 简化)

响应HTTP 200 + JSON无论 healthy/degraded/unhealthy 都返回 200状态在 body 里;这样探活失败和网络故障可区分——网络故障是连不上/超时,服务不健康是 200 但 status 字段非 healthy

可选:对 unhealthy 同时返回 HTTP 503便于直接接入按状态码告警的监控平台。这点待定见决策点

响应示例

{
  "service": "DataMacroSync",
  "status": "healthy",
  "checked_at": "2026-08-05T11:30:00",
  "pid": 1234,
  "started_at": "2026-08-05T11:01:51",
  "uptime_seconds": 1689,
  "service_health": {
    "last_cycle_at": "2026-08-05T11:29:50",
    "last_cycle_duration_s": 1.05,
    "cycle_lag_seconds": 10.0,
    "last_cycle_active": false,
    "queue": {"pending": 0, "error": 0, "dead": 0, "cleaned": 3829},
    "capture_since_start": {"enqueued": 103, "deferred": 0, "aged_out": 0}
  },
  "data_health": {
    "last_compare": {
      "at": "2026-08-05T05:08:21",
      "granularity": "ids",
      "tables_compared": 81,
      "mismatch": false,
      "mismatch_tables": 0
    },
    "drift_tables": [],
    "dead_rows_sample": []
  }
}

五、实现拆解

5.1 新增 src/sync/health.py(核心,约 200 行)

  • ServiceState 数据类:进程内全局状态对象,由 cycle() 每轮更新last_cycle_at / duration / active / 累计计数)。
  • HealthChecker 类:持有 ServiceState + SyncConfig,方法 snapshot() 返回上面 JSON 对应的 dict。实时查 SQL 用一次性 SqlWriter查完即关
  • run_health_server(state, cfg, host, port):起 http.server.ThreadingHTTPServerhandler 调 HealthChecker.snapshot() 序列化返回。

5.2 改 src/sync/service.py

  • run() 入口创建 ServiceState 实例,传给 cycle()cycle() 结束时更新 statelast_cycle_at 等)。
  • run() 起健康检查后台线程(threading.Thread(target=run_health_server, daemon=True)),主循环照常。
  • 新增累计:把每轮 CaptureStats 的 enqueued/deferred/aged_out 累加进 ServiceState

5.3 改 src/sync/config.py

RuntimeConfig 或新增 HealthConfig

class HealthConfig(BaseModel):
    enabled: bool = True
    host: str = "0.0.0.0"      # 监听地址,内网可访问
    port: int = 8421            # 健康检查端口
    path: str = "/health"       # URL 路径

SyncConfig 增 health: HealthConfig = HealthConfig()(默认值,老 config.yaml 无需改动)。

5.4 改 config.example.yaml

health 段示例。


六、不改动的地方

模块 是否改动 原因
capture/apply/cleanup 主循环逻辑零侵入,只由 cycle 更新一个内存 state
sql_writer 的写方法 健康检查只用现有的只读查询方法queue_status_summary 等),不新增写操作
NSSM 配置 单进程内置,端口由进程自己起
main.py 小改 incremental --loop 分支照常走 service.run(健康线程在 run 内起)

七、改动文件清单

文件 改动
src/sync/health.py 新增 ServiceState + HealthChecker + run_health_server
src/sync/config.py 新增 HealthConfigSyncConfig 加 health 字段
src/sync/service.py run() 起 health 线程 + 传 statecycle() 更新 state
config.example.yaml 补 health 段示例
tests/test_health.py 新增 覆盖 status 判定、snapshot 结构、各状态组合

零 SQL 改动、零 schema 改动、零新第三方依赖(标准库 http.server


八、待你确认的决策点

  1. 端口 8421 是否合适?(避开 114 上已有服务端口)
  2. unhealthy 的 HTTP 状态码:始终 200状态在 bodyvs unhealthy/degraded 返回 503便于按码告警我倾向后者。
  3. host 监听地址0.0.0.0(内网/FRP 都可访问vs 127.0.0.1(仅本机,需配合 FRP 转发)?我倾向 0.0.0.0
  4. drift_tables 查询范围:查"启动至今"的异常归档 vs 查"最近 N 小时"?我倾向"启动至今"(量可控,且能发现历史遗留)。

确认后即实施。