Java

Spring Boot 灰度发布后,为什么只有部分实例慢或行为不一致?

灰度发布后只有部分实例变慢,最容易让人把锅全甩给"新版本"。但很多时候,真正拉开差异的不是代码本身,而是流量命中、环境现实、冷启动和本地状态没有对齐。

  • Spring Boot
  • 灰度发布
  • 发布排查
  • 实例差异
  • 故障排查
17 分钟阅读

灰度发布最难排的,通常不是全量一起挂,而是这种只有一部分实例不对、又说不清到底哪里不对的状态:

  • 只有一部分新实例变慢
  • 只有灰度实例行为和旧版本不一致
  • 同样一个请求,有时正常,有时像落到了另一套逻辑
  • 平均指标看起来还行,但局部实例 RT、错误率、探针状态已经开始飘

这种场景最容易把团队带进两种低效判断里:

  • 不是全量异常,说明问题不严重,先观察
  • 只有部分实例异常,说明肯定是机器偶发问题

这两种都很危险。

因为灰度场景里“只有部分实例慢或行为不一致”,往往不是偶发,而是在提醒你:

当前问题和实例维度、流量维度或环境维度强相关。

更常见的真实根因通常包括:

  • 灰度流量本来就不是随机样本,而是更重、更偏的请求
  • 新实例虽然代码一样,但环境变量、配置源、JDK、资源限制并不完全一样
  • readiness 太早,新实例还没热起来就开始接流量
  • 本地缓存、连接池、线程池和 JIT 都还是冷的
  • 新旧版本共用数据库、缓存和下游资源,局部实例先被放大
  • 功能开关、序列化协议、本地状态让不同实例走了不同代码路径

所以真正值得抓住的主线,不是一句“灰度有问题”,而是:

先把问题按实例、版本、节点、流量和时间维度摊开,再判断差异到底来自流量本身、运行环境、冷启动状态,还是配置与本地状态漂移。

灰度后只有部分实例慢或不一致时,先把差异维度摊开

先用这张表把范围压到实例、流量和环境几个维度,别把所有”部分实例不对”都混成同一个问题。

你现在看到的现象更像哪类问题下一步更适合去哪篇
只在灰度窗口、灰度实例或灰度流量里出现部分实例慢或行为不一致,想先判断差异来自流量、冷启动还是环境典型灰度差异问题先用本文的对比矩阵拆开
发布后只有少数实例异常,但还没确认是不是灰度规则、灰度流量带来的差异更像通用实例对照问题发布后只有一小批实例异常,按实例对照顺序查 Actuator、配置和环境差异
已经怀疑 profile、环境变量、挂载文件或 JVM 参数不一致更像环境现实核对问题Spring Boot 环境不一致怎么查?从 profile、挂载文件到 JVM 参数逐项核对
已经明确是新旧值混跑、配置刷新或 source drift 导致的行为差异更像配置生效链问题Spring Boot 配置改了还是旧值?从配置生效链判断卡在哪一层
实例状态反复 ready / not ready,更像探针和时序问题更像运行态状态管理问题Spring Boot 健康检查、就绪探针和优雅下线为什么经常让服务状态看起来不对?

如果你的现场最像第一行,就直接沿这个矩阵收范围;如果灰度规则本身都还没确认清楚,别急着把所有问题都归到代码层。

一、慢和不一致,经常不是同一类问题

灰度发布后的异常,至少可以拆成三类。

1. 性能差异

常见现象:

  • 只有一部分实例 RT 高
  • 某些实例线程池、连接池水位更高
  • 只有灰度实例慢,旧实例正常

这类更偏:

  • 冷启动
  • 流量偏置
  • 环境差异
  • 共享资源争抢

2. 行为差异

常见现象:

  • 同一请求命中新旧实例时返回结果不一致
  • 同样配置下,只有部分实例走新逻辑或旧逻辑
  • 某些实例功能开关状态和别的实例不一样

