Java后端线上应急手册 — 从告警到复盘的完整SOP

CPU飙升 / OOM / 磁盘打满 / 连接池耗尽 — 一本线上事故的"灭火指南"

Java 线上事故 Arthas JVM调优 止血三板斧 复盘SOP

一、应急响应流程总览 — 黄金10分钟

类比理解:线上事故就像一场火灾。
发现(烟雾报警器)→ 定位(找到火源在哪)→ 止血(灭火器先压制)→ 修复(重建被烧坏的部分)→ 复盘(消防演习,防止再犯)。
核心原则:先止血,再治病。别等查清根因才动手,用户已经在掉单了。
线上事故应急响应5步法
① 发现
告警/Prometheus/Grafana
用户报障
② 定位
日志/Metrics/链路追踪
缩小范围
③ 止血
降级/限流/重启/回滚
恢复服务
④ 修复
代码修复/发版
彻底解决
⑤ 复盘
无指责复盘/根因分析
预防改进

⏱️ 黄金10分钟 — 时间就是金钱

从事故发生到止血,前10分钟决定了事故的影响范围。一个成熟的SRE团队目标:P0级故障15分钟内止血,30分钟内恢复

时间段应该做什么关键动作
T+0min告警触发,值班同学收到确认告警真实性,不忽略
T+1min拉群,通知相关方创建事故群,@oncall,拉业务方
T+2min初步判断影响范围看Grafana大盘,确认是否核心链路
T+5min定位故障模块查日志、链路追踪、排查最近发布
T+8min执行止血回滚/降级/限流/重启
T+15min确认恢复看监控恢复,用户不再报障
实战忠告:很多事故之所以拖到1小时,是因为"想先搞清楚原因再处理"。错!用户不会等你查根因。先做无害的止血动作(如回滚到上一版本),再慢慢查。回滚可以挽救,掉单挽不回。
🤔 值班同学的内心独白:"凌晨3点告警响了...先别慌。第一件事:打开Grafana看大盘,这个告警是不是误报?如果是真的,立刻拉群——别一个人闷头查,团队的力量大于个人。然后看最近1小时有没有发版,80%的事故是变更引起的。"
🔔 #线上故障-支付服务异常(P0)
———— 2026-06-13 02:33 ————
📊
Grafana告警机器人
[P0-FIRING] payment-service CPU usage > 95% for 3min
当前值: 98.7% | 阈值: 95%
实例: 10.0.12.45
Ops
@oncall-小王
收到告警,我现在看。先确认一下,支付链路是否受影响?
Dev
@运营-李姐
客服群已经炸了,20多个用户反馈支付失败 😰
Ops
@oncall-小王
确认是P0,立刻拉大家进来。今晚02:15刚发过版,我先看是不是发布引入的。不排除直接回滚。
———— 02:35 ————
Dev
@开发-老张
我看了一下,这次发版改了订单查询的批量接口,可能是慢SQL拖垮了线程池。我先看日志确认。

二、事故1:CPU飙升到100% — 最常见的事故

📋 事故现象

  • Grafana告警:CPU使用率持续 > 95%
  • 接口大面积超时(RT从50ms飙到3000ms+)
  • 用户反馈:App卡顿、下单失败、页面加载不出来
  • 负载均衡健康检查开始剔除节点
T+2min
开始定位

定位过程:5步找到"罪魁祸首"

