社会热点
生产事故现场作战白皮书:百条速查命令 + 三大血泪案例 + 架构级防故障指南
2026-07-23 22:59
生产事故现场作战白皮书:百条速查命令 + 三大血泪案例 + 架构级防故障指南

《生产事故现场作战白皮书:百条速查命令 + 三大血泪案例 + 架构级防故障指南》

真正把系统拖进深夜会议室的,通常不是“某个组件挂了”,而是大家在前 10 分钟里判断错了方向。本文不讲空泛口号,只讲生产现场怎么判断、命令为什么这样打、哪些修复只是止血、哪些改造才能避免同一类事故反复出现。


一、先回到事故现场:P0 不是从报错开始,而是从误判开始

这篇文章讨论的不是单个中间件,也不是某一类报错,而是高并发 Java 微服务在生产环境中的一整套应急动作。目标读者也不是刚学命令的人,而是已经要值班、要扛线上稳定性指标、要在群里给出判断的人。

原始场景来自一个典型电商订单链路:

用户端 -> API 网关 -> 订单服务 -> 库存服务 -> 支付服务

依赖组件包括:

  • • MySQL 8.0 主从
  • • Redis 7 集群
  • • RocketMQ 4.9
  • • Nacos 2.2
  • • Kubernetes 1.28

业务约束也很真实:

  • • 平时日均订单量约 500 万
  • • 大促峰值 QPS 12 万
  • • 核心表 orders 已达 3 亿行
  • • 订单链路包含同步写库和异步状态流转

事故发生在大促开始后的 10 分钟内:

时间
现场现象
当时最可能的误判
20:00
下单页开始转圈
以为只是网关流量高
20:02
订单服务 P99 从 120ms 升到 12s,网关 502 比例达到 45%
以为只是应用实例不够
20:05
MySQL 主库 CPU 100%,连接数打满,日志出现 Too many connections
容易直接把根因归到数据库
20:08
RocketMQ 消费 Lag 超过 800 万
容易把消息积压当成独立故障
20:10
用户无法下单,业务损失按分钟累计
所有人都在催恢复,最容易仓促改配置

真正危险的点在这里:数据库打满、消息积压、应用超时,往往不是三个问题,而是一条故障链上的三个结果。

如果现场直接做这几件事,通常会把事故拖得更久:

  • • 看到 CPU 高就先扩容全部应用
  • • 看到 MQ 积压就先加消费者线程
  • • 看到 502 就先重启 Pod
  • • 看到慢查询就先 Kill 掉一批 SQL

这些动作不一定错,但如果没有先判断“瓶颈是在 CPU、锁、连接池、网络、下游超时还是线程池”,就很可能把一个局部故障扩大成系统性抖动。

生产现场最先要回答的 4 个问题

在任何命令之前,先把判断框架立住:

  1. 1. 系统是在变慢,还是已经部分不可用
  2. 2. 瓶颈在调用链哪一层扩散
  3. 3. 当前是资源耗尽,还是等待放大
  4. 4. 应急动作会不会把压力转移到下游

这四个问题的价值,比多记几十条命令更大。因为命令只是取证手段,不是结论本身。


二、第一响应不是“查全部”,而是按故障链收缩范围

多数线上事故都可以先归到下面四类之一:

故障类型
典型信号
先看什么
资源耗尽
CPU、内存、连接数、线程数打满
top
vmstat、连接池指标
等待放大
锁等待、线程阻塞、下游超时
jstack
processlist、调用超时分布
流量失控
突刺、缓存同时失效、重试风暴
网关 QPS、Redis TTL 分布、失败重试量
变更引入
发布后抖动、某版本独有报错
发布记录、灰度比例、变更 diff

现场动作也应该分成三层,而不是“发现异常就开始改”:

1. 先止血

目标是让系统别继续失血,而不是立刻找出全部真相。

常见动作:

  • • 对入口限流,压住写流量
  • • 关闭非核心异步链路
  • • 暂停会放大压力的重试任务
  • • 对缓存回源增加舱壁和兜底

2. 再定位

这一阶段只回答“当前主瓶颈是什么”,不急着一口气解释所有异常。

例如这次事故里,最关键的事实链其实是:

MySQL metadata lock / 慢 SQL -> 连接池等待 -> 订单线程池阻塞 -> 网关超时 -> MQ 消费跟不上

如果这个链条没看清,后面所有扩容都只是在拖时间。

3. 最后修复

