发布后只有一小批实例异常时,我更相信实例对照,而不是整体平均值
只有一小批实例异常时,整体大盘经常会把问题盖住。把异常实例和正常实例拉成一组对照,再看 Actuator、配置来源、环境变量和启动现实,通常比继续争“是不是代码回归”快得多。
发布后只坏一小批实例,是我最不愿意只看平均值的一类现场。
因为平均值太容易骗人:整体成功率还行、整体 RT 只高了一点,甚至监控大盘还没红,但用户投诉已经开始集中落在那几台 Pod 上。这个时候如果大家还在讨论“是不是代码回归”“是不是机器偶发”,通常都还没真正碰到问题本体。
这类场景里,我更愿意先做一件很具体的事:拿一台坏实例,配一台好实例,做逐项对照。
只要对照做起来,很多原本抽象的争论会变得非常具体:
- 是不是同一个版本
- 最终配置值来自不是同一个 source
- 绑定到对象上的值有没有不一样
- 启动参数、环境变量、挂载文件是不是一样
- 真正先抬头的是线程池、连接池,还是某个下游组件
为什么我不太相信“只有几台异常,先等等看”
因为“只有几台异常”本身就已经是一条强线索。
如果同一个发布批次里:
- 老实例正常
- 新实例里只有部分异常
- 异常实例的行为稳定复现
那我第一反应不是“可能是巧合”,而是这几台实例在运行现实上大概率和别人不一样。这个不一样,可能在配置、环境、节点、资源,也可能在接流量时间线。
我通常怎么选对照样本
对照样本别乱拿,最好满足两个条件:
- 时间上接近:都属于同一轮发布、同一批灰度
- 业务上可比:承接的是同类流量,不是一台是热租户、一台是冷租户
确定样本后,我会直接记下:
bad-pod: order-api-7c9f9d77b9-kj2tp
good-pod: order-api-7c9f9d77b9-pv6hn
window: 14:20 - 14:40
symptom: bad-pod P99=2.1s, good-pod P99=180ms
这一步看起来简单,但能避免后面把“样本差异”误当成“实例差异”。
我先看的不是 value,而是这几个端点回答的问题
/actuator/info:它们真的是同一版吗
第一步很土,但必须做。
curl -s http://bad-pod:8080/actuator/info
curl -s http://good-pod:8080/actuator/info
我要确认的不是只有 git.commit.id,还包括:
- build time
- image tag
- app version
- 任何会暴露发布批次信息的字段
线上最怕的不是问题复杂,而是你以为在比同一版,实际上根本不是。
/actuator/health:到底是哪个组件先分叉
我会特别看 health 里有没有这种差异:
- 一个实例的 db / redis / discovery / custom indicator 明显更慢或 intermittent fail
- readiness 依赖的某个子组件在坏实例上频繁超时
- 好实例和坏实例的 overall
UP虽然一样,但细项已经不一样
很多时候,实例异常不是整个应用都坏了,而是某一个被 health 纳入判定的组件先分叉。
/actuator/env:重点不是值长什么样,而是值从哪来
这是很多人会看,但经常没看对的端点。
我更关心的是 source:
- 是来自 config server / nacos / apollo
- 还是来自环境变量
- 还是来自挂载文件
- 还是被 command line args 覆盖了
像下面这种差异就非常关键:
bad-pod: spring.datasource.hikari.maximum-pool-size = 20 (source: systemEnvironment)
good-pod: spring.datasource.hikari.maximum-pool-size = 60 (source: configserver:prod.yml)
两边看到的 value 都“像是合理值”,但 source 一不一样,问题性质完全不同。
/actuator/configprops:新值真的绑到对象上了吗
/env 看到新值,不等于业务拿到的就是新值。
这时我会继续看 configprops:
- 线程池、连接池、HTTP client、缓存相关的配置对象有没有一致
- 坏实例上是不是还停留在旧绑定
- 同一个 prefix 下有没有字段部分新、部分旧
如果 env 一样、configprops 不一样,方向就已经很明确:别再讨论外部配置中心,先看绑定和刷新链。
/actuator/metrics:让“感觉某台更慢”变成可对照的指标
我通常不会只看 HTTP RT,而会把下面几类一起对照:
http.server.requeststomcat.threads.busy或对应容器线程指标hikaricp.connections.active/pendingjvm.memory.usedprocess.cpu.usage- 自定义线程池 queue / active
因为很多“部分实例异常”,真正先露头的不是接口 RT,而是某个共享资源在那台实例上先顶到了边。
Actuator 看完以后,我一定会补环境现实
只看 Actuator 还不够,因为很多实例差异根本不在 Spring 配置层。
我通常会继续补这些对照:
# 环境变量
kubectl exec bad-pod -- printenv | sort > /tmp/bad.env
kubectl exec good-pod -- printenv | sort > /tmp/good.env
diff -u /tmp/good.env /tmp/bad.env
# 启动命令 / JVM 参数
kubectl exec bad-pod -- sh -c 'cat /proc/1/cmdline | tr "\0" " "'
kubectl exec good-pod -- sh -c 'cat /proc/1/cmdline | tr "\0" " "'
# 挂载文件
kubectl exec bad-pod -- ls -R /app/config
kubectl exec good-pod -- ls -R /app/config
这一步最常抓出来的差异包括:
SPRING_PROFILES_ACTIVE不同JAVA_TOOL_OPTIONS不同- 某个挂载文件没挂上
- 节点标签不同,导致网络或资源现实不同
- 某台 Pod 的 limit/request 和别人不一样
我还会把“启动多久后开始异常”放回时间线
有些实例一启动就不对,有些是跑十几分钟后才开始分叉。这个差别很大。
刚启动、刚接流量就更差
这更像:
- 冷启动没热起来
- 配置或环境一开始就不一样
- readiness 放流太早
- 本地缓存、JIT、连接池还没稳定
跑一段时间后才越来越差
这更像:
- 某个定时任务只在部分实例触发
- 配置刷新后只有部分实例切到新状态
- 某些实例承接了更差的流量样本
- 连接泄漏、线程池堆积、资源竞争逐渐拉开
所以我不会只看“它现在坏不坏”,还会看“它从什么时候开始和别人不一样”。
这类问题里,我最常见到的几个误判
误判一:同一个镜像就不该有实例差异
错。镜像一样,运行现实完全可能不一样。
误判二:/env 看起来一样,就说明配置没问题
也不对。source 可能不同,configprops 可能不同,底层资源对象也可能没切过去。
误判三:只有几台异常,问题不严重
很多系统性问题一开始就是先从几台机器露头,等整体平均值坏掉再追,往往已经晚了。
最后一句
发布后只有一小批实例异常时,最值钱的不是继续看整站平均值,也不是先吵“是不是代码回归”。更有效的动作是尽快把坏实例和好实例拉成一组对照,把版本、source、绑定对象、环境现实和时间线一项项对齐。
一旦对齐,你通常不会再面对一个抽象问题,而是在面对一条很具体的差异链。那时候排查才真正开始收敛。