1
top 命令找到Java进程PID
看到哪个进程吃CPU最多,记下PID
root@prod-server:~# top top - 02:36:01 up 45 days, 3:21, 1 user, load average: 16.82, 14.20, 8.05 Tasks: 128 total, 2 running, 126 sleeping %Cpu(s): 98.7 us, 1.0 sy, 0.0 ni, 0.3 id, 0.0 wa PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 18432 appuser 20 0 4.2g 2.1g 12m S 785.3 6.5 123:45 java ← 就是它!PID = 18432 2031 mysql 20 0 3.8g 1.2g 8m S 8.2 3.7 45:12 mysqld 8321 root 20 0 200m 80m 5m S 2.1 0.2 1:23 node
🤔 PID是 18432。注意 %CPU 显示785%是因为这是多核,785/8核 ≈ 98%。load average 16.82 也说明CPU已经严重过载(8核机器,理想load < 8)。
2
top -Hp 找到高CPU线程TID
-H 显示线程级别,找到吃CPU的具体线程
root@prod-server:~# top -Hp 18432 top - 02:36:15 ... Threads: 215 total PID USER PR NI VIRT RES SHR S %CPU COMMAND 18478 appu 20 0 4.2g 2.1g 12m R 98.2 java 18479 appu 20 0 4.2g 2.1g 12m R 97.8 java 18480 appu 20 0 4.2g 2.1g 12m R 96.5 java 18392 appu 20 0 4.2g 2.1g 12m S 2.1 java 18501 appu 20 0 4.2g 2.1g 12m S 0.5 java ↑ 三个线程吃满了CPU!记下TID: 18478, 18479, 18480
3
printf 转换TID为十六进制
jstack输出的是十六进制nid,需要转换
root@prod-server:~# printf "%x\n" 18478 482e ↑ 18478 的十六进制是 0x482e,这就是jstack里的 nid=0x482e
4
jstack 抓取线程堆栈,grep 定位
找到该线程正在执行的代码
root@prod-server:~# jstack 18432 | grep -A 30 "nid=0x482e" "http-nio-8080-exec-3" #56 daemon prio=5 os_prio=0 tid=0x7f8a9c01b800 nid=0x482e runnable [0x7f8a8b1e8000] java.lang.Thread.State: RUNNABLE at java.util.regex.Pattern$Curly.match(Pattern.java:4148) at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658) at java.util.regex.Pattern$Loop.match(Pattern.java:4785) at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717) ... (几百行正则回溯) at java.util.regex.Pattern.matcher(Pattern.java:665) at com.order.utils.RegexValidator.checkPhone(RegexValidator.java:23) ← 找到了! at com.order.service.OrderService.validateOrder(OrderService.java:156) at com.order.controller.OrderController.create(OrderController.java:42)
🤔 找到了! 正则回溯灾难(Catastrophic Backtracking)。RegexValidator.checkPhone 这个方法在处理某些特殊格式的手机号时,正则 ^(a+)+$ 这类模式会导致指数级回溯,一个恶意输入就能把CPU打满。根因锁定。
RegexValidator.java — IntelliJ IDEA
RegexValidator.java
OrderService.java
📁 com.order
📁 utils
📄 RegexValidator
📁 service
📁 controller
20// 罪魁祸首:这个正则有回溯灾难问题
21
22public class RegexValidator {
23 // 这个正则在遇到 "aaaaaaaaaaaaaaaaaaaaaaa!" 会指数级回溯
24 private static final String PHONE_REGEX = "^([a-zA-Z0-9]+)+$";
25
26 public static boolean checkPhone(String phone) {
27 return Pattern.matches(PHONE_REGEX, phone); // CPU杀手
28 }
29}
⚠️ 2 warnings UTF-8 LF Java 8
5
或用 Arthas 一键搞定(强烈推荐)
Arthas是阿里开源的Java诊断神器,省去了上面1-4步
root@prod-server:~# java -jar arthas-boot.jar 18432 [arthas@18432] thread -n 3 # 查看CPU占用最高的3个线程 Threads[482e, 482f, 4830] - CPU usage: 295.2%, 294.8%, 293.1% "thread-482e" Id=25 cpuUsage=98.2% RUNNABLE at java.util.regex.Pattern$Curly.match(Pattern.java:4148) at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658) ... at com.order.utils.RegexValidator.checkPhone(RegexValidator.java:27) at com.order.service.OrderService.validateOrder(OrderService.java:156) # 一条命令直接看到最忙线程的完整堆栈,神器!
Arthas 是线上排障神器:除了 thread -n 3,还有 dashboard(看大盘)、trace(方法耗时)、watch(查看参数返回值)、profiler(火焰图)。每个Java后端都应该熟练使用Arthas。安装:curl -O https://arthas.aliyun.com/arthas-boot.jar

根因分析:CPU飙升的常见原因

根因特征典型案例
死循环/无限递归单线程100%,堆栈重复while条件错误,递归无终止
正则回溯灾难Pattern.java大量帧恶意输入触发指数级回溯
频繁Full GCGC线程占CPU,jstat可见内存泄漏,堆几乎满
大对象序列化Jackson/Gson占用高返回10万条数据的列表接口
加密/压缩运算BouncyCastle/zip栈同步加密大文件,未用异步
T+8min
开始止血

🩹 止血方案

A
最快:重启服务kill -9 18432 让K8s自动拉起新Pod。治标不治本,但能立刻恢复。
B
限流降级 — 在网关层对 /api/order/create 接口加限流,拒绝部分请求,保住其他接口。
C
回滚发版 — 如果是最近发布引入的,直接 kubectl rollout undo deployment/order-service 回滚。

三、事故2:内存溢出OOM — 服务定时挂掉

📋 事故现象

类比理解:内存泄漏就像浴缸漏水。水龙头(新对象)一直在流,下水道(GC)也在排,但漏水的速度 > 排水的速度,最终浴缸满了就溢出来了(OOM)。重启服务 = 把浴缸水放空,但漏水的洞还在,所以几小时后又会满。

定位过程:抓取并分析Heap Dump

