Java

Spring Boot 服务发布后状态开始漂移,怎么判断问题先从哪一层开始

16:04 发布完成时一切都还是绿的,16:11 开始只有两台新 Pod 的报价结果不对,16:18 P99 才跟着抬头。发布后“状态漂移”最怕的不是难,而是把后面出现的红灯误当成第一现场。

  • Spring Boot
  • 发布排查
  • 健康检查
  • 缓存
  • 配置
18 分钟阅读

发布后最让人难受的,通常不是一上线就挂,而是那种刚发完像没事,过几分钟才一点点跑偏的现场。

有次我看 pricing-service 的发布,就是这么开始的:

时间事件现场信号
16:00开始发布 pricing-service v2026.03.23.4老实例稳定
16:046 个新 Pod 全部 Ready健康检查全绿
16:11业务方反馈部分报价结果偏旧只有 2 个新 Pod 能复现
16:15rule-cache miss rate 在这 2 个 Pod 上升高接口 RT 还没明显变差
16:18/quote/calc P99 210ms → 1.9s仍集中在这 2 个 Pod
16:21readiness 开始间歇波动大盘这时才真正红起来

这张时间表对我最有帮助的地方,是它把“状态漂移”拆开了。

如果不把这 20 分钟摊平,团队很容易只盯住 16:21 的 readiness 波动,或者 16:18 的性能抬头,然后把最早出现的那条分叉完全盖掉。

而这类问题真正的入口,往往不是最后那盏最红的灯,而是最先开始和别人不一样的那一层。

先别把“状态漂移”当成一句笼统的话

发布后说“状态漂了”,其实至少可能是三种完全不同的东西。

第一种:健康状态先漂

最常见的形状是:

  • readiness、liveness 或自定义 health indicator 开始抖
  • 实例在摘流量和接流量之间来回跳
  • 业务还没明显报错,但 Pod 状态已经不稳

这种时候,优先级通常在探针、注册发现、启动时序、依赖探活。

第二种:行为状态先漂

这是我更怕被忽略的一种。

  • 同样的请求,在部分新实例上返回旧规则、旧配置、旧结果
  • 健康检查是绿的
  • RT 也未必立刻很差
  • 但业务结果已经分叉

这说明服务“活着”,但跑出来的状态已经不是一套了

第三种:性能状态先漂

这一类更直观:

  • P95、P99、timeout 开始抬
  • 线程池、连接池、GC 或下游等待变差
  • 但功能上不一定马上错

很多人一看到性能告警就立刻冲进去抓 SQL、抓线程栈。问题是,性能漂移有时只是后果,不是第一现场。

上面那次发布,最早分叉的是行为状态:只有两台新 Pod 还在按旧规则出报价。性能和健康都是后面才跟上来的。

我更愿意先做的,不是分类,而是把发布后的分钟级时间线钉死

我会强迫自己先回答三个问题:

  1. 发布完成时,现场到底有没有马上异常?
  2. 第一条可重复的分叉,最早出现在哪一分钟?
  3. 后面出现的红灯,是先前分叉的结果,还是新的分叉?

这一步很土,但非常值钱。

因为发布窗口里常常不是只有代码在变。还有:

  • 流量灰度
  • 配置生效
  • 本地缓存预热
  • 定时任务恢复
  • Sidecar / 注册中心同步
  • 依赖连接池重新建立

你要是不把时间线拉平,后面看到什么都能解释,反而最难收敛。

这次真正该从哪层开始?答案不是健康,也不是性能

继续往下看那 2 个异常 Pod 的对照,第一条最值钱的证据其实是这个:

指标异常 Pod正常新 Pod
readinessUPUP
/quote/calc 结果版本旧规则新规则
ruleCache.version2026-03-182026-03-23
ruleCache.missRate38%1.2%
/quote/calc P991.6s240ms

这张表一出来,我就不太愿意先去查探针了。

因为它告诉我:

  • 这 2 个 Pod 并不是“没活着”
  • 它们是活着,但拿着旧状态在工作
  • 性能变慢是旧状态持续 miss、回源和重算后的结果

日志里也能接上这条线:

16:05:12 INFO  RuleCacheBootstrap start targetVersion=2026-03-23
16:05:19 WARN  RuleCacheBootstrap fallbackToLastSnapshot snapshotVersion=2026-03-18 reason=remote load timeout
16:11:07 WARN  quote result use stale rule version=2026-03-18 traceId=9ab1...
16:18:44 WARN  /quote/calc cost=1942ms reason=ruleMissAndReload pod=pricing-new-2

从这里开始,排查入口就变了。

如果我还继续拿“readiness 绿不绿”当主线,等于把最早出现的行为漂移又盖了一层。更合适的做法,是先追:

  • 为什么这 2 个 Pod 启动后没把规则缓存切到新版本
  • fallback 到旧快照以后,为什么没有重新收敛
  • 为什么后面才拖出性能和健康的连锁反应

真到发布现场,我会按“谁先分叉”来决定先钻哪层

如果是健康先漂

更像是这些方向:

  • readiness 过早放流
  • liveness 把还没收敛的实例反复拉起
  • 注册中心摘挂不稳定
  • 某个 health indicator 把依赖抖动直接放大了

这种时候,先看探针定义、发布步进、依赖探活,比先翻业务日志更有用。

如果是行为先漂

我会优先看:

  • 本地缓存和本地对象是不是切到新状态了
  • 配置有没有部分实例仍读旧 source
  • 启动后的 bootstrap、同步、预热任务有没有半途失败
  • 同版本实例之间是否存在 profile、挂载文件、环境变量差异

这一类问题,最容易被“健康是绿的”骗过去。

如果是性能先漂

再去查:

  • 线程池、连接池、RPC 等待链
  • 新版本引入的重路径
  • 新老实例之间的资源现实差异
  • 背景任务、预热任务、流量切换是不是把新实例先顶住了

性能线当然重要,但我通常只会在确认它是最早分叉层时,才把它放到第一位。

有些“发布后漂移”,其实是在运行几分钟后才被放大

这也是为什么我不太相信“发布完成时没问题,那就不是发布相关”。

很多漂移不是一上线就站出来,而是要等下面这些动作慢慢叠上去:

  • 本地缓存第一次 miss
  • 定时任务恢复
  • 灰度流量继续放大
  • 长连接、线程池、连接池开始累积压力
  • 某个 fallback 持续吃旧快照

上面那次就是这样。16:04 Ready 时没红,不代表没问题;它只是还没跑到能把问题放大的那一步。

所以发布后判断入口时,我会特别在意一句话:

它是“发布即坏”,还是“发布后跑着跑着才坏”?

这两种现场,后面的排查顺序差很多。

我最后会把证据收回一条很短的线

那次问题最后收得很简单:

  • 发布后 2 个新 Pod 在启动阶段加载规则超时
  • 它们 fallback 到了旧快照
  • readiness 没把这件事挡住,实例照样接流量
  • 同样请求在这 2 个 Pod 上开始跑出旧行为
  • 旧规则导致更多 miss 和回源,性能才跟着变差
  • 最后缓存加载重试和探针波动一起把健康状态也拖坏了

你会发现,所谓“发布后状态开始漂移”,最后其实不是一句抽象判断,而是一条很具体的顺序链。

真正难的不是知识点,而是别被最后那盏最红的灯抢走视线。只要你能把发布后的分钟级时间线拉平,再盯住最先开始分叉的那一层,这类问题通常不会查得太散。