Java

稳定性治理先做容量评估、慢链路梳理还是告警分层?

这三件事都重要,但真正的先后顺序往往不是在会里排出来的,而是在一次事故里被逼出来的:先找到那条真实拖垮现场的慢链,告警才知道该按哪一层重做,容量边界也才算得出来。

  • 稳定性治理
  • 容量评估
  • 告警分层
  • 故障链
  • Java
15 分钟阅读

那次把顺序逼出来的事故,发生在一个周三晚上。

19:07,结算页 confirmOrder 的 p95 从 180ms 抬到 1.4s。

19:09,客服开始收到“优惠明细转很久,订单提交不出去”的反馈。

19:11,网关 504 冒头。

19:13,值班群已经吵成三条线:

  • 平台同学说先看容量,活动流量比平时高了三成多。
  • 值班负责人说先把告警收成 P1、P2,不然满屏都是红点,谁也看不清。
  • 业务研发说别先谈治理动作,先把最慢的那一段找出来。

最后前两种声音先占了上风。

原因也不难理解。

团队在事故前两周刚做过一轮稳定性治理启动:

  • 容量评估已经出了压测报告。
  • 告警也刚按 P1、P2、P3 重新分过一遍。
  • 只有慢链梳理还停在“知道依赖关系”,没有拉出一条真正的变慢传导线。

所以现场一出事,大家的直觉非常自然:要么先扩一把,要么先把告警墙收干净。

可真正把顺序打出来的,偏偏是这两件事都没把现场救下来。

先出现的症状,其实已经在暗示“不是平均容量不够”

那天最早坏掉的,不是整个站点,而是结算链里“查最优优惠”这一段。

按分钟回放,前 10 分钟的画面很清楚:

  • 19:07,confirmOrder p95 从 180ms 升到 1.4s,购物车、商品详情还正常。
  • 19:08,order-service 自身 CPU 只有 54%,GC 没异常,但调用 promotion-service 的 client RT 开始抬头。
  • 19:10,promotion-service 的重试倍率从 1.02 涨到 1.34。
  • 19:11,网关 504 出现,超时率从 0.2% 涨到 3.8%。
  • 19:12,coupon-rule-service 的连接池 pending 开始连续堆高。
  • 19:14,订单线程池 queue 也跟着上来,结算页全面变慢。

这组现象后来回头看,已经很像一条慢链在往外传。

但在当时,它先被解释成了另一件事:活动流量把系统顶到了容量边缘。

因为从表面上看,这个判断很合理:

  • 活动流量确实比平日晚高峰高 37%。
  • 事故前的压测报告里,结算链路的安全区间本来就离这次峰值不远。
  • 历史上也出现过“流量一上来,扩 Pod 就恢复”的情况。

于是第一反应先落在了容量上。

团队第一反应为什么会先错做容量

19:14 到 19:19,现场连续做了三件典型的“容量先手”:

  • order-service 从 16 个 Pod 扩到 24 个。
  • promotion-service 从 12 个 Pod 扩到 18 个。
  • coupon-rule-service 背后的 MySQL 连接上限从 600 提到 900。

这套动作的逻辑很直白:

  • 先把入口和中间层摊薄。
  • 再把数据库连接打宽。
  • 如果真是流量顶满,扩完以后单机压力和等待时间应该一起回落。

结果 5 分钟后,最该下来的东西没有下来。

扩容之后确实有两项指标变“好看”了:

  • order-service 单 Pod CPU 从 54% 降到 31%。
  • promotion-service 单 Pod QPS 被摊薄了。

但真正代表现场有没有被救回来的指标,几乎没动:

  • confirmOrder p99 还在 3s 左右。
  • 网关 504 还在持续增加。
  • 新拉起来的 Pod 在 90 秒内也迅速变慢。
  • coupon-rule-service 连接池 pending 不降反升。

更扎心的是,数据库 QPS 还因为扩容后回源请求更多,短时间又抬了一截。

这就是第一条反证。

如果问题真是平均容量不够,扩容应该先让单次请求等待变短。可这次只是把 CPU 摊薄了,没有把每个慢请求变快,反而把同一条回源链上的压力继续送进下游。

也就是说,团队先做的不是完全错误的动作,但它只碰到了表层负载,没有碰到那条真正把请求拖慢的链。

容量没救场后,团队又为什么会先错做告警分层

扩完没起色,值班群马上滑向第二种直觉:先把告警墙收一收。

