找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4057

积分

0

好友

535

主题
发表于 2 小时前 | 查看: 4| 回复: 0

真正把系统拖进深夜会议室的,通常不是“某个组件挂了”,而是大家在前 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. 系统是在变慢,还是已经部分不可用
  2. 瓶颈在调用链哪一层扩散
  3. 当前是资源耗尽,还是等待放大
  4. 应急动作会不会把压力转移到下游

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

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

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

故障类型 典型信号 先看什么
资源耗尽 CPU、内存、连接数、线程数打满 topvmstat、连接池指标
等待放大 锁等待、线程阻塞、下游超时 jstackprocesslist、调用超时分布
流量失控 突刺、缓存同时失效、重试风暴 网关 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 OOM、内核级软硬件错误
18 journalctl -xe --since '10 min ago' 系统级错误的最近上下文
19 cat /proc/loadavg 当前负载是否还在继续爬升
20 uname -a 事故现场确认内核版本和环境差异

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

# 命令 它回答什么
21 ps -ef \| grep java Java 进程是否还在
22 ps -Lp <pid> \| wc -l 线程数是否过多
23 prlimit --pid <pid> 进程级资源上限是多少
24 lsof -p <pid> \| wc -l 打开的文件句柄总数
25 lsof -p <pid> \| grep TCP 打开的 TCP 连接情况
26 lsof -i :8080 谁占用了服务端口
27 cat /proc/<pid>/limits nofile 等限制值是否过低
28 cat /proc/<pid>/status 线程数、内存、上下文切换摘要
29 pmap -x <pid> \| tail -20 内存映射段是否异常增长
30 watch -n 1 "ls /proc/<pid>/fd \| wc -l" 文件描述符增长趋势

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

# 命令 它回答什么
31 ss -s 当前 TCP 状态总体分布
32 ss -lntp 监听端口是否正常
33 ss -antp \| grep :3306 到 MySQL 的连接情况
34 ss -antp \| grep TIME-WAIT TIME-WAIT 是否过多
35 ss -antp \| grep SYN-SENT 是否存在大量建连失败
36 netstat -anp \| grep <pid> 特定进程的网络连接(备选方案)
37 ip addr 网卡和 IP 是否正常
38 ip route 路由是否异常
39 ping <host> 连通性是否直接失败
40 traceroute <host> 路由路径是否异常变长
41 mtr -rwzbc 20 <host> 是否存在持续抖动和丢包
42 nstat -az \| grep TcpRetransSegs 是否存在 TCP 重传
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、死锁、锁竞争还是下游等待

当我们在分析Java应用时,JVM 层面的排查至关重要。

# 命令 它回答什么
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 <pid> \| grep -n 'BLOCKED' 是否有阻塞线程
53 jstack <pid> \| grep -n 'waiting for monitor entry' 是否有等待锁的线程
54 jcmd <pid> GC.class_histogram 哪类对象在疯狂占内存
55 jmap -histo:live <pid> \| head -50 存活对象 Top 50(会触发 Full GC)
56 jmap -dump:live,format=b,file=heap.hprof <pid> 需要离线分析堆时使用,注意会暂停
57 jcmd <pid> VM.native_memory summary 是否本地内存膨胀而非 Java 堆
58 jcmd <pid> PerfCounter.print \| head -50 JVM 内部性能计数器概览
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 不高 jstackSHOW FULL PROCESSLISTss -antp
CPU 100% top -Hpasync-profilerEXPLAIN
大量 502/504 网关日志、应用线程栈、下游连接状态
消息积压 consumerProgress、消费者线程栈、下游超时分布
大促整点雪崩 Redis TTL、回源 SQL、入口 QPS 与重试数

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

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

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

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

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

现场现象

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

第一眼最容易下的结论

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

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

现场排查路径

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

真正根因

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

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

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

临时止血

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

长期修复

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

修复后的验证方式

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

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

现场现象

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

第一眼最容易下的结论

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

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

现场排查路径

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

真正根因

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

这类问题为什么危险

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

临时止血

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

长期修复

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

修复后的验证方式

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

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

现场现象

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

第一眼最容易下的结论

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

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

现场排查路径

  1. mqadmin consumerProgress 看到某个核心消费组落后严重。
  2. mqadmin consumerConnection 确认消费者实例都在线,并不是实例丢失。
  3. jstack 发现大量消费线程等待在调用支付网关的 HTTP 客户端上。
  4. 支付网关配置超时 5 秒,但实际平均 RT 已经高于 8 秒。
  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
public class HikariMetricsConfig {

    @Bean
    public DataSource dataSource(MeterRegistry registry) {
        HikariConfig config = new HikariConfig();
        // 省略基础连接配置
        config.setMetricRegistry(registry);
        return new HikariDataSource(config);
    }
}

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

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

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

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

@Service
public class CacheService {

    private final RedisTemplate<String, Object> redisTemplate;

    public void setWithJitter(String key, Object value, long baseSeconds, long jitterSeconds) {
        long ttl = 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)
public class OrderCoreConsumer implements RocketMQListener<OrderMessage> {

    @Override
    public void onMessage(OrderMessage message) {
        String key = "order:msg:" + message.getOrderId() + ":" + message.getEventId();
        Boolean locked = 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 分钟

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

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

如果要从本文只带走一件事,那就是把每次事故都复盘成一句可执行的话:
以后再出现这类等待,我们会在它拖垮共享资源之前发现它、限制它、隔离它。




上一篇:本体论落地5层7点错配,及注入Agent Harness的4个方法
下一篇:RocketMQ 顺序消息实战:高并发订单状态流转架构设计
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-24 09:30 , Processed in 0.942937 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表