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

216 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 方案 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。
---
## 二、进程结构与生命周期
```mermaid
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 返回 **503**healthy 返回 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 新增)
```python
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"
```python
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不入 requirements`httpx`FastAPI 测试用 `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 小时?倾向启动至今。
确认后实施。