因为那时屏上已经同时在响:

  • 网关 504
  • 结算超时率
  • 订单线程池 queue
  • promotion-service client timeout
  • coupon-rule-service pending
  • cache miss 升高
  • MySQL RT 抬头
  • 重试倍率上升

告警太多时,人的本能就是先缩成几个层级,不然谁也不敢说第一轮该盯哪块。

于是 19:20 左右,值班负责人把主屏临时切成两组:

  • P1:网关 504、结算超时率、核心 SLA
  • P2:线程池 queue、连接池 pending、cache miss、重试倍率

这套分法在“安静一点”这件事上是有效的。

屏变干净了,群里的讨论也统一到“先看 P1”。

可 6 分钟过去,现场还是没有变快。

原因也很直接。

P1 上那些最红的信号,几乎全是结果层:

  • 它们能证明用户已经受影响。
  • 能证明事情在扩大。
  • 却不能告诉你最早分叉的是哪一段。

更糟的是,最早开始移动的几项信号反而被放进了 P2:

  • coupon-template-cache 命中率从 97% 掉到 63%
  • promotion-service -> coupon-rule-service 的 client RT 先抬头
  • coupon-rule-service 连接池 pending 比 504 早了将近 3 分钟

这就形成了第二条反证。

如果先把告警分层就足够救场,那么主屏变干净之后,诊断应该更快收敛。可这次主屏只是更安静了,判断却没有更靠近起点,大家只是更整齐地盯着结果层发红。

所以这一步也没把现场救回来。

它只是证明了一件事:

告警再怎么分层,如果没有先知道“系统是沿哪条链在变慢”,分出来的仍然可能只是更好看的结果层和更安静的噪音区。

真正把团队逼回慢链的,是一组没法再绕开的证据

19:26,现场终于出现了那个把争论压住的证据。

一位同学临时抽了 40 条慢请求的 traceId,按同一时间窗对了一遍,结果非常集中:

  • 40 条里有 33 条,时间都耗在 order-service -> promotion-service -> coupon-rule-service 这一段。
  • 这些请求在 order-service 本地执行业务逻辑只花了几十毫秒,并没有本地 CPU 忙或 GC 卡顿。
  • 同一批请求到达 coupon-rule-service 之后,数据库取券模板的 SQL p95 从 18ms 涨到 420ms。
  • coupon-template-cache 命中率下滑,正好发生在 19:06,比网关 504 提前了 5 分钟。

这组证据有两个关键点。

第一,它把“慢”压到了同一段,而不是整个系统平均变慢。

第二,它把先后顺序钉住了:先是券模板缓存失去吸收,接着是规则服务回源变慢,再往外才是线程等待、重试放大、网关超时。

到这一步,团队才没法继续在“是不是先扩”“是不是先收告警”上打转,而是老老实实把那条慢链摊开。

那条链后来被还原成这样:

  1. 活动开始后,一批 coupon-template key 因为相同 TTL 集中过期。
  2. promotion-service 查询最优优惠时,大量回源到 coupon-rule-service
  3. coupon-rule-service 读模板表,连接池 pending 持续升高。
  4. order-service 同步等待优惠结果,业务线程被占住。
  5. 同步重试把真实调用量从 1 倍放大到 1.6 倍。
  6. 最外层网关超时和 504 才开始集中暴露。

顺序就是在这时被事故逼出来的。

不是因为有人在会上讲明白了,而是因为前两种先手都没救场,只有这条链能同时解释:

  • 为什么只有结算链先坏,别的读请求还没一起坏。
  • 为什么扩了 Pod,慢请求还是慢。
  • 为什么 504 最刺眼,却不是最早动的那一层。

慢链梳理之后,现场动作才第一次真正有效

链路一拉清,后面的动作一下子不再散。

19:29 开始,现场只做了三件和这条慢链直接相关的事:

  • 关掉 confirmOrder 这条链上的同步重试。
  • 暂停券模板回填任务,避免和回源查询抢同一张表。
  • 让最优优惠计算先走旧模板兜底,不再强制等待最新模板回源。

这三步下去之后,指标的回落顺序非常有说服力:

  • 19:31,重试倍率先从 1.6 回到 1.1。
  • 19:32,订单线程池 queue 开始下降。
  • 19:33,coupon-rule-service 连接池 pending 明显回落。
  • 19:35,结算链 p99 从 3s 降回 800ms 左右。
  • 19:37,网关 504 才开始成片下降。

