高负载下 readiness 一直抖时,实例其实不是“坏了”,而是在容量边缘来回掉线
高负载下 readiness 一直抖,往往不是探针接口自己坏了,而是实例的接流量能力反复跨过阈值:线程池、连接池、下游等待和探针判定一起顶到了边缘。先看谁最早失稳,再决定要不要动探针。
readiness 抖动最烦的地方,不是它让实例直接死掉,而是它让实例处在一种很难看的半失控状态:
- 一会儿 ready,一会儿 unready
- 刚接回一点流量,又被摘掉
- 集群副本数看着还在,用户体验却越来越差
这类现场最容易被一句话带偏:
探针太敏感了,调宽一点就好了。
这句话有时候只说对了一半。因为高负载下 readiness 抖动,很多时候不是探针本身出问题,而是实例的接流量能力已经跑在容量边缘,探针只是碰巧把这个事实暴露出来。
我会先分清:它是真的坏了,还是“不适合继续接新流量”
这两个状态很像,但处理方式不一样。
真的坏了
比如:
- 进程卡死
- JVM 已经 OOM 或持续 full GC
- 关键依赖彻底不可用
- 应用线程几乎完全拿不到执行机会
这种情况下 readiness 失败只是表象,本体是实例已经不可服务。
还活着,但一接新流量就更糟
这种更常见。
实例并没有完全死,但只要继续加流量,下面某一层就会先跨过阈值:
- Tomcat / Netty 工作线程先满
- 业务线程池 queue 开始堆
- 连接池归还速度下降
- 下游 RT 拉长,健康检查也跟着超时
于是实例被摘流;摘流后压力稍微缓一口气,它又恢复 ready;一恢复又接流量,然后再次失稳。你看到的就是“来回震荡”。
为什么会震荡,本质上是接流量能力反复跨过阈值
我更习惯把 readiness flap 理解成一个容量问题,而不是一个布尔状态问题。
想象一个实例的安全承载范围大概是每秒 800 请求;但现在集群在高峰下把它推到 750 到 900 之间来回波动。只要再叠加一点点下游变慢、连接归还变慢、GC 抖一下,探针判定就会从“还能接”切到“别再接了”。
于是现场常常长这样:
14:01 qps 上升
14:03 tomcat.busy 接近上限
14:04 hikari.pending 开始出现
14:05 readiness probe timeout
14:05 pod 被摘流
14:07 压力下降,probe 恢复成功
14:08 pod 重新接流
14:10 再次超时,被摘流
如果这条时间线是成立的,那探针并不是根因,它只是站在最末端宣布“这台机器现在不适合继续接单了”。
我最常看到的三条震荡链
第一条:入口线程先满,探针请求自己也被堵住
这是最直接的一种。
高峰流量把 Web 容器工作线程占满以后,健康检查请求本身也排不到执行机会,于是你看见的是 readiness 超时。但真实根因在入口线程已经顶满。
这时我会优先看:
tomcat.threads.busyhttp.server.requests的高分位 RT- 入口层 reject / timeout
第二条:业务线程池或连接池先失稳,探针只是后知后觉
有些项目的健康检查接口很轻,本身并不耗资源;但业务线程池和数据库连接池一旦开始排队,应用整体响应能力就会急剧下降,探针也会跟着变慢。
这种现场里,最早冒头的通常不是 health endpoint,而是:
- 自定义线程池 queue / active
- Hikari pending / acquire time
- 下游数据库或 RPC RT
第三条:readiness 依赖了过重、过敏的检查项
也有些项目确实把 probe 设计得太“认真”了:
- readiness 里直接打数据库
- readiness 里调用外部依赖
- readiness 里塞进很重的自定义检查
这会导致实例稍微有点抖动,probe 就立刻跟着一起抖。但即使如此,我也不会直接停在“探针写坏了”这层,而是仍然会先问:它为什么在高负载时更容易暴露这个问题?
为什么我通常不先改 probe 阈值
因为阈值调宽,很容易只是把告警和摘流时间往后拖几秒。
如果真正的问题是线程池、连接池或者某个下游先失稳,那你把 timeoutSeconds、failureThreshold 调大,最多只是让实例在更差的状态里多接一会儿流量。短期看波动可能少一点,长期看却可能把失败做得更深。
所以我更愿意先找“谁最早变坏”,再决定 probe 要不要动。
真到线上,我会先看哪几类证据
先看事件和时间线
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp
我要确认的是:
- probe fail 是从什么时候开始
- fail 前后有没有扩容、摘流、重启、节点抖动
- fail 和业务指标抬头谁在前谁在后
再看入口、执行层、数据库访问层谁先抬头
我通常会把下面这些指标放一起看:
- Web 容器 busy thread
- 自定义业务线程池 queue / active
- Hikari active / pending
- 下游数据库或 RPC RT
- health endpoint 自身 RT
如果入口线程最早打满,方向就很清楚;如果连接池 pending 先涨,就别只盯 probe 本身。
最后才看 readiness 判定条件本身
这时才去确认:
- readiness health group 里到底挂了什么检查项
- 超时阈值是不是明显偏紧
- 成功 / 失败阈值有没有滞后空间
- 恢复 ready 后是否马上就重新接满流量
震荡为什么会越来越严重
因为实例被摘流再接回时,往往不是“满血回归”,而是“半冷状态回归”。
- 本地缓存可能还没热
- 连接池刚刚恢复
- JIT / 热路径状态没稳定
- 剩余实例刚经历过额外压力
于是每次恢复 ready,集群都像是在把一个刚喘过气的实例重新推回高压线。只要负载还在边缘,下一次掉线几乎是注定的。
如果这时自动扩缩容的节奏又和摘流节奏打架,抖动会更明显。
我更习惯的止血顺序
第一步:先减轻继续放大的那部分流量
能限流、降级、关重路径、停回填任务的,先做。先把继续把实例往阈值外推的力量减下来。
第二步:确认健康检查是不是背了不该背的责任
如果 readiness 依赖了过重的检查项,当然要改;但这一步是在知道主链之后做,而不是一上来就当成唯一动作。
第三步:给恢复留一点缓冲,而不是立刻重新接满
有些系统的问题不是 fail 太快,而是恢复后重新接流过快。没有缓冲带,实例只会再次被立刻打回阈值外。
第四步:最后再决定 probe 参数是否需要调整
这时候调参数才是有依据的,不是在拿阈值碰运气。
最容易误判的地方
误判一:readiness 抖动就等于探针配置错了
不一定。更多时候是探针把容量边缘的问题照出来了。
误判二:实例能恢复 ready,说明问题不严重
恰恰相反,能恢复又立刻再掉,往往说明系统正在边缘震荡。
误判三:只要把阈值调宽,就能解决问题
很多时候只是把故障做得更隐蔽,而不是更轻。
最后收一句
高负载下 readiness 一直抖,真正要找的不是“为什么健康检查不稳定”,而是:到底是哪一层先把实例的接流量能力打穿了。
先把这个问题答出来,再去决定是改 probe、改隔离、改路由还是先止血,顺序才不会倒。