这类更偏:

  • 配置漂移
  • feature flag 不一致
  • 本地缓存 / 本地状态不同
  • 代码路径受环境变量、profile、依赖版本影响

3. 状态差异

常见现象:

  • 部分实例 readiness 频繁波动
  • 部分实例反复重启、摘流量、再接流量
  • 只有灰度实例健康状态看起来不稳定

这类更偏:

  • 探针定义和启动时序
  • 资源限制
  • 启动预热不完整
  • 环境差异或局部依赖抖动

先分清你看到的是性能差异、行为差异还是状态差异,后面排查才不会混线。

下面这张灰度差异定位表,更适合拿来决定第一轮先往哪边切。

你先看到的信号第一轮更该先看什么常见下一步
只有灰度实例 RT 明显更高灰度流量样本和冷启动窗口先核对流量偏置、预热和 readiness
同请求打到灰度实例和旧实例结果不一致配置来源、feature flag、本地状态先核对环境差异、配置漂移和本地缓存
灰度实例 readiness 波动、反复摘流量探针定义和启动时序先查冷启动、探针和高负载下状态管理
只有某一批灰度实例慢,且集中在特定节点或机房环境现实和共享资源差异先查 JVM、limit、DNS 和共享依赖链

二、第一步:把对比矩阵立起来,不要只盯“灰度版本”这一个维度

灰度现场最怕的,就是大家脑子里只有“新版本 vs 旧版本”。

真实项目里,更值得先对齐的是这个矩阵:

  • 版本:新版本还是旧版本
  • 实例:是哪几台实例
  • 节点 / 机房 / AZ:它们跑在哪些宿主环境上
  • 流量:它们接到的是哪类用户、哪类 URI、哪类租户
  • 时间:是刚启动时异常,还是跑一段时间后才异常

为什么这一步优先级最高

因为很多“只有部分实例有问题”的现场,真正的关键差异根本不在版本本身。

例如:

  • 是新版本里只有跑在某个节点上的实例慢
  • 是接到灰度租户的实例慢,不是所有新实例都慢
  • 是实例刚启动 10 分钟内慢,热起来后正常
  • 是某一组实例因配置源不同,表现像另一套逻辑

如果没有这个对比矩阵,你后面很容易把所有问题都归因给“代码刚发布”。

三、第二步:先确认灰度流量本身是不是就和全量流量不一样

很多团队默认灰度流量等于“随机抽一部分正常请求”,但现实里经常不是。

常见偏流量场景

  • 灰度只放内部用户、测试用户或大客户
  • 某些 URI、某些功能按钮只在灰度版本打得更多
  • 某类 header、租户、AB 实验开关只打到灰度实例
  • 观察期内灰度实例承接了更热的 key、更重的查询或更频繁的写请求

这类问题为什么高发

因为从监控上看,你会看到:

  • 只有部分实例慢
  • 只有部分实例行为不一致

但根因其实不是实例本身有问题,而是:

它接到的根本不是同一种流量。

这一层最值得先回答的问题

  • 慢的是不是固定某几类 URI
  • 慢的是不是固定某类租户或业务线
  • 灰度实例是不是承接了更高权重流量
  • 某些功能开关是不是只在灰度流量中打开了

如果流量样本本来就不一致,后面直接对比整体 RT 往往会被带偏。

四、第三步:如果问题集中在新实例刚接流量时,优先查冷启动、预热和 readiness 时序

灰度发布后,部分实例慢得最明显的一个高发原因,就是它们还没真正“热起来”。

常见冷启动链路

  • JIT 还没热
  • 本地缓存还空着
  • 数据库连接池、HTTP 客户端连接池还没预热
  • 线程池刚启动,任务排队策略还没跑稳
  • 启动后一些必要数据刚开始加载

readiness 最容易出的问题

  • 端口一起来就 ready
  • 但关键缓存、连接、规则、配置其实还没准备好
  • 流量过早打入,导致新实例比老实例慢得多