这个回落顺序又反过来验证了那条慢链没有看错。

如果起点真在网关、入口容量或者告警系统本身,这组指标不会按这个顺序往回掉。

它先掉的是放大器,再掉的是中间等待,最后才掉外层结果。

这比任何会议上的治理排序都更硬。

也是从那次之后,告警分层才真正重做成功

事故前,团队做过一次告警分层,但更多是按严重级别和责任团队分。

事故后,分法彻底换了。

不再先问 P1、P2,而是先问它在慢链里站哪一层。

后来结算链的主看板只保留三层信号。

第一层:起点层

专门看“第一张倒下的牌”有没有动:

  • coupon-template-cache 命中率
  • 模板回源耗时
  • coupon-rule-service 连接池 pending

这一层一旦连续异常,值班第一步不是拉群里所有人,而是先抽样 traceId,确认是不是这条老慢链又在起。

第二层:放大层

专门看系统有没有开始自我放大:

  • promotion-service client timeout
  • confirmOrder 重试倍率
  • order-service 业务线程 queue

这一层抬起来,第一动作不是继续猜根因,而是先关重试、停回填、压低非核心计算,把放大器先拆掉。

第三层:结果层

最后才是用户和业务最先感知的结果:

  • 结算超时率
  • 网关 504
  • 核心转化损失

这一层负责判断影响面和升级级别,不再承担“谁是起点”的职责。

也就是说,告警分层真正落地,不是因为颜色分得更细,而是因为慢链先被看清以后,每一层终于知道自己该服务什么动作。

容量评估也是在慢链梳理之后,才第一次算出了边界

事故前那份容量评估,主要看的还是平均 CPU、峰值 QPS、数据库利用率。

这些数不能说没用,但它们解释不了这次事故里最关键的事:

  • 为什么缓存一失去吸收,系统很快就不是“慢一点”,而是整条链一起塌。
  • 为什么重试倍率到 1.3 以后,扩 Pod 反而可能把回源压力继续往下送。
  • 为什么连接数还有余量,用户请求还是已经超时。

事故后,团队重算容量时,不再先看全局平均值,而是只盯这条关键慢链的预算:

  • coupon-template-cache 命中率不能长期掉到 92% 以下。
  • coupon-rule-service 连接池 pending 超过 150ms,就说明这条链已经接近失稳。
  • confirmOrder 同步重试倍率超过 1.25,就不能再继续放量。
  • 最优优惠计算必须有旧模板兜底,不允许把“等最新模板”当成默认路径。

后来重新压测时,团队第一次算出一个真正有意义的容量边界:

不是“结算服务 24 个 Pod 能扛多少 QPS”,而是“在模板缓存命中率、回源并发、重试倍率都受控的前提下,这条优惠慢链最多能承受多大流量;一旦哪个条件被打穿,应该先触发什么保护动作”。

这时容量评估才不再是离现场很远的一张报表,而是和真实故障链绑在一起的边界说明。

所以那次事故最后留下来的,不是一句治理口号,而是一条被验证过的顺序

后来再开稳定性治理会,大家不太再争“容量、慢链、告警,谁更基础”。

因为那次事故已经把顺序压得很清楚了。

如果连系统会沿哪条链变慢都说不清,先做容量,容易只算出平均余量;先做告警分层,也很容易只把结果层排得更漂亮。

只有先把那条会把现场拖垮的慢链找出来,后两件事才知道自己到底该落在哪里。

所以对那支团队来说,顺序不是在白板上排出来的,而是被事故硬生生逼出来的:

  • 先把最危险的慢链梳清。
  • 再把告警按这条链重做,让值班知道先看哪一层。
  • 最后再按这条链去算容量、限流、重试和兜底边界。

这不是因为慢链这件事更高级。

而是因为没有它,后面两件正确的事都会先落空。

这篇的边界也得卡住

如果你的系统已经把关键慢链看得很清楚,告警也已经按链路摆好了,只差线程池、连接池、限流和超时预算,那就别被这篇拦住,直接做容量设计更合适。

如果你还在现场里,连最早分叉的是哪一层都没抓住,也别急着谈治理顺序,先回到具体故障链上。

这篇只想说明一件事:

稳定性治理里的先后顺序,很多时候不是靠抽象判断排出来的,而是靠一次事故里“哪种先手救不了场、哪条证据链能解释现场、哪组动作能被回落顺序验证”硬压出来的。