Spring Boot 服务发布后状态开始漂移,怎么判断问题先从哪一层开始
16:04 发布完成时一切都还是绿的,16:11 开始只有两台新 Pod 的报价结果不对,16:18 P99 才跟着抬头。发布后“状态漂移”最怕的不是难,而是把后面出现的红灯误当成第一现场。
发布后最让人难受的,通常不是一上线就挂,而是那种刚发完像没事,过几分钟才一点点跑偏的现场。
有次我看 pricing-service 的发布,就是这么开始的:
| 时间 | 事件 | 现场信号 |
|---|---|---|
| 16:00 | 开始发布 pricing-service v2026.03.23.4 | 老实例稳定 |
| 16:04 | 6 个新 Pod 全部 Ready | 健康检查全绿 |
| 16:11 | 业务方反馈部分报价结果偏旧 | 只有 2 个新 Pod 能复现 |
| 16:15 | rule-cache miss rate 在这 2 个 Pod 上升高 | 接口 RT 还没明显变差 |
| 16:18 | /quote/calc P99 210ms → 1.9s | 仍集中在这 2 个 Pod |
| 16:21 | readiness 开始间歇波动 | 大盘这时才真正红起来 |
这张时间表对我最有帮助的地方,是它把“状态漂移”拆开了。
如果不把这 20 分钟摊平,团队很容易只盯住 16:21 的 readiness 波动,或者 16:18 的性能抬头,然后把最早出现的那条分叉完全盖掉。
而这类问题真正的入口,往往不是最后那盏最红的灯,而是最先开始和别人不一样的那一层。
先别把“状态漂移”当成一句笼统的话
发布后说“状态漂了”,其实至少可能是三种完全不同的东西。
第一种:健康状态先漂
最常见的形状是:
- readiness、liveness 或自定义 health indicator 开始抖
- 实例在摘流量和接流量之间来回跳
- 业务还没明显报错,但 Pod 状态已经不稳
这种时候,优先级通常在探针、注册发现、启动时序、依赖探活。
第二种:行为状态先漂
这是我更怕被忽略的一种。
- 同样的请求,在部分新实例上返回旧规则、旧配置、旧结果
- 健康检查是绿的
- RT 也未必立刻很差
- 但业务结果已经分叉
这说明服务“活着”,但跑出来的状态已经不是一套了。
第三种:性能状态先漂
这一类更直观:
- P95、P99、timeout 开始抬
- 线程池、连接池、GC 或下游等待变差
- 但功能上不一定马上错
很多人一看到性能告警就立刻冲进去抓 SQL、抓线程栈。问题是,性能漂移有时只是后果,不是第一现场。
上面那次发布,最早分叉的是行为状态:只有两台新 Pod 还在按旧规则出报价。性能和健康都是后面才跟上来的。
我更愿意先做的,不是分类,而是把发布后的分钟级时间线钉死
我会强迫自己先回答三个问题:
- 发布完成时,现场到底有没有马上异常?
- 第一条可重复的分叉,最早出现在哪一分钟?
- 后面出现的红灯,是先前分叉的结果,还是新的分叉?
这一步很土,但非常值钱。
因为发布窗口里常常不是只有代码在变。还有:
- 流量灰度
- 配置生效
- 本地缓存预热
- 定时任务恢复
- Sidecar / 注册中心同步
- 依赖连接池重新建立
你要是不把时间线拉平,后面看到什么都能解释,反而最难收敛。
这次真正该从哪层开始?答案不是健康,也不是性能
继续往下看那 2 个异常 Pod 的对照,第一条最值钱的证据其实是这个:
| 指标 | 异常 Pod | 正常新 Pod |
|---|---|---|
| readiness | UP | UP |
/quote/calc 结果版本 | 旧规则 | 新规则 |
ruleCache.version | 2026-03-18 | 2026-03-23 |
ruleCache.missRate | 38% | 1.2% |
/quote/calc P99 | 1.6s | 240ms |
这张表一出来,我就不太愿意先去查探针了。
因为它告诉我:
- 这 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 和回源,性能才跟着变差
- 最后缓存加载重试和探针波动一起把健康状态也拖坏了
你会发现,所谓“发布后状态开始漂移”,最后其实不是一句抽象判断,而是一条很具体的顺序链。
真正难的不是知识点,而是别被最后那盏最红的灯抢走视线。只要你能把发布后的分钟级时间线拉平,再盯住最先开始分叉的那一层,这类问题通常不会查得太散。