服务器健康检查(Health Check)入门指南
健康检查是服务自我汇报状态的接口。本文讲清 liveness 与 readiness 的区别、健康检查该查什么(资源/进程/依赖)、失败处理策略与七条最佳实践。
健康检查是应用主动暴露的一个"自检接口"(通常是 /healthz 或 /health),负载均衡器、K8s、监控系统定期调用它,据此决定要不要把流量发给这个实例、要不要重启它。设计良好的健康检查是自愈系统的感知器官;设计糟糕的则要么误杀健康实例,要么带病硬扛。
最小可用的健康检查端点
// Express 版最小实现
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'ok' });
});
返回 200 即健康——但这只证明进程活着。生产环境需要更分层的设计。
两类健康检查:Liveness 与 Readiness
这是最重要的概念区分(K8s 里直接对应两个探针):
- Liveness(存活检查):回答"这个进程还值得留着吗?" 失败 = 进程卡死/死锁,重启它。检查项应该极简——只验证进程内部状态,不要检查外部依赖。
- Readiness(就绪检查):回答"这个实例现在能接流量吗?" 失败 = 暂时摘除,不重启。这里才检查数据库连接、缓存、配置加载等依赖。
经典事故剧本:数据库挂了 → 所有实例的 liveness 检查失败(因为查了数据库)→ K8s 重启所有 Pod → 重启后数据库还没好,继续失败 → 滚动雪崩,整个集群自杀。Liveness 里查外部依赖是健康检查设计的第一大忌。
健康检查清单:该查什么
- 进程状态:进程活着、能响应(liveness 的核心);
- 服务器资源:磁盘未满、文件描述符未耗尽、内存未到临界点;
- 核心依赖(readiness):数据库连接池可用、缓存可读写、消息队列可连;
- 异常自查:内部错误率超阈值时主动报告"亚健康"。
失败后的处理策略
健康检查失败不是只有"重启"一个选项:
- 重启或替换实例:进程级故障(死锁、内存泄漏到临界)→ K8s 自动完成;
- 摘出流量池:依赖故障 → readiness 失败,LB 把流量切走,恢复后自动回池;
- 降级服务:非核心依赖故障(推荐服务挂了)→ 返回降级结果而不是整体 500;
- Fail-open(谨慎使用):非关键检查失败时继续服务,只记录——比如灰度配置中心连不上时用本地缓存配置;
- 告警:无论自动处置是什么,人要知道发生了什么。
七条最佳实践
- 区分关键与非关键依赖:数据库挂了该摘除,日志收集器挂了不该——别把非关键依赖放进 readiness。
- 防级联故障:健康检查本身别成为压垮服务的稻草(见下条);依赖方的故障也别通过你的健康检查传导放大。
- 后台异步执行检查:依赖探测放后台线程定期跑,检查结果缓存起来,健康端点只读缓存——避免每个 LB 请求都触发一次数据库查询(几十个 LB × 每秒探测 = 额外负载风暴)。
- 防误报:瞬时针刺不该摘流量——连续 N 次失败才判定不健康(K8s 的
failureThreshold就是干这个的)。 - 深浅分离:
/healthz(浅,进程活着就行)和/health/deep(深,查依赖)分开——LB 用浅的,排查用深的。 - 探测频率适度:每 1-5 秒一次足够,别搞成每秒十次。
- 禁缓存:健康端点加
Cache-Control: no-cache,CDN/代理绝不能缓存健康检查结果——缓存的"健康"是最危险的状态。
观测云对照
健康检查端点天然适合接入可用性监测:观测云对 /healthz 做外部拨测,从用户视角验证服务健康;K8s 环境里,观测云采集 Pod 的 liveness/readiness 探针事件与重启记录,实例频繁重启、就绪抖动都会成为可告警的信号;结合 APM 看摘除/重启前后的请求成功率变化,健康检查策略是否合理有据可依。
常见问题(FAQ)
Q:liveness 里到底能不能查数据库?
A:不能。liveness 失败 = 重启,而数据库挂了重启应用进程毫无用处,只会造成滚动重启雪崩。数据库放 readiness。
Q:健康检查返回什么?200 就够吗?
A:状态码是协议(200 健康、503 不健康),响应体可以带结构化细节:各依赖的状态、版本号、运行时长——排查时非常有用。
Q:微服务里每个服务都要健康检查吗?
A:是。每个可被 LB/网关注册的服务都应暴露健康端点;同时调用方也该有客户端侧的健康感知(连接池探活、熔断器)。