1
确认JVM启动参数(预防措施)
OOM时自动dump堆内存,这是排查的"黑匣子"
📁 order-service
📂 deploy
📄 jvm-args.sh
📄 Dockerfile
# 生产环境必须配置的JVM参数(OOM时自动dump) java -Xms2g -Xmx2g \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dumps/oom.hprof \ -XX:+PrintGCDetails \ -Xloggc:/data/logs/gc.log \ -jar order-service.jar # 这样OOM时会自动生成 /data/dumps/oom.hprof 文件 # 没配置?手动dump也可以:
专业定义 OOM Heap Dump = JVM 内存溢出时自动生成堆转储文件(hprof),用于事后分析。-XX:+HeapDumpOnOutOfMemoryError 开启,-XX:HeapDumpPath 指定路径。本质:JVM 在抛出 OOM 前,将整个堆内存快照序列化到文件。用 MAT/Arthas 分析大对象占比。
2
手动抓取Heap Dump
服务还没挂的时候抓最有价值
root@prod-server:/data# jmap -dump:format=b,file=heap.hprof 18432 Dumping heap to heap.hprof ... Heap dump file created [1854621873 bytes in 8.234 secs] # 生成了1.7G的dump文件,下载到本地用MAT分析 local@macbook:~# scp root@10.0.12.45:/data/heap.hprof ~/Downloads/
3
jstat 监控GC情况(确认内存泄漏)
每秒打印一次GC状态,观察趋势
root@prod-server:~# jstat -gcutil 18432 1000 # 列含义: S0 S1 E O M YGC YGCT FGC FGCT GCT S0 S1 E O M YGC YGCT FGC FGCT GCT 0.00 85.44 73.22 91.45 95.32 523 12.34 47 8.921 21.26 # T0: 老年代91% 0.00 88.12 78.55 93.88 95.32 541 12.89 52 9.102 22.01 # +1s: 老年代93% 0.00 91.30 82.10 96.22 95.32 568 13.45 58 9.567 22.89 # +2s: 老年代96% 0.00 93.50 88.40 98.71 95.32 589 14.01 63 10.23 23.78 # +3s: 老年代98%!FGC飙到63 0.00 95.20 92.15 99.82 95.32 612 14.67 71 11.01 24.66 # +4s: 老年代99.8% 快炸了 ⚠️ 老年代(O)从91%涨到99%,Full GC(FGC)从47涨到71,每秒都在涨! # FGC 10秒内从47→71,涨了24次!Full GC停顿导致STW,接口超时
🤔 jstat 证明了内存泄漏无疑:老年代使用率持续上升,Full GC无法回收。O列 = 老年代使用率,FGC = Full GC次数。正常情况FGC应该很稳定(几小时一次),现在1秒1次,每次STW几百毫秒,这就是接口超时的原因。
4
MAT 分析Dump文件,找到大对象
Eclipse MAT(Memory Analyzer Tool)是排查内存泄漏的标准工具
Eclipse MAT — Leak Suspects Report
⚠️ Problem Suspect 1 — 占堆内存 68.3%
One instance of "com.order.cache.LocalCacheManager"
contains 1,248,331 instances of "com.order.dto.UserDTO"
occupied 1.27 GB (68.3%) of 1.86 GB total heap.
The class "com.order.cache.LocalCacheManager" has a static ConcurrentHashMap userCache that keeps growing because entries are never evicted.
🔗 Shortest Path to GC Root:
java.lang.Thread → LocalCacheManager.userCache (static) → ConcurrentHashMap$Node[] → UserDTO ×1,248,331
🤔 根因锁定! LocalCacheManager.userCache 这个静态ConcurrentHashMap只put不remove,用户数据无限堆积。124万个UserDTO对象占满了堆。这是一个典型的"静态集合无限增长"内存泄漏。
LocalCacheManager.java — 问题代码
LocalCacheManager.java
OrderService.java
📁 com.order
📁 cache
📄 LocalCacheManager
📁 service
1package com.order.cache;
2
3import java.util.concurrent.*;
4
5/**
6 * 本地缓存 — 罪魁祸首!只进不出 = 内存泄漏
7 */
8public class LocalCacheManager {
9
10 // 静态Map,GC Root,永远不会被回收
11 private static final ConcurrentHashMap<String, UserDTO> userCache
12 = new ConcurrentHashMap<>();
13
14 public static void put(String userId, UserDTO user) {
15 userCache.put(userId, user); // 只put,永远不remove!
16 }
17
18 public static UserDTO get(String userId) {
19 return userCache.get(userId);
20 }
21 // ❌ 没有过期机制,没有size上限,没有eviction策略
22}
⚠️ Memory leak detected UTF-8 Java 8
5
Arthas 实时观察 — dashboard 命令
[arthas@18432] dashboard Dashboard for JVM: 18432 (order-service) Threads: 215 total, 212 RUNNABLE, 3 BLOCKED Memory: heap used: 1.82G / 2.0G (91.0%) ← 快满了 non-heap used: 85.4M / 256M (33.3%) eden used: 180M / 640M survivor used: 50M / 80M old used: 1.59G / 1.28G (124.2%) ← 老年代溢出!OOM前兆 Garbage Collector: PS Scavenge 612 times, total 14.67s PS MarkSweep(Full GC) 71 times, total 11.01s ← Full GC太频繁

根因:内存泄漏的常见类型

泄漏类型原理解决方案
静态集合无限增长static Map只put不remove用Caffeine/Guava Cache设过期+上限
ThreadLocal未清理线程池复用线程,ThreadLocal残留finally中 threadLocal.remove()
数据库大结果集SELECT * 查出百万行到内存分页查询 / 流式读取
监听器未注销注册的Listener/Callback未移除对象销毁时主动unregister
内部类持有外部类非静态内部类隐式持有this改为静态内部类
T+10min
止血

