readinessProbe 不是健康检查:它决定流量,不决定重启
readinessProbe 不是健康检查:它决定流量,不决定重启
结论先行
Kubernetes 的三个探针里,最常被误解的是 readinessProbe。很多人把它当成"健康检查的第二种写法",于是把 livenessProbe 的判据原样复制过去——进程活着、端口能连上、接口返回 200。这样配出来的探针在大多数时间里"工作正常",却在你最需要它的那一刻完全失效:数据库连接池被打满时,进程还活着,端口还在监听,健康检查接口依然返回 200,readiness 于是继续把流量放进这个已经无法处理请求的 Pod。
三者的职责必须分清:
livenessProbe 决定"要不要重启这个容器",失败的处理是 kill 掉重来。
readinessProbe 决定"要不要把流量给它",失败的处理只是从 Service 的 Endpoints 里摘掉,容器不动。
startupProbe 决定"启动期要不要宽容",在它成功之前另外两个探针都不跑。
判断一个探针写对没写对,只有一个标准:它失败之后,系统的处置动作是不是你希望发生的那件事。希望重启的放 liveness,希望摘流量的放 readiness,两个都想要就写两个不同判据的探针。
为什么"能连上"是最糟的判据
一个探针接口最容易掉的坑是写成对本地状态的检查:读一下自己的进程、ping 一下自己的端口、返回一个写死的 ok。这类接口的失败条件是"进程死了"——而进程死了的时候,kubelet 自己就知道,根本不需要你去探测。
探针的价值全部在"进程活着但服务不可用"这个区间里。这个区间的成因很具体。
依赖侧的连接池耗尽。数据库最大连接数打满,应用侧的连接获取超时,请求全部卡在三十秒的等待上,但 HTTP 服务的 accept 队列照常工作,端口探测一切正常。
内部工作线程池饱和。异步任务队列积压到几千条,同步接口被同一个线程池共享,响应时间从二十毫秒涨到八秒;而健康检查接口走的是独立路径、不碰线程池,依然一毫秒返回。
缓存与降级状态翻转。缓存集群整体不可达,所有请求击穿到数据库,QPS 瞬间超限,但服务本身没报错、没崩溃,只是全部超时。
这三种情况下,"端口通"和"接口返回 200"都是真的,而服务对用户已经死了。正确的 readiness 判据必须包含依赖可用性与自身饱和度的信号:连接池可用连接数低于阈值、请求排队长度超过阈值、最近一分钟的依赖调用错误率高于阈值,任何一条命中就返回 503。
探针参数:默认值几乎总是错的
periodSeconds 默认十秒,timeoutSeconds 默认一秒,failureThreshold 默认三次。翻译成实际行为:从"服务开始不可用"到"真正被摘出 Endpoints",最长要三十秒延迟;从"服务恢复"到"重新接流量",又要三十秒。前一个三十秒是所有打到这个 Pod 的请求在等待超时,后一个三十秒是容量被凭空扣掉一份。
更糟的是 timeoutSeconds 设成一秒,却让探针接口去检查依赖,这两件事是矛盾的。正常情况下问一次数据库要两毫秒,压力大的时候要八百毫秒,超过一秒就被判失败。于是探针本身成了负载的放大器:压力越大越容易失败,越失败越摘流量,剩下的 Pod 压力更大,形成级联。这不是探针的错,是把"依赖检查"和"一秒超时"配在一起的结果。
可用的参数组合大致是这个形状。
readiness 用短周期、低阈值,追求反应快:periodSeconds 三秒、timeoutSeconds 两秒、successThreshold 一次、failureThreshold 两次,最坏六秒完成摘流量。代价是 kubelet 的请求变多,一个一千 Pod 的集群每秒会多出三百多次探针请求,所以探针接口必须足够轻。
liveness 用长周期、高阈值,追求不误杀:periodSeconds 二十秒、timeoutSeconds 三秒、failureThreshold 六次,最坏两分钟才重启一次。liveness 的判据要刻意保守,只检查那些"重启真能解决"的问题,比如主循环死锁、配置解析后进入永久不可用状态。数据库连不上不该触发 liveness,重启一个健康的进程对它毫无帮助,只会让所有 Pod 一起重启,把一次依赖故障变成一次全站故障。
startupProbe 用来兜住慢启动:periodSeconds 五秒、failureThreshold 六十次,给足五分钟。没有它,慢启动的应用只能靠把 liveness 的初始延迟调大来换取启动时间,而那个值在运行期又变成误杀窗口。
一个具体的改写
改写前:两个探针都指向同一个永远返回 200 的 healthz,periodSeconds 都是十秒,initialDelaySeconds 三十秒。问题在于判据是空的,等于两个都没写。
改写后:startupProbe 指向 healthz,periodSeconds 五秒、failureThreshold 六十次;livenessProbe 指向 healthz/live,periodSeconds 二十秒、timeoutSeconds 三秒、failureThreshold 六次;readinessProbe 指向 healthz/ready,periodSeconds 三秒、timeoutSeconds 两秒、failureThreshold 两次。
其中 healthz/live 只检查主循环是否还在推进——一个每秒自增的计数器,两秒没动就返回 500;healthz/ready 检查三件事:数据库连接池可用连接数大于零、请求排队长度小于阈值、最近六十秒依赖错误率小于 20%。两个接口都不做真实业务查询,最坏耗时控制在五十毫秒内。
三个容易被忽略的边界
第一,探针失败不摘流的场景。如果 Service 打开了 publishNotReadyAddresses,或者调用方直接用 Pod IP、走 Headless Service 做客户端负载均衡,readiness 失败对流量没有任何影响。这种架构下必须由调用方自己实现摘除逻辑,探针只能当监控信号用。
第二,滚动更新与 readiness 的耦合。maxUnavailable 与 maxSurge 的语义依赖 readiness 的判断:新 Pod 只有 ready 之后才算"已就绪",滚动才会继续推进。如果 readiness 判据过于宽松,更新会在新 Pod 还连不上数据库时就宣布完成,用户看到的是几分钟的错误率上升;反过来,判据过于严格会卡住整个发布,只能手工回滚。
第三,探针不是监控。探针的采样频率由 kubelet 决定,历史数据不留存,也没有告警通道。把探针当唯一可观测手段,等于用"摘流量"这种破坏性动作去发现"依赖变慢"这种渐进问题。真正的做法是探针负责处置,指标负责发现:连接池使用率、排队长度、依赖 P99 这些指标要有告警,探针只是最后一道把坏实例从池子里踢出去的闸门。
小结
写探针之前先问一句:这个检查失败之后,我希望系统重启它,还是希望系统不再给它流量。答案决定它该挂在哪个字段上。然后再问一句:它检查的东西,是不是只有在这个进程"活着但坏了"的时候才会失败。两个问题都答对,探针才算写完了。
- 点赞
- 收藏
- 关注作者
评论(0)