一个服务滚动发布,10 个新实例全部探针通过,流量切过去之后错误率飙到 60 %。回滚、再发、再切,同样的现象。查日志发现新实例不是"挂了",而是探针只检查了"进程活着",没检查"依赖可用"——它们连不上数据库,所有真实请求都在 500。
这类故障有个共同特征:探针的定义与"能不能服务"是两件事。而 K8s 的三种探针各自管一件不同的事,混用或只用一个,就会出现"要么永远不切流量、要么切了等于没切"。
1. 三种探针,三个不同的问题
| 探针 | 回答的问题 | 失败后 K8s 做什么 |
|---|---|---|
| liveness | 进程是不是该被重启 | 杀掉容器,重启 |
| readiness | 现在能不能接流量 | 从 Service 摘除 |
| startup | 启动完了没有 | 在它成功前不跑另外两个 |
第一行的动作最危险:liveness 失败会重启容器。所以它只能检查"不可恢复的僵死"(死锁、事件循环卡住),绝不能检查"依赖是否可用"——否则数据库抖动会被放大成"所有实例一起重启"。
第二行才是"能不能服务"的判定点:依赖不可用时摘流量而非重启,这是唯一的正确动作。
// 三种设计的后果对比
liveness 检查 DB 连接:DB 抖动 30s → 全部实例重启 → 启动风暴 → 事故扩大 10 倍
readiness 检查 DB 连接:DB 抖动 30s → 实例被摘流 → 前端 503 但实例还在 → 恢复即好
只有 liveness 没 readiness:坏实例一直收流量 → 用户看到 60% 错误
2. 探针要检查"最小的可服务集合"
| 检查项 | 该放哪个探针 | 理由 |
|---|---|---|
| 进程/端口存活 | liveness | 僵死才需要重启 |
| 依赖连通(DB/Redis/MQ) | readiness | 不可用时摘流即可恢复 |
| 本地缓存预热完成 | startup 或 readiness | 未预热就接流量会拖慢全部 |
| 磁盘可用 / 队列积压 | readiness(有条件) | 过载时摘流能保护自己 |
| 外部第三方接口可达 | 都不要 | 第三方故障不该影响自己的存活判定 |
第五行是实践中最容易过度的:把"第三方支付接口可达"放进 readiness,结果是对方一次抖动把你所有实例摘光,你的服务本身明明是健康的。
3. 超时与阈值:默认值几乎都是错的
| 参数 | 默认 | 默认值的问题 |
|---|---|---|
timeoutSeconds |
1 | 高负载下探针自己超时,误判 |
periodSeconds |
10 | 故障发现延迟 10s |
failureThreshold |
3 | 连续 3 次才动作 = 30s 窗口 |
initialDelaySeconds |
0 | 冷启动期被误杀 |
第二行与第三行叠加起来是**"最坏 30 秒才摘流量"**——对高 QPS 服务,30 秒意味着几十万次失败请求。所以生产上常用 periodSeconds: 3 + failureThreshold: 2(约 6 秒响应),代价是探针请求更频繁。
第四行提示:有了 startupProbe 之后,initialDelaySeconds 就该退场——startup 探针用宽松阈值覆盖启动期,liveness 用严格阈值覆盖稳态,两者分工比调一个大数字更清晰。
// 一个可用的三段配置(Spring Boot / Java 服务)
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 5
failureThreshold: 30 # 最多等 150s,覆盖冷启动
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 3
failureThreshold: 2 # 约 6s 摘流
# 注意:readiness 的端点内部必须包含 DB/Redis 检查,liveness 的不包含
4. 摘流量的两个隐藏延迟
即使 readiness 正确,流量也不会"瞬间"消失:
| 环节 | 延迟来源 |
|---|---|
| kube-proxy / iptables 规则更新 | 秒级,节点越多越慢 |
| 客户端连接池复用 | 已建立的连接继续发请求 |
| LB 健康检查周期 | 各云厂商不同,常 5–30s |
| 优雅停机宽限期 | 决定在途请求能否跑完 |
第二行是最容易被忽略的:被摘除的实例,连接池里的旧连接仍在工作。所以"先把 readiness 置 false、等 5–10 秒、再停进程"是标准顺序;反过来(先停进程)会让那部分连接直接被重置。
// 一次正确的下线时序
t=0 readiness 置 false(依赖不可用 / 准备停机)
t=0..10 等 kube-proxy 与 LB 收敛;在途请求正常返回
t=10 收到 SIGTERM → 停止接受新连接
t=10..30 等在途请求完成(宽限期)
t=30 进程退出
// 关键:那 10 秒等待是必须的,跳过它就会有"零星的 connection reset"
5. "假死":探针通过但服务不可用
| 假死形态 | 为什么探针看不出来 |
|---|---|
| 线程池被慢请求占满 | 端口能连、探针线程能跑 |
| 事件循环被 CPU 任务阻塞 | 探针 endpoint 走另一个线程 |
| 所有连接都卡在锁上 | 探针不需要那把锁 |
| 队列积压到内存极限 | 探针只看"活着" |
// 现场取证:探针通过但服务不健康时,先看这三处
$ curl -s localhost:8080/actuator/health/readiness | head -c 300
$ curl -s -o /dev/null -w "real_request=%{http_code}\n" localhost:8080/api/ping
$ curl -s localhost:8080/actuator/metrics/executor.queue.remaining | head -c 200
// 判据:readiness 200 而 real_request 500 → 探针没覆盖真实路径
第二种在 Node/Go 服务里最常见:探针线程与业务线程不是同一个,业务卡死而探针依旧 200。修法是把"业务健康信号"接进 readiness——例如"最近 1 分钟成功处理的请求数 > 0"或"线程池队列深度 < 阈值"。
// 一个可用的 readiness 组合条件(三条都要满足)
1 依赖连通:DB / Redis 各一次 ping(超时 300ms)
2 自身未过载:线程池队列使用率 < 80%
3 最近有成功:近 60s 内成功请求数 > 0
// 第三条专治"假死":业务全卡住时它必然为 false
6. 排错:五种"探针正常但服务异常"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
| 新实例探针过、请求 500 | readiness 没查依赖 | readiness 加依赖检查 |
| 依赖抖动导致全体重启 | 依赖检查放在 liveness | 移到 readiness |
| 第三方故障把自己摘光 | 第三方检查进了 readiness | 移除,改降级 |
| 发布后零星 connection reset | 摘流量后没等收敛 | 加 5–10s 等待 |
| 高负载时实例反复重启 | 探针 timeout 太短 | 调到 3s + 阈值 2 |
第一行与第二行是一对:同一个检查项放错探针,会产生完全相反的事故。放 readiness 是"摘流自保",放 liveness 是"重启风暴"。
7. 边界:什么情况不需要复杂探针
| 场景 | 为什么 | 做法 |
|---|---|---|
| 无状态纯计算服务 | 无依赖可查 | 端口探测足够 |
| 单实例/单机部署 | 没有"摘流量"的概念 | 只做存活监控 |
| 批处理任务(Job) | 不接流量 | 看退出码即可 |
| 依赖本身就是弱依赖 | 不可用时应降级而非摘流 | 移除该项检查 |
判断口径一句话:探针要回答"这个实例现在该不该接流量",而不是"这个实例有没有问题"——前者是摘流的依据,后者是重启的依据,两者混淆就是事故来源。而检查项的取舍标准是:这项失败时,摘流能不能让用户更好;不能,就别放进 readiness。
8. 自测
- 三种探针各自回答什么问题?哪个失败会导致容器重启?
- 为什么"依赖检查"该放 readiness 而不是 liveness?放错会放大成什么事故?
- 第三方接口可达性为什么两个探针都不该查?
periodSeconds: 10+failureThreshold: 3意味着最坏多久才摘流量?生产上怎么调?- 摘流量之后为什么还要等 5–10 秒才停进程?
- "假死"为什么探针看不出来?readiness 里该加什么来判断?
9. 一句话总结
探针回答的是"该不该接流量",不是"有没有问题":readiness 失败摘流、liveness 失败重启——把依赖检查放进 liveness,会把数据库抖动放大成重启风暴。三件纪律:readiness 查最小可服务集合(依赖 + 过载 + 最近有成功)、liveness 只查僵死、第三方故障一律不查;摘流之后要等 5–10 秒让 kube-proxy 与连接池收敛,再发 SIGTERM。