🩹 止血方案

A
重启 + 临时扩容 — 先重启恢复服务,临时把 -Xmx2g 改成 -Xmx4g 争取排查时间(治标)。
B
限流减压 — 减少QPS,减缓内存增长速度,争取排查窗口。
C
紧急修复 — 给 userCache 换成 Caffeine(设最大容量+过期时间),紧急发版。
📁 order-service
📂 src
📂 main
📂 resources
📄 logback-spring.xml
📄 application.yml
// 修复:用 Caffeine 替代手写的本地缓存,自动过期+限制大小 @Component public class UserCacheManager { private final Cache<String, UserDTO> cache = Caffeine.newBuilder() .maximumSize(10_000) // 最多1万条,防无限增长 .expireAfterWrite(Duration.ofMinutes(30)) // 30分钟过期 .recordStats() // 开启统计 .build(); public UserDTO get(String userId) { return cache.getIfPresent(userId); } public void put(String userId, UserDTO user) { cache.put(userId, user); } }
专业定义 Logback 滚动策略 = 通过 RollingFileAppender + SizeAndTimeBasedRollingPolicy 自动管理日志文件。maxFileSize 单文件大小,maxHistory 保留天数,totalSizeCap 总容量上限。

四、事故3:磁盘打满 — 隐蔽的杀手

📋 事故现象

类比理解:磁盘打满就像你家的垃圾桶满了。你不能扔垃圾(写日志),不能收快递(上传文件),连马桶都不能冲(数据库binlog写不进去)。最隐蔽的是:垃圾堆在走廊(日志目录),但客厅看起来还正常,直到垃圾堆到天花板才发现。
🤔 磁盘打满最容易被忽略,因为服务不会立刻挂。但日志写不进去意味着你丢了最重要的排障线索!很多团队规定:磁盘告警阈值设为 80%,给运维留出处理时间。

定位过程:找到"磁盘杀手"

1
df -h 查看整体磁盘使用情况
root@prod-server:~# df -h Filesystem Size Used Avail Use% Mounted on /dev/vda1 50G 48G 2G 96% / ← 根分区快满了!只剩2G /dev/vdb1 200G 45G 155G 23% /data ← 数据盘很空 tmpfs 7.8G 0 7.8G 0% /dev/shm # 根分区 96% 使用率!告警阈值通常设80%,已经超了
2
du -sh 逐层找最大的目录
root@prod-server:~# du -sh /* 2>/dev/null | sort -rh | head -10 38G /var ← 最大!进去看 5.2G /usr 2.1G /home 1.8G /opt 450M /tmp root@prod-server:~# du -sh /var/* 2>/dev/null | sort -rh | head -5 35G /var/log ← 日志目录!最大嫌疑 2.1G /var/lib 500M /var/cache root@prod-server:~# du -sh /var/log/* 2>/dev/null | sort -rh | head -5 28G /var/log/order-service ← 就是它!order服务日志28G 4.2G /var/log/nginx 1.1G /var/log/mysql
3
find 找到大文件,通常是未做轮转的日志
root@prod-server:~# find /var/log -type f -size +500M -exec ls -lh {} \; -rw-r--r-- 1 appuser appuser 8.2G Jun 13 02:40 /var/log/order-service/order.log ← 8.2G单个日志文件! -rw-r--r-- 1 appuser appuser 6.5G Jun 13 02:40 /var/log/order-service/order-error.log -rw-r--r-- 1 appuser appuser 4.8G Jun 13 02:20 /var/log/order-service/gc.log -rw-r--r-- 1 appuser appuser 3.1G Jun 13 02:35 /var/log/order-service/order.log.20260610 ← 3天前的日志还在! # 罪魁祸首:logback没有配置日志轮转(rolling),单个文件无限增长
🤔 找到了!日志未做轮转。生产环境的日志必须配置:
① 按大小切割(如100MB一个文件)
② 按天滚动 + 自动压缩
③ 只保留最近N天(如7天/30天自动清理)
Spring Boot 默认 logback 配置是单文件无限制,必须自定义!
T+5min
止血

🩹 止血:紧急清理 + 长期修复