这类问题的典型特征

  • 新实例启动后的前几分钟明显更慢
  • 时间一长,指标逐步恢复
  • 只有刚切流量的实例不稳定
  • 并不是所有新实例长期都差

如果你看到的是这种“刚接流量时抖,跑一会儿好转”的模式,优先级应该先给:

  • readiness 定义
  • 启动预热
  • 连接池 / 本地缓存 / JIT 冷启动

而不是一上来就认定是代码逻辑退化。

五、第四步:如果只有部分实例一直异常,就优先查环境差异和配置漂移

这才是灰度里最容易耗时间的一层。

很多团队会以为:

  • 镜像一样
  • 代码一样
  • 那实例就应该一样

真实项目里并不成立。

常见环境差异包括

  • 环境变量不同
  • profile 不同
  • JVM 参数不同
  • JDK / 基础镜像版本不同
  • 容器 CPU / 内存 limits 不同
  • Sidecar、代理、节点内核或 DNS 差异

常见配置漂移包括

  • 新实例订阅了不同 namespace / group / dataId
  • 启动参数仍然带着旧开关
  • ConfigMap / Secret 挂载和仓库配置不一致
  • 部分实例刷新到新配置,部分实例没有

这类问题的典型特征

  • 不是所有新版本实例都差,只有某一批差
  • 同版本不同实例行为不一致
  • 问题和节点、机房、实例标签高度相关

这时更值得先回答:

当前异常实例和正常实例,除了代码版本,还有什么运行环境差异?

六、第五步:如果是“行为不一致”,别漏掉本地状态和特性开关

“行为不一致”这一类,很多时候比“实例慢”更像配置和状态问题。

高发方向包括

1. Feature flag / 配置开关不一致

表现:

  • 某些实例走新逻辑,某些实例走旧逻辑
  • 只有灰度实例返回结构、校验逻辑或降级路径不同

2. 本地缓存和本地内存状态不同

表现:

  • 老实例缓存里是热数据
  • 新实例还是冷缓存
  • 某些本地状态在启动后按异步任务刷新,刷新窗口内行为不同

3. 协议或序列化兼容性问题

表现:

  • 新旧版本共存时,请求或缓存内容在不同实例上的解释不一致
  • 某些实例反序列化、字段兼容、枚举处理不同

4. 本地任务或延迟加载顺序不同

表现:

  • 刚启动时某些开关、本地字典、规则还没加载齐
  • 行为像“偶发不一致”,其实是初始化时序不一致

所以只要问题更像“行为不一致”,就不要只盯 RT 和池子指标,也要把开关、本地缓存和初始化状态一起拉出来看。

七、第六步:如果只有部分实例慢,别忘了共享资源也可能只先放大到局部实例

很多性能问题虽然最终体现在局部实例上,但根因其实在共享资源层。

常见场景

  • 新版本某条 SQL 更重,先把这批实例的连接池打紧
  • 灰度实例集中命中某些热 key,Redis / DB 等待先在局部出现
  • 某组实例承接更高写流量,锁等待和长事务先放大
  • 下游服务对某些 header、某些路由分组更敏感,导致灰度实例先慢

这类问题最容易误判成什么

  • 某台机器有问题
  • 新版本偶发不稳定
  • 线程池参数不合适

但真实判断更应该是:

局部实例变慢,是因为它们先碰到了共享瓶颈,不代表瓶颈只属于那台实例。

所以一旦局部实例慢的同时,数据库、缓存、下游等待也在抬头,就要把共享资源链一起串起来看。

八、我更常用的收敛顺序:先对比,再判断是流量、环境、冷启动还是状态漂移

如果线上已经出现“灰度发布后只有部分实例慢或行为不一致”,我更建议按下面顺序走。

第 1 步:立对比矩阵

  • 哪些实例异常
  • 它们的版本、节点、启动时间、探针状态是什么
  • 接到的流量和租户是否一致

第 2 步:先区分是性能差异、行为差异还是状态差异

  • 逻辑不同
  • 健康状态漂移