修复要分成两类:

  • • 临时修复:为恢复服务而做的止血动作
  • • 长期修复:为避免同类事故再次发生的改造

把这两类混在一起,是复盘文章最常见的问题。生产现场能救回来,不代表架构已经健康。


三、百条速查命令:不是背命令,而是知道每条命令在回答什么

下面这 100 条命令按现场排查顺序组织,而不是按工具分类堆砌。每条命令都只解决一个判断问题。

A. 机器与内核:先确认是不是宿主机已经扛不住

#
命令
它回答什么
1
uptime
负载是否在短时间内陡增
2
w
机器上是否有异常交互操作
3
top
CPU、内存、负载的总体形态
4
top -Hp <pid>
具体是哪些业务线程在吃 CPU
5
htop
适合快速查看多核分布,需要环境已安装
6
vmstat 1
是 CPU 忙、IO 等待还是上下文切换异常
7
mpstat -P ALL 1
是否单核打满或出现核间不均衡
8
pidstat 1
哪些进程在异常抢占 CPU 或 IO
9
free -h
可用内存是否已经见底
10
cat /proc/meminfo
是否存在页缓存挤压、脏页堆积
11
sar -r 1 5
内存回收和缓存变化趋势
12
sar -u 1 5
CPU 用户态、系统态、iowait 分布
13
iostat -x 1
磁盘是否成为根瓶颈
14
iotop
是谁在持续打磁盘
15
df -h
磁盘空间是否耗尽
16
df -i
inode 是否耗尽
17
`dmesg -T
tail -100`
18
journalctl -xe --since '10 min ago'
系统级错误的最近上下文
19
cat /proc/loadavg
当前负载是否还在继续爬升
20
uname -a
事故现场确认内核版本和环境差异

B. 进程与文件句柄:确认是不是应用自身已经进入资源枯竭

#
命令
它回答什么
21
`ps -ef
grep java`
22
`ps -Lp
wc -l`
23
prlimit --pid <pid>
进程级资源上限是多少
24
`lsof -p
wc -l`
25
`lsof -p
grep TCP
26
lsof -i :8080
谁占用了服务端口
27
cat /proc/<pid>/limitsnofile
 等限制值是否过低
28
cat /proc/<pid>/status
线程数、内存、上下文切换摘要
29
`pmap -x
tail -20`
30
`watch -n 1 "ls /proc//fd
wc -l"`

C. 网络:确认超时是来自链路问题还是应用等待