A
紧急清理(止血) — 先释放空间恢复服务
📁 order-service
📂 src
📂 main
📂 java
📂 com
📂 shop
📂 controller
📄 OrderController.java
📄 FallbackController.java
# 删除3天前的旧日志(留近期日志用于排障) find /var/log/order-service -name "*.log.*" -mtime +3 -delete # 压缩大于1G的历史日志 gzip /var/log/order-service/order.log.20260610 # 清空当前大文件(保留文件句柄,不重启服务) # 用 truncate 而不是 rm,避免服务找不到文件 truncate -s 0 /var/log/order-service/order.log
面试金句 降级 (Degradation) = 非核心功能在系统压力大时主动关闭,保核心链路。面试要点:(1) 降级开关放配置中心,无需发版 (2) 降级粒度:接口级 > 功能级 > 页面级 (3) 降级 ≠ 熔断。
B
长期修复:配置 logback 日志轮转
📁 order-service
📂 src
📂 main
📂 java
📂 com
📂 shop
📂 config
📄 FlowRuleConfig.java
📄 RateLimitConfig.java
<configuration> <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/order-service/order.log</file> <rollingPolicy class="...SizeAndTimeBasedRollingPolicy"> <fileNamePattern>order-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <maxFileSize>100MB</maxFileSize> <!-- 单文件最大100M --> <maxHistory>7</maxHistory> <!-- 保留7天 --> <totalSizeCap>5GB</totalSizeCap> <!-- 总上限5G --> </rollingPolicy> </appender> </configuration>
专业定义 限流 (Rate Limiting) = 控制单位时间请求数量。核心算法:(1) 计数器 (2) 滑动窗口(Sentinel)(3) 令牌桶(Guava)(4) 漏桶(Nginx)。
C
扩容磁盘 — 云盘可以在线扩容:lsblkgrowpartresize2fs
⚠️ 关键注意:清理正在写入的日志文件,千万不要直接 rm!rm 删除文件但Java进程仍持有文件句柄,空间不会释放,且进程仍往"已删除"的文件写日志。必须用 truncate -s 0 清空内容,或 echo "" > file。要彻底解决需重启服务。

五、事故4:数据库连接池耗尽 — 接口雪崩

📋 事故现象

核心机制:数据库连接池大小=20,正常情况下请求用完连接就归还。但如果某个请求开启了事务却长时间不提交也不回滚(长事务),这个连接被一直占用不释放。20个连接都被长事务占满后,新请求拿不到连接,只能排队等待(接口超时),最终连接池耗尽。
🔔 #线上故障-接口超时
———— 2026-06-13 14:20 ————
📱
@测试-小陈
所有接口都超时了!首页加载30秒,点哪都是超时。APM看到大量 HikariPool timeout 错误。
Ops
@oncall-小王
看到了。MySQL本身正常,CPU才20%。问题在应用侧——连接池被打满了。我先看Druid监控。
Dev
@开发-老张
等等,我看了一下最近提交...那个"导出报表"的功能,一个事务里查了50万行数据,还在for循环里调远程接口...连接一直占着不放。
TL
@架构师-刘
经典的长事务 + 连接不释放。先紧急下线"导出报表"功能接口,止血。然后老张改代码:大事务拆成小批次 + 异步导出。

定位过程

1
查看 HikariCP / Druid 监控面板 — 连接池状态
Druid Monitor — http://app:8080/druid/datasource.html
⚠️ DataSource: orderDataSource — 连接池耗尽
Active Pooling MaxActive WaitThreadCount
20 0 20 147
Active = MaxActive = 20 → 连接池已满,所有连接都被占用
WaitThreadCount = 147 → 147个线程在排队等连接,全部超时
2
查看 MySQL SHOW PROCESSLIST — 谁占着连接不放?
MySQL Workbench — 10.0.12.46:3306 / production
SHOW FULL PROCESSLIST;
Id
User
Host
db
Command
Time
State
Info
8231
app
10.0.12.45
order
Query
1240s
Sending data
SELECT * FROM order_detail WHERE...
8232
app
10.0.12.45
order
Query
1185s
Sending data
SELECT * FROM order_detail WHERE...
8233
app
10.0.12.45
order
Sleep
890s
NULL
8234
app
10.0.12.45
order
Sleep
745s
NULL
...
(还有18个类似连接)
600s+
🤔 真相大白:20个连接全部被占住!其中2个在执行"导出报表"的大SQL(跑了1240秒还没结束!),18个处于Sleep状态——这是典型的长事务不提交:代码开了事务但忘了close/commit,连接被挂起不归还。Time=1240s 说明这个查询跑了20分钟,肯定是在做全表扫描。
3
查代码:找到长事务的根源
ReportExportService.java — 问题代码
ReportExportService.java
📁 com.order
📁 service
📄 ReportExportService
1@Service
2public class ReportExportService {
3
4 @Autowired private JdbcTemplate jdbc;
5
6 @Transactional // ❌ 罪魁祸首!整个方法一个大事务
7 public void exportReport(String date) {
8 List<OrderDTO> all = jdbc.queryForList(
9 "SELECT * FROM order_detail WHERE create_time > ?", // ❌ 50万行全查
10 date);
11 for (OrderDTO o : all) { // ❌ for循环里调远程接口,超慢
12 String name = userService.getUserName(o.getUserId()); // HTTP调用!
13 excelWriter.write(o, name);
14 }
15 // 整个方法执行完才提交事务,连接被独占20分钟!
16 }
17}
⚠️ Transaction held too long UTF-8 Java 8
4
Arthas: trace 查看连接获取耗时
[arthas@18432] trace javax.sql.DataSource getConnection -n 5 --condition '#cost > 1000' # 只看耗时超过1秒的 getConnection 调用 `---p.CobarDataSource.getConnection() cost: 30.23s ← 获取连接花了30秒! +---HikariPool.getConnection() cost: 30.23s | `---HikariPool.pool.borrow() cost: 30.22s ← 一直在等连接归还 | `---HikariPool.waitForConnection() timeout! # 30秒都没拿到连接,最终抛出 ConnectionNotAvailableException
T+6min
止血