第 3 步:核对流量样本是否一致

  • URI
  • 租户
  • header
  • 功能入口
  • 灰度规则

第 4 步:核对冷启动与 readiness

  • 刚启动时是否异常更明显
  • 缓存、连接池、JIT 是否未热
  • readiness 是否过早放流量

第 5 步:核对环境和配置

  • 环境变量
  • profile
  • 配置中心来源
  • JVM / JDK / 容器资源
  • 节点差异

第 6 步:最后把共享资源链拉进来

  • 数据库
  • 连接池
  • Redis
  • 下游服务
  • 线程池和任务队列

这条顺序的核心是:

不要把“只有部分实例有问题”直接理解成“只能看实例本身”,先看它们到底在哪个维度上不同。

九、最常见的几个误判

误判 1:只有灰度实例慢,说明新代码一定有问题

不一定。

先确认灰度实例接到的是不是同一种流量,以及它们是否还处于冷启动窗口。

误判 2:只有几台实例有问题,说明就是机器偶发问题

也不一定。

环境差异、配置漂移、节点差异和局部共享资源放大,都可能表现成“只有几台有问题”。

误判 3:平均指标没问题,就说明问题不大

灰度问题常常首先体现在:

  • P95 / P99
  • 局部实例
  • 某类请求
  • 某组租户

平均值很容易把早期信号盖掉。

误判 4:readiness 绿了,说明新实例已经完全稳定

不成立。

如果 readiness 只代表端口可用,而不代表真正完成预热,新实例照样可能在接流量后明显慢于旧实例。

误判 5:行为不一致就是代码逻辑不一致

也可能是:

  • feature flag 不一致
  • 本地缓存和本地状态不同
  • 配置源不同
  • 协议兼容和初始化顺序不同

十、FAQ:这类灰度现场最常被问到的几个问题

1. 灰度后只有部分实例慢,第一步最该看什么?

先看对比矩阵:

  • 慢的是哪些实例
  • 它们接的是什么流量
  • 启动了多久
  • 跑在哪些节点

2. 为什么只有新实例慢,过一会儿又恢复?

高概率是冷启动、缓存未热、连接池未热或 readiness 过早放流量。

3. 为什么同版本实例行为还会不一致?

常见原因是配置漂移、本地缓存状态不同、feature flag 不一致,或者某些实例实际上读取了不同来源的配置。

4. 什么时候应该优先查环境差异,而不是代码?

当你发现:

  • 同版本里只有某一批实例异常
  • 问题和节点、机房、实例标签高度相关
  • 行为像另一套配置或另一套运行参数

就该优先查环境。

十一、最后总结:灰度后只有部分实例慢或不一致,先找清楚差异到底落在哪个维度

这类问题最容易把人带偏的地方,是太快把根因压成一句”新版本有问题”。可灰度现场里,真正先拉开差异的往往不是版本号本身,而是流量样本、启动时机、环境条件、本地状态或者共享资源。

更实用的做法,是把异常实例和正常实例按版本、节点、流量、启动时长、配置来源几个维度摆平,再决定下一步该追哪条线。这样一来,“为什么只有部分实例慢或行为不一致” 就不会一直停留在模糊的灰度异常里。

差异压到这里以后,后面通常只剩三条线

把版本、节点、流量、启动时长和配置来源这些维度摆平后,下一步通常不用再泛讲“灰度排查方法”,而是直接选和证据最贴近的那条线。

如果你怀疑差异主要来自配置来源、实例状态或运行态观测点没对齐,就先看:

如果差异已经带到最近变更、接口链路或共享资源,再继续看:

真到现场里,我一般就按这个顺序收:怀疑配置和实例来源不一致,就先接配置生效链;实例状态已经表现成 readiness 波动、摘流量异常或下线时序混乱,就看健康检查和就绪探针;局部实例已经慢到连接池、线程池和慢请求一起抬头,再把运行态观测点、连接池和线程池那几篇连起来看。