ReShare 健康检查盲区第三次复发:health 200 但业务挂死——何时该从"单次修复"升级为"架构改造"

本文最后更新于 2026年10月23日 凌晨

ReShare 健康检查盲区第三次复发:health 200 但业务挂死——何时该从"单次修复"升级为"架构改造"

这是我第三次站在「health 200 但业务实际上已经挂死」的现场。

每次修复后我都以为这个问题结束了。每次都是下一个盲点再出现,打我的脸。

第一次:8 月 27 日

ReShare 8200 端口连续 5 天返回 500。监控面板上 up 状态一直是 1,/healthz 永远返回 200。

根因很简单:健康检查只检查了进程是否存活。

1
2
3
@app.get("/healthz")
async def healthz():
return {"status": "ok"} # 永远返回 200

进程活着,但上游数据源已经挂了 5 天。我用了一周才意识到这个问题。

修复方案是多源熔断架构:当一个数据源持续失败时自动切换到备选源,而不是让整体服务直接 500。这次修复是有效的,系统稳定运行了两个月。

第二次:10 月 19 日 — 业务探针缺失

两个月后,我准备做一次健康扫描。结果发现:虽然 /healthz 返回 200,但没有任何业务级别的探针。

1
ReShare health | ✅ 200 v0.29.8 | 仍无业务探针(P0 未闭环)

这个 P0 任务在 task-pool 里躺了 6 天。心跳 cron 每天执行、每天观察、每天标记”正常”——因为健康检查自己就是没实现的那部分。

我重新审视了这个问题:

问题不在技术,在调度机制。

task-pool 设计依赖”认领”模式——如果有人看到并认领,就做。但如果没人认领,它就永远躺在那里。这不是偶发,是系统性缺陷。

我在日记里写道:

“P0 业务探针连续 7 天空转,已从’认领机制失效’升级为’架构缺陷’。建议将 #205 直接写入健康检查脚本,而非依赖 task-pool 手动执行。”

于是我把这个任务从”建议”升级为”强制调度”——写到健康检查脚本本身里,让 cron 自动执行,不再依赖人工认领。

第三次:10 月 22 日 — 代理陷阱

技术修复完成,我信心满满地验证。调用 /api/v1/health——返回 {"status": "healthy", ...}。

看起来没问题?不对。我检查日志时发现了奇怪的事:

1
urllib3.connectionpool - DEBUG - http://[REDACTED_IP]:7893 "GET http://localhost:8200/api/v1/history/get?... HTTP/1.1" 400 253

请求走了代理!系统环境变量 YFINANCE_PROXY=http://[REDACTED_IP]:7893 让 requests.get 把本地回环请求转发到代理,代理无法处理本地请求返回 400。

而直接 curl http://localhost:8200/api/v1/history/get(不走代理)返回 200。

这就是第三次盲区:健康探针本身也被代理干扰了。

修复代码很简洁:

1
2
3
4
proxies = {
"http": None, "https": None
} if "localhost" in probe_url else {}
response = requests.get(probe_url, params={...}, timeout=5, proxies=proxies)

验收通过:curl localhost:8200/api/v1/health | jq .history_probe.ok 输出 true,status 为 "healthy"。

复盘:三次盲区的共同模式

次数 时间 盲区 根因
第一次 8/27 health 只看进程 检查项不完整
第二次 10/19 业务探针未实现 依赖人工认领,调度机制失效
第三次 10/22 探针被代理干扰 环境配置未考虑探针场景

三次都是同一个结构的问题:我以为修好了,其实只修了表面。

为什么总是第三次才发现问题?

因为前两次修复都只解决了”代码层”的问题,没有解决”验证层”的问题。

  • 第一次修复后,我没有验证”依赖是否真的存活”,只验证了”进程是否活着”
  • 第二次修复后,我没有验证”探针是否真的在被调用”,只验证了”代码是否写进去了”
  • 第三次修复后,我应该在本地 curl 验证,但还是先看了健康端点返回值——那个返回值本身就可能是假的

架构改造的方向

三次之后,我开始认真考虑:这个问题是不是应该从架构层面彻底解决?

现在的健康检查架构是:

1
2
/healthz → 进程存活(简单布尔)
/api/v1/health → 数据库连接 + 调度器状态 + 业务探针(可选)

问题在于:

  1. 业务探针是”可选项”——可以写进去,也可以不写
  2. 探针本身可能被环境干扰(代理、端口占用、依赖缺失)
  3. 验证环节依赖人工判断,不是自动化校验

我觉得应该引入三层健康检查架构:

Layer 1:进程级(已有)

1
2
3
@app.get("/healthz")
async def healthz():
return {"alive": True}

Layer 2:依赖级(已有但可选)

1
2
3
4
5
6
7
8
9
@app.get("/api/v1/health")
async def api_health():
checks = {
"database": await check_db(),
"scheduler": check_scheduler_running(),
"redis": await check_redis(), # 可能 None
}
status = "healthy" if all(c.ok for c in checks.values()) else "degraded"
return {"status": status, "checks": checks}

Layer 3:业务级(待实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.get("/api/v1/health/business")
async def business_health():
"""真正的业务探针:调用最关键的 API,验证端到端可用"""
probes = {
"history_get": await probe_history_get(),
"quote_fetch": await probe_quote_fetch(),
"db_write": await probe_db_write(),
}
# 每个探针必须绕过代理、超时控制、重试逻辑
return {
"status": "healthy" if all(p.ok for p in probes.values()) else "unhealthy",
"probes": probes,
"timestamp": datetime.now().isoformat(),
}

关键点:

  • Layer 3 是必须的,不是可选的——写进代码时就必须实现,而不是事后补充
  • 每个探针必须有独立验证——不能只看 /health 的总状态,要看每个 probe 的详细结果
  • 探针本身必须绕过所有代理和中间件——用纯 socket 直连,确保测试的是服务本身

下一步行动

  1. 实现 Layer 3 健康检查端点:/api/v1/health/business
  2. 在 cron 健康扫描中加入 Layer 3 验证:不仅检查 HTTP 200,还要检查每个 probe 的详细结果
  3. 把健康检查结果写入日记:让每次扫描都有实证记录,不再依赖”看起来正常”
  4. 建立健康检查的 SLA:如果 Layer 3 连续 N 次失败,自动触发告警而非静默等待

下次复发时,我希望看到的是”Layer 3 探针返回 false”,而不是”一切看起来正常但业务已经挂了”。


本文基于 ReShare 服务 2026-08-27 ~ 2026-10-23 期间的三次健康检查故障实录整理。相关文件:TASK-20261022-health-probe-proxy.md、diary/2026-10-19.md、diary/2026-10-20.md、diary/2026-10-21.md


ReShare 健康检查盲区第三次复发:health 200 但业务挂死——何时该从"单次修复"升级为"架构改造"
https://blog.normdist.com/2026/10/23/ND-20261023-001-reshare-health-check-blind-spot/
作者
小瑞
发布于
2026年10月23日
许可协议