🩹 止血方案

A
Kill慢查询 — 直接干掉占着连接的大查询:KILL 8231; KILL 8232;(processlist里的Id)。连接立刻释放。
B
临时调大连接池spring.datasource.hikari.maximum-pool-size=50(治标,20→50,但MySQL侧也要能承受)。
C
下线问题接口 — 紧急关闭"导出报表"功能入口,或改成异步导出(MQ + 定时任务)。
编码规范:① 事务方法不要包含远程调用(HTTP/RPC);② 大数据量操作必须分批(每批1000条);③ 查询尽量走索引,避免全表扫描;④ 连接池大小 = (核心数 * 2) + 有效磁盘数(PostgreSQL公式),不是越大越好。

六、止血手段大全 — 系统的"刹车系统"

线上事故定位后(甚至定位中),止血永远是第一优先级。以下是后端常用的5种止血手段,每种都有适用场景。

类比理解:止血手段就像汽车的刹车系统:
降级 = 关空调保发动机(牺牲非核心保核心)
限流 = 收费站只开2个口(控制流量到系统能承受的范围)
熔断 = 保险丝跳闸(下游挂了就别调了,快速失败)
回滚 = 撤销刚才的操作(回到上一个已知正常的版本)
扩容 = 叫增援部队(加机器顶住流量)

止血决策树 — 根据场景选手段
事故发生 是发版引起的吗?
是 → 回滚 | 否 → 是下游依赖挂了吗?
是 → 熔断该依赖 | 否 → 流量过大?
是 → 限流 + 扩容 | 否 → 降级非核心功能

五大止血手段对比

手段 适用场景 生效速度 数据影响 常用工具
降级
关闭非核心
核心链路受影响,需要"断臂求生" 秒级(开关) 部分功能不可用 配置中心开关 / Sentinel
限流
拒绝部分请求
流量超过系统承载能力 秒级 部分请求被拒(429) Sentinel / Guava RateLimiter
熔断
快速失败
下游服务超时/错误率飙升 自动触发 依赖接口暂不可用 Hystrix / Resilience4j
回滚
回到上一版本
新版本引入的Bug 分钟级(发版) 无(回到已知正常状态) kubectl rollout undo
扩容
加机器
流量真实增长,资源不足 分钟级 无(增加处理能力) kubectl scale / HPA

手段1:降级 (Degradation) — 壮士断腕

思路:关闭非核心功能(评论、推荐、积分、消息推送),把所有资源让给核心交易链路(下单、支付)。

📁 order-service
📂 src
📂 main
📂 java
📂 com
📂 shop
📂 cache
📄 LocalCache.java
📄 CacheConfig.java
// 降级开关:通过配置中心动态控制,无需发版 @RestController public class OrderController { @NacosValue(value = "${degradation.recommendation.enabled:false}", autoRefreshed = true) private boolean recommendDegraded; @GetMapping("/api/home") public Result home() { Result result = new Result(); result.put("banners", bannerService.list()); // 核心:保留 result.put("products", productService.list()); // 核心:保留 if (!recommendDegraded) { result.put("recommendations", recommendService.get()); // 非核心:可降级 } return result; } }
面试金句 Caffeine 本地缓存 = JVM 级别高性能缓存库(Spring Boot 默认)。相比手写 Map 缓存:(1) W-TinyLFU 淘汰算法命中率更高 (2) 自动过期(TTL/TTI)(3) 大小限制防止 OOM (4) 异步刷新。面试要点:说明为什么不用 ConcurrentHashMap——无界增长导致 OOM。
实战:双11大促时,淘宝会降级"评价"、"收藏"、"推荐"等功能,把所有算力让给交易。降级开关必须通过配置中心(Nacos/Apollo)动态推送,不能改代码发版。

手段2:限流 (Rate Limiting) — 控制流量

思路:当流量超过系统承载能力,主动拒绝多余请求,保住系统不崩溃。

📁 order-service
📂 src
📂 main
📂 java
📂 com
📂 shop
📂 service
📄 UserService.java
📄 OrderService.java
// Sentinel 限流规则配置 public class FlowRuleInit { public static void init() { List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("createOrder"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(500); // 限制QPS = 500 rule.setLimitApp("default"); // 对所有来源生效 rules.add(rule); FlowRuleManager.loadRules(rules); } } // 业务代码使用 @SentinelResource 注解保护 @SentinelResource(value = "createOrder", blockHandler = "orderBlockHandler") public Order createOrder(OrderRequest req) { return orderService.create(req); } // 被限流时的兜底方法 public Order orderBlockHandler(OrderRequest req, BlockException ex) { throw new ServiceException("系统繁忙,请稍后重试"); // 返回429 }
专业定义 熔断 (Circuit Breaker) = 下游异常率超阈值时自动切断请求。三态:CLOSED → OPEN → HALF_OPEN。Resilience4j 用滑动窗口统计失败率。