#
命令
它回答什么
31
ss -s
当前 TCP 状态总体分布
32
ss -lntp
监听端口是否正常
33
`ss -antp
grep :3306
34
`ss -antp
grep TIME-WAIT
35
`ss -antp
grep SYN-SENT`
36
`netstat -anp
grep
37
ip addr
网卡和 IP 是否正常
38
ip route
路由是否异常
39
ping <host>
连通性是否直接失败
40
traceroute <host>
路由路径是否异常变长
41
mtr -rwzbc 20 <host>
是否存在持续抖动和丢包
42
`nstat -az
grep TcpRetransSegs`
43
sar -n DEV 1 5
网卡吞吐是否打满
44
sar -n TCP,ETCP 1 5
TCP 建连、重置、重传异常情况
45
tcpdump -i any port 3306 -c 50
必须抓包时确认报文层异常

D. JVM:判断是 GC、死锁、锁竞争还是下游等待

#
命令
它回答什么
46
jps -lvm
JVM 进程、启动参数和主类
47
jstat -gcutil <pid> 1000 10
GC 是否频繁打断业务
48
jstat -gccapacity <pid> 1000 5
堆区容量配置是否异常
49
jcmd <pid> VM.flags
线上 JVM 参数到底是什么
50
jcmd <pid> GC.heap_info
当前堆使用情况
51
jcmd <pid> Thread.print
线程阻塞、锁等待和调用栈
52
`jstack
grep -n 'BLOCKED'`
53
`jstack
grep -n 'waiting for monitor entry'`
54
jcmd <pid> GC.class_histogram
哪类对象在疯狂占内存
55
`jmap -histo:live
head -50`
56
jmap -dump:live,format=b,file=heap.hprof <pid>
需要离线分析堆时使用,注意会暂停
57
jcmd <pid> VM.native_memory summary
是否本地内存膨胀而非 Java 堆
58
`jcmd PerfCounter.print
head -50`
59
async-profiler -d 30 -e cpu <pid>
CPU 热点到底在业务代码还是框架
60
jfr start name=incident settings=profile duration=60s filename=incident.jfr
需要短时录制现场时使用

E. 应用日志与配置:确认是不是变更、重试或错误分支放大了压力

#
命令
它回答什么
61
tail -200 app.log
最近错误是什么类型
62
tail -f app.log
错误是否仍在持续发生
63
`grep -i 'Exception' app.log
tail -50`
64
`grep -i 'timeout' app.log
tail -50`
65
`grep -i 'Too many connections' app.log
tail -20`
66
`grep -i 'RejectedExecution' app.log
tail -20`
67
`grep -i 'Sentinel' app.log
tail -20`
68
`grep -i 'oom' app.log
tail -20`
69
kubectl logs <pod> --previous --tail=200
容器重启前到底发生了什么
70
kubectl get cm -n <ns>
最近是否改过配置中心内容

F. MySQL:把“数据库慢”拆成连接、锁、执行计划三件事

#
命令
它回答什么
71
SHOW FULL PROCESSLIST;
当前是谁在占连接、在等什么
72
SHOW STATUS LIKE 'Threads_connected';
已连接数是否逼近上限
73
SHOW VARIABLES LIKE 'max_connections';
上限本身是否过低
74
SHOW ENGINE INNODB STATUS\G
死锁、锁等待、事务摘要
75
SELECT * FROM sys.innodb_lock_waits;
谁锁住了谁
76
SELECT * FROM information_schema.innodb_trx\G
长事务是否拖住了全局
77
SHOW VARIABLES LIKE 'slow_query_log';
慢日志是否开启
78
SHOW VARIABLES LIKE 'long_query_time';
慢 SQL 门槛是多少
79
EXPLAIN SELECT ...;
有没有走错索引
80
EXPLAIN ANALYZE SELECT ...;
实际执行是否与预期不符
81
SHOW INDEX FROM <table>;
索引设计是否支持当前查询
82
SHOW OPEN TABLES WHERE In_use > 0;
是否存在表级占用
83
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
行锁等待是否异常
84
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
临时表是否过多
85
SHOW GLOBAL STATUS LIKE 'Handler_read%';
是否存在大量全表扫描迹象

G. Redis:不要只看“可不可用”,还要看是不是被热 Key 和过期风暴打穿

#
命令
它回答什么
86
redis-cli INFO server
基本角色和运行状态
87
redis-cli INFO memory
内存是否逼近上限
88
redis-cli INFO stats
命中率、拒绝连接、淘汰情况
89
redis-cli INFO clients
客户端连接是否异常
90
redis-cli INFO keyspace
各 DB 键数量和过期分布
91
redis-cli SLOWLOG GET 20
哪些命令拖慢了 Redis
92
redis-cli LATENCY LATEST
最近延迟事件是什么
93
redis-cli --latency -h <host>
网络还是服务端延迟
94
redis-cli CLUSTER INFO
集群状态是否健康
95
redis-cli TTL <key>
热点 Key 是否正好集中失效

H. RocketMQ:消息积压要区分“生产过快”和“消费被堵”

#
命令
它回答什么
96
mqadmin clusterList -n <namesrv>
Broker 和集群是否正常
97
mqadmin topicStatus -n <namesrv> -t <topic>
Topic 各队列写入情况
98
mqadmin consumerProgress -n <namesrv> -g <group>
当前积压量到底有多大
99
mqadmin consumerConnection -n <namesrv> -g <group>
消费者实例是否在线
100
mqadmin queryMsgByKey -n <namesrv> -t <topic> -k <key>
关键消息到底有没有投递和消费

这 100 条命令怎么用,才不会把自己查乱

现场不建议从第 1 条一直打到第 100 条。更实用的方式是按症状进入:

现场症状
建议起手命令
RT 突然升高,但 CPU 不高
jstack
SHOW FULL PROCESSLISTss -antp
CPU 100%
top -Hp
async-profilerEXPLAIN
大量 502/504
网关日志、应用线程栈、下游连接状态
消息积压
consumerProgress
、消费者线程栈、下游超时分布
大促整点雪崩
Redis TTL、回源 SQL、入口 QPS 与重试数

这里有两个边界要说清楚:

  • • jmap -dumptcpdumpEXPLAIN ANALYZE 这类命令很有价值,但都可能对现场造成额外负担,不适合在主库已经打满时随意执行。
  • • 单条命令永远只能给出局部证据。比如 Threads_connected 高,并不自动等于“数据库不够用”,也可能只是应用连接归还失败。

四、三大血泪案例:真正的根因,通常藏在“看起来最正常”的地方

下面三个案例保留了原文的核心问题,但把判断过程补完整,因为真正有价值的不是“最后答案”,而是怎么排除错误方向。

案例一:数据库连接池爆满,根因不在数据库,而在连接没有被归还

现场现象

  • • 订单服务大面积报 502
  • • 日志连续出现 Connection is not available
  • • 数据库连接数超过 1500
  • • 应用线程大量阻塞在获取连接

第一眼最容易下的结论

很多团队到这里会直接说“数据库不够用了”,然后开始加 max_connections、扩主库规格,甚至把连接池也一起调大。

这类处理有时能临时缓一下,但它绕开了一个关键问题:连接是被真正占用,还是借出去之后没有归还。

现场排查路径

  1. 1. SHOW FULL PROCESSLIST 发现大量连接处于 Sleep,并且 Time 很长。
  2. 2. 订单服务配置为 maximum-pool-size=200,共 5 个 Pod,理论上最多约 1000 连接。
  3. 3. 数据库侧却看到 1500+ 连接,说明存在池外连接,或者连接生命周期已经失控。
  4. 4. jstack 继续看,线程并没有都在执行 SQL,而是有相当一部分阻塞在下载接口相关逻辑。
  5. 5. 最后追到一个文件导出接口,代码使用了 jdbcTemplate.queryForStream,但消费完成后没有完整关闭底层 ResultSet 和 Connection

真正根因

根因不是“数据库扛不住”,而是连接泄漏导致连接池的借出和归还失衡。数据库只是最先被拖死的那一层。

为什么这种问题在大促更容易爆

  • • 平时流量低,泄漏速度慢,看起来像偶发抖动
  • • 大促时请求量陡增,泄漏变成持续累积
  • • 一旦连接池等待出现,业务线程池会被一起卡死
  • • 上游超时后触发重试,会进一步放大连接压力

临时止血

  • • 限制导出接口和其他非核心查询
  • • 杀掉明显失控的长连接
  • • 适度清理空闲连接
  • • 入口限流,避免新流量继续堆积到连接池

长期修复

  • • 禁止业务代码直接暴露 DataSource.getConnection()
  • • 流式查询必须配套 try-with-resources 或回调式关闭
  • • 暴露 HikariCP 的 ActiveConnectionsIdleConnectionsPendingThreads
  • • 连接池等待数持续非 0 即告警,而不是等数据库连接数打满再告警
  • • 将导出类接口与核心下单链路隔离到独立只读资源池

修复后的验证方式

  • • 压测时持续观察 PendingThreads
  • • 模拟下载接口中断,确认连接仍能回收
  • • 检查 Threads_connected 与理论池大小是否一致

案例二:缓存雪崩不是“Redis 挂了”,而是你把失效时间排成了队

现场现象

  • • 整点活动开始后,订单 RT 瞬间抬升
  • • Redis 仍然可连,但 MySQL CPU 迅速拉满
  • • 大量热点请求开始直接回源

第一眼最容易下的结论

大家很容易把问题说成“缓存击穿”或者“Redis 被打爆了”。这两个说法都不完全对。

如果 Redis 自身没有明显超时、没有拒绝连接、没有主从切换,那么更应该先想的是:是不是大量热点 Key 在同一时间段集中过期了。

现场排查路径

  1. 1. SLOWLOG GET 没看到 Redis 自身的慢命令堆积。
  2. 2. INFO stats 里命中率开始掉,但服务端并没有明显资源瓶颈。
  3. 3. 追活动缓存预热任务,发现一批核心 Key 都在 20:00 统一加载。
  4. 4. TTL 统一写成 7200 秒,于是 22:00 前后集中失效。
  5. 5. 请求回源后,由于数据库本来就承接同步写压力,读流量一叠加,主库立刻被拉满。

真正根因

根因不是 Redis 故障,而是缓存生命周期设计过于整齐,导致过期事件集中爆发

这类问题为什么危险

  • • Redis 监控看起来仍然健康,容易误导现场判断
  • • 应用看到的是数据库慢,而不是缓存策略错
  • • 一旦回源没有做并发保护,热点 Key 会形成局部风暴

临时止血

  • • 对热点接口快速增加入口限流
  • • 临时回填关键缓存,并把 TTL 打散
  • • 对热点回源逻辑加互斥锁或单飞控制
  • • 关闭非核心推荐、画像等附加查询

长期修复

  • • 物理 TTL 加随机抖动,例如 base + random
  • • 热点数据采用逻辑过期 + 异步刷新,而不是统一物理过期
  • • 增加本地缓存挡住极短时间内的热点重复回源
  • • 对空值和异常结果也做短 TTL 缓存,避免穿透
  • • 缓存重建要和数据库限流绑定,不能允许无限回源

修复后的验证方式

  • • 统计同一批预热 Key 的 TTL 分布,而不是只看平均值
  • • 演练 Redis 单分片抖动时,验证回源并发是否被限制
  • • 观察缓存 miss 突增时,数据库是否仍能稳定在安全水位

案例三:消息积压表面看是 MQ 问题,实质是消费者把下游超时原样放大了

现场现象

  • • 下单成功后 30 分钟仍未收到通知
  • • 库存扣减延后
  • • consumerProgress 显示积压达到千万级

第一眼最容易下的结论

常见反应是立刻把消费者线程数调大,或者多扩几个 Pod。这个动作不是不能做,但它默认假设“消费速度慢只是因为算力不够”。

如果真正的问题是下游调用平均 RT 从 50ms 变成 8s,那么单纯加线程只会更快把线程池、连接池、下游接口一起拖死。

现场排查路径

  1. 1. mqadmin consumerProgress 看到某个核心消费组落后严重。
  2. 2. mqadmin consumerConnection 确认消费者实例都在线,并不是实例丢失。
  3. 3. jstack 发现大量消费线程等待在调用支付网关的 HTTP 客户端上。
  4. 4. 支付网关配置超时 5 秒,但实际平均 RT 已经高于 8 秒。
  5. 5. 消费线程池默认只有 20,线程被长时间占住后,新的消息根本拿不到消费能力。

真正根因

根因不是 MQ,而是消费者把同步下游超时带进了异步链路,并且没有做隔离和快速失败

临时止血

  • • 将非核心消费者先停掉,保住核心状态流转
  • • 调低单次消费批量,避免长耗时消息长时间占用线程
  • • 对支付网关调用先熔断,失败消息进入重试或死信

长期修复

  • • 核心状态流转和通知类逻辑拆到不同 Consumer Group
  • • 消费线程池配置和下游超时预算一起设计,而不是各配各的
  • • 失败消息要有明确去向:重试、死信、人工回放,不能一直阻塞主消费线程
  • • 基于 Lag 做弹性扩容时,前提是下游依赖还有余量,否则只是更快放大故障

修复后的验证方式

  • • 注入下游 5 秒以上超时,观察消费者是否快速失败
  • • 验证死信回放不会重复扣减库存或重复发券
  • • 将积压恢复时间纳入压测目标,而不是只测正常吞吐

五、命令之外更重要的事:为什么这些故障会沿着调用链扩散

三类问题虽然长得不同,但扩散机制很像,都是同一条链:

入口流量抬升
某一层出现等待
线程 / 连接 / 队列被占满
上游超时与重试增加
压力回灌到共享依赖
局部故障变成全链路故障

这里最容易忽略的是“等待”。

工程上很多事故并不是资源一开始就不够,而是某个等待时间被拉长后,占住了稀缺资源:

  • • SQL 变慢,占住数据库连接
  • • 下游接口超时,占住业务线程
  • • 缓存回源慢,占住请求窗口
  • • 消费阻塞,占住消息线程池

一旦占住的时间足够长,系统就会从“吞吐下降”进入“排队放大”,最后表现成:

  • • 连接数打满
  • • 线程池打满
  • • 队列积压
  • • 重试风暴
  • • 全链路 RT 抬升

所以生产排障不能只问“谁最慢”,还要问:谁在持有共享资源,持有了多久。


六、从被动救火到主动防复发:架构级稳定性指南

如果只收藏命令,不改架构,下一次事故通常只会换个时间再来。真正有效的改造,应该围绕“阻止等待扩散”来设计。

1. 入口先做流量整形,不要让所有请求平等进入核心链路

很多系统在低并发时不需要复杂治理,但当下列条件出现时,入口整形就不是可选项了:

  • • 活动流量会在分钟级突刺
  • • 读写共用数据库或共用核心线程池
  • • 非核心功能与下单、支付等核心路径混跑

建议把入口流量分成三类:

流量类型
处理策略
核心写流量
优先保障,单独限额
核心读流量
允许降级但不允许无限回源
非核心流量
发生故障时优先牺牲

这里需要区分一件事:限流不是为了把 QPS 压低,而是为了给关键资源留出生存空间。

2. 共享依赖必须做舱壁,不要让所有压力落到一个资源池

以下几类资源最容易成为共享瓶颈:

  • • 数据库连接池
  • • Redis 连接池
  • • HTTP 客户端连接池
  • • 业务线程池
  • • MQ 消费线程池

更稳妥的做法是:

  • • 导出、报表、批处理使用独立线程池和独立数据源
  • • 核心消费和通知消费分组隔离
  • • 下游不稳定的调用使用独立连接池和超时策略

如果所有业务共享一套线程和连接,任何一个慢依赖都能拖住全局。

3. 缓存不只是“加一层 Redis”,而是要设计失效方式和回源上限

缓存方案成立的前提是:

  • • 热点键分布相对可预期
  • • 回源数据库仍有安全余量
  • • 应用在 miss 时能控制并发

当这些前提不成立时,单层 Redis 远远不够。更适合的组合通常是:

  • • 本地短 TTL 缓存,挡住瞬时热点
  • • Redis 做集中式共享缓存
  • • 热点键逻辑过期,异步刷新
  • • 回源路径带限流、互斥和兜底

4. 异步链路必须把“失败去哪里”设计清楚

异步不是天然更稳。很多团队把同步压力搬到 MQ 后,以为问题已经解决,实际上只是延后暴露。

设计时至少要回答:

  • • 下游超时后是重试、丢弃还是进入死信
  • • 重试是立即重试、指数退避还是人工回放
  • • 同一条消息被重复投递时,如何保证幂等
  • • 核心流程和附属流程是否隔离

如果这些问题没有先设计,MQ 积压迟早会变成新的 P0。

5. 监控要覆盖资源、依赖和业务结果,三者缺一不可

一个常见误区是“监控很多,但还是不知道为什么出事”。原因通常是只监控了资源,没有监控等待链。

建议最少覆盖这些指标:

维度
必看指标
资源
CPU、内存、磁盘 IO、线程数、连接数
依赖
数据库慢 SQL、锁等待、Redis 命中率、MQ Lag、下游 RT
业务
下单成功率、支付回调延迟、库存扣减延迟、对账差异

如果只能先做一件事,优先把“等待相关指标”补出来:

  • • 连接池等待线程数
  • • 线程池队列长度
  • • 下游调用 RT 分布
  • • 锁等待时间
  • • 消息积压恢复时间

6. 变更治理要把“可回滚”放在“可发布”前面

很多事故不是业务峰值打出来的,而是变更把系统推到了边缘。

生产变更至少要满足:

  • • 有灰度,不全量直上
  • • 有回滚,不依赖临场手改配置
  • • 有基线指标,知道发布前后变化
  • • 有冻结窗口,避免在高峰期做高风险动作

这里不建议空谈流程,最实用的动作反而很朴素:

  • • 保留最近 3 个可回退版本
  • • 每次发布前明确观察指标和观察时长
  • • 高风险 DDL 与业务发布解耦

七、只保留真正关键的工程实现

原文里有代码示例,这里只保留最能体现长期修复价值的三段。目的不是凑实现,而是说明“怎么把复盘结论变成可执行约束”。

1. 把连接池等待暴露成指标,而不是等数据库先报警

@Configuration
publicclassHikariMetricsConfig {

@Bean
public DataSource dataSource(MeterRegistry registry) {
HikariConfigconfig=newHikariConfig();
// 省略基础连接配置
        config.setMetricRegistry(registry);
returnnewHikariDataSource(config);
    }
}

这段代码本身不复杂,关键在于后面的监控规则。生产环境至少要补两条判断:

  • • 活跃连接长期超过池上限的 80%
  • • 等待线程数持续大于 0

第一条说明资源紧张,第二条说明业务已经开始排队。真正要命的通常是第二条。

2. 缓存防雪崩不是“加随机数”这么简单,还要限制回源并发

@Service
publicclassCacheService {

privatefinal RedisTemplate<String, Object> redisTemplate;

publicvoidsetWithJitter(String key, Object value, long baseSeconds, long jitterSeconds) {
longttl= baseSeconds + ThreadLocalRandom.current().nextLong(jitterSeconds);
        redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));
    }
}

这只能解决“同一时刻一起过期”的问题,解决不了“过期后一起回源”的问题。生产环境还需要再补:

  • • 热点键单飞控制
  • • 逻辑过期异步刷新
  • • 回源失败的兜底数据
  • • 空值短 TTL 缓存

3. MQ 消费者要先保证幂等和隔离,再谈扩容

@Component
@RocketMQMessageListener(
        topic = "OrderTopic",
        consumerGroup = "core-order-consumer",
        consumeThreadMin = 15,
        consumeThreadMax = 30)

publicclassOrderCoreConsumerimplementsRocketMQListener<OrderMessage> {

@Override
publicvoidonMessage(OrderMessage message) {
Stringkey="order:msg:" + message.getOrderId() + ":" + message.getEventId();
Booleanlocked= redisTemplate.opsForValue().setIfAbsent(key, "1"5, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(locked)) {
try {
                orderService.processCore(message);
            } finally {
                redisTemplate.delete(key);
            }
        }
    }
}

这段代码解决的是“重复消费别把核心业务做两次”。但它依然有适用边界:

  • • Redis 本身必须稳定,否则幂等层会变成新依赖
  • • 幂等键过期时间要覆盖业务可重试窗口
  • • 如果业务要求强一致,还需要结合本地消息表或去重表设计

八、什么时候值得把系统升级到“架构级防故障”

不是所有系统都需要把 Sentinel、HPA、混沌工程、全链路治理一次配齐。对低并发、低复杂度系统来说,过度设计本身就是成本。

更适合升级的典型信号是:

业务条件
推荐动作
原因
核心接口 P99 持续波动,且人工扩容无法稳定
先补等待链监控,再做入口限流和资源隔离
先看清瓶颈再改架构
数据库连接数频繁逼近上限
拆分连接池、治理慢 SQL、限制回源
盲目加连接只会延迟爆炸
每次活动都靠人盯盘手工降级
建立预案开关、灰度和自动告警
人工处置速度跟不上突刺
MQ 积压恢复时间不可预测
重构消费者隔离、失败流转和扩缩容策略
积压本质是消费模型设计问题
发布后抖动频繁,但定位依赖经验
建立基线指标、灰度回滚和变更审计
没有基线就无法判断变更影响

相反,如果系统还处在这些阶段,就没必要一开始就上很重的治理:

  • • 单体应用,日订单量只有几万
  • • 故障主要来自功能 bug,而不是容量和等待链
  • • 团队还没有稳定的监控和发布流程

在这种阶段,先把日志、慢 SQL、连接池和超时配置做好,收益往往比上复杂中间件更直接。


九、复盘之后该落地什么:一份可执行的检查清单

最后留一份简短清单,目的是让复盘能落到下个迭代,而不是停在会议纪要里。

本周内应该完成

  • • 是否已经给数据库连接池暴露活跃数和等待数
  • • 是否区分核心流量和非核心流量的限流策略
  • • 是否检查过热点缓存 TTL 是否集中
  • • 是否核对过核心消费者的下游超时和线程池大小
  • • 是否为关键接口准备了人工降级开关

本月内应该完成

  • • 是否做过一次带业务指标观测的压测
  • • 是否验证过 Redis miss 激增时数据库仍能承受
  • • 是否验证过下游超时场景下消费者会进入死信而不是无限阻塞
  • • 是否保留了可回滚版本和对应变更说明
  • • 是否做过一次最小规模故障演练,例如杀一个 Pod 或注入 200ms 延迟

不要等出事再补的能力

  • • 连接池等待告警
  • • 线程池队列长度告警
  • • MQ Lag 恢复时间监控
  • • 发布后基线对比
  • • 核心链路幂等校验

十、结尾只说一个结论

生产事故里最贵的,不是多买几台机器,也不是多背几条命令,而是在错误方向上浪费掉的前 10 分钟

这份白皮书真正想沉淀的不是“命令大全”,而是一套现场判断顺序:

  • • 先确认是资源耗尽,还是等待放大
  • • 再确认瓶颈在调用链哪一层扩散
  • • 然后用命令取证,而不是靠经验猜测
  • • 最后把止血动作和长期修复彻底分开

如果要从本文只带走一件事,那就是把每次事故都复盘成一句可执行的话:

以后再出现这类等待,我们会在它拖垮共享资源之前发现它、限制它、隔离它。

发表评论
0评