手段3:熔断 (Circuit Breaker) — 保护下游也保护自己

熔断器三状态机
CLOSED
正常调用
→ 错误率>50% → OPEN
熔断,快速失败
→ 等待10s → HALF_OPEN
放行1个试探
→ 成功 → CLOSED
📁 order-service
📂 deploy
📄 cleanup-logs.sh
📄 logback.xml
// Resilience4j 熔断器配置 @CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback") public User getUser(String userId) { return userClient.getUser(userId); // 调下游 } // 熔断后的降级方法:返回缓存或默认值 public User getUserFallback(String userId, Exception e) { return userCache.get(userId); // 从本地缓存返回 // 或返回 User.default() } # application.yml 熔断配置 resilience4j.circuitbreaker: instances: userService: failureRateThreshold: 50 # 错误率阈值50% waitDurationInOpenState: 10s # OPEN持续10秒 slidingWindowSize: 100 # 滑动窗口100次
专业定义 日志磁盘清理 = 紧急止血手段。长期方案:(1) Logback RollingFileAppender 自动按大小/时间切割 (2) maxHistory 控制保留天数 (3) totalSizeCap 限制总大小 (4) 日志收集到 ELK/Loki 集中存储。止血 = rm/find,根治 = Logback 配置。

手段4:回滚 (Rollback) — 最快恢复

# K8s 回滚到上一个版本(秒级) root@jenkins:~# kubectl rollout undo deployment/order-service -n prod deployment.apps/order-service rolled back # 回滚到指定历史版本 root@jenkins:~# kubectl rollout history deployment/order-service -n prod deployment.apps/order-service REVISION CHANGE-CAUSE 1 kubectl apply --filename=order-v1.2.0.yaml 2 kubectl apply --filename=order-v1.2.1.yaml ← 这次发版出事了 3 kubectl apply --filename=order-v1.2.2.yaml ← 当前 root@jenkins:~# kubectl rollout undo deployment/order-service --to-revision=1 -n prod deployment.apps/order-service rolled back # 回到v1.2.0

手段5:扩容 (Scaling) — 加机器顶住

# K8s 手动扩容:从3个Pod扩到8个 root@jenkins:~# kubectl scale deployment/order-service --replicas=8 -n prod deployment.apps/order-service scaled # 查看扩容进度 root@jenkins:~# kubectl get pods -n prod -l app=order-service -w NAME READY STATUS RESTARTS AGE order-service-7b8f9-x2k4p 1/1 Running 0 45d order-service-7b8f9-m9n3q 1/1 Running 0 45d order-service-7b8f9-p1o8r 1/1 Running 0 45d order-service-7b8f9-a2b3c 0/1 ContainerCreating 0 3s # 新Pod启动中 order-service-7b8f9-d4e5f 0/1 ContainerCreating 0 3s order-service-7b8f9-g7h8i 0/1 ContainerCreating 0 3s order-service-7b8f9-j1k2l 0/1 ContainerCreating 0 2s order-service-7b8f9-m3n4o 0/1 ContainerCreating 0 2s # 约30秒后全部Ready,流量自动分发到8个Pod
📁 order-service
📂 deploy
📄 hpa-config.yaml
📄 deployment.yaml
# HPA 自动扩容(根据CPU自动调整Pod数量) kubectl autoscale deployment order-service \ --cpu-percent=60 \ --min=3 \ --max=20 \ -n prod # CPU超过60%自动扩容,最少3个Pod,最多20个Pod
专业定义 K8s HPA = 根据 CPU/内存/自定义指标自动水平扩缩容。Metrics Server 采集 → HPA Controller 对比目标值 → 调整 replicas。
🤔 止血选择的优先级:① 能回滚就先回滚(最快最安全);② 回滚不了就降级 + 限流;③ 资源不够就扩容;④ 下游问题就熔断。千万不要第一反应是"重启大法"——重启丢失现场,后面定位根因会非常困难。

七、复盘文档模板 — 无指责复盘文化

类比理解:复盘就像交通事故的"事故鉴定书"。目的不是追责谁闯了红灯,而是搞清楚为什么红绿灯设计有问题,让下次不再发生。
Google/SRE 的核心理念:Blameless Postmortem(无指责复盘)——对事不对人。如果一个人犯了错,那说明流程/系统有漏洞,让人容易犯错。
无指责复盘的核心原则: ① 假设所有参与者都尽了最大努力,做了正确的判断;
② 聚焦系统和流程的问题,而不是个人的错误;
③ 每一个"人为失误"背后,都有一个"系统设计缺陷";
④ 目标是改进,不是惩罚。惩罚会让人隐瞒问题。
📋 事故复盘 — INC-2026-0613-001:支付服务CPU飙升导致P0故障 P0 · 已关闭

📌 事故概述

2026年6月13日凌晨02:33,支付服务CPU飙升至98%,导致支付接口大面积超时,持续28分钟,影响约3400笔交易失败。经查为02:15发布版本引入正则回溯漏洞。

⏱️ 事故时间线

  • 02:15 支付服务发布 v1.2.2 版本(含订单查询接口优化)
  • 02:33 Grafana触发CPU告警(>95%持续3分钟),oncall收到
  • 02:35 确认P0故障,拉故障群,通知业务方
  • 02:40 Arthas定位到 RegexValidator.checkPhone 正则回溯
  • 02:43 决策:执行回滚(回到 v1.2.1)
  • 02:48 回滚完成,CPU恢复正常,服务恢复
  • 02:55 确认所有监控恢复正常,用户不再报障
  • 03:10 故障关闭

📊 影响范围

  • 持续时间:28分钟(02:33 ~ 03:01)
  • 业务影响:约3400笔支付失败,预估损失 ¥18.7万
  • 用户影响:约2100名用户受影响,客服收到87个投诉
  • 资损处理:补偿受影响用户优惠券(成本约¥5万)

🔍 根因分析 (Root Cause Analysis)

技术根因:v1.2.2版本中,RegexValidator.checkPhone() 使用了存在回溯灾难的正则 ^([a-zA-Z0-9]+)+$,当收到特殊格式输入时触发指数级回溯,CPU打满。

流程根因(5 Whys):

  • Why 1:为什么CPU飙升?→ 正则回溯灾难
  • Why 2:为什么这个正则上线了?→ 测试环境没有覆盖这类输入
  • Why 3:为什么测试没覆盖?→ 缺少边界值/恶意输入的测试用例
  • Why 4:为什么缺少这类用例?→ Code Review阶段没有检查正则安全性
  • Why 5:为什么CR没检查?→ 团队缺少"正则安全"的Review Checklist

🩹 临时措施(已完成)

  • ✅ 已回滚至 v1.2.1
  • ✅ 临时在网关层对手机号字段加格式校验(拦截特殊字符)

🚀 长期改进项(Action Items)

改进项负责人截止时间优先级
修复正则表达式,增加输入长度限制(≤20字符)老张06-14P0
在CI流程中加入正则安全检查(使用RE2)架构组06-20P1
补充边界值测试用例(恶意输入、超长字符串)QA小陈06-18P1
更新Code Review Checklist,增加正则安全项架构师刘06-25P2
优化发布流程:P0接口发版需灰度10%流量观察30minSRE07-01P1
报告人: oncall-小王 审核: 架构师-刘 创建: 2026-06-13 10:00 状态: 已闭环
🤔 复盘的核心价值不在于"找到责任人",而在于"改进系统"。这次复盘发现了5个流程漏洞:测试覆盖不足、CR Checklist缺失、灰度机制不到位……修好这些,比处罚任何人都有效。好的复盘文档 = 让同样的故障不再发生第二次。

复盘会议的"三要三不要"

✅ 三要❌ 三不要
聚焦系统和流程的改进 不要追责或批评个人
基于事实和数据说话 不要用"如果当时你…就好了"的句式
输出可执行、有Owner、有Deadline的Action Items 不要泛泛而谈"以后注意",没有落地

总结 — 做一个靠谱的On-Call

🎯 线上应急的核心素养

素养说明
冷静慌乱是最大的敌人。深呼吸,按SOP走。
先止血恢复服务 > 查根因。用户在掉单,每秒都是钱。
善用工具Arthas / Grafana / jstack / jmap 是你的武器库。
及时沟通拉群、同步进展、管理预期。别一个人闷头查。
事后复盘每一个故障都是宝贵的经验,写进知识库。
预防为主监控告警 + 混沌工程 + 演练,把问题消灭在发生前。
应急能力的成长路径
新手
告警来了
手忙脚乱
熟练
按SOP
止血恢复
资深
快速定位
多种手段
专家
根因分析
系统改进
架构师
预防为主
混沌演练
最后的话:没有不出故障的系统,只有不会处理故障的团队。每一次线上事故,都是一次免费的压力测试和成长机会。重要的是:同样的故障不要发生第二次。这就是复盘的意义。

记住这句话:"好的系统不是不挂,而是挂了能快速恢复。"
🔔 #线上故障-支付服务异常(已恢复)
———— 2026-06-13 03:10 ————
Ops
@oncall-小王
各位,支付服务已恢复,CPU正常,用户不再报障。故障持续28分钟,已确认回滚止血。辛苦大家,明天上午10点开复盘会。
Dev
@开发-老张
收到。根因已初步定位:正则回溯。我今晚写完复盘文档,明天会上过。Action Items我认领。
TL
@架构师-刘
处理得不错,28分钟止血,达标。复盘会聚焦流程改进:为什么这个正则能上线?我们的防线哪里漏了? 不追责,看流程。
📱
@运营-李姐
客服侧确认,用户投诉已平息,补偿券已发。大家辛苦了,早点休息 🌙