一、应急响应流程总览 — 黄金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%的事故是变更引起的 。"
———— 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
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
3
printf 转换TID为十六进制
jstack输出的是十六进制nid,需要转换
root@prod-server:~# printf "%x\n" 18478
482e
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
22 public 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
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 GC GC线程占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 — 服务定时挂掉
📋 事故现象
服务每次重启后正常运行3-4小时,然后突然OOM挂掉
Grafana显示堆内存呈"锯齿状"但整体持续上升(monotonic increase)
Full GC越来越频繁,从10分钟一次变成每分钟一次
最终 java.lang.OutOfMemoryError: Java heap space 服务崩溃
类比理解: 内存泄漏就像浴缸漏水。水龙头(新对象)一直在流,下水道(GC)也在排,但漏水的速度 > 排水的速度,最终浴缸满了就溢出来了(OOM)。重启服务 = 把浴缸水放空 ,但漏水的洞还在,所以几小时后又会满。
定位过程:抓取并分析Heap Dump
1
确认JVM启动参数(预防措施)
OOM时自动dump堆内存,这是排查的"黑匣子"
# 生产环境必须配置的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 ]
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
0.00 85.44 73.22 91.45 95.32 523 12.34 47 8.921 21.26
0.00 88.12 78.55 93.88 95.32 541 12.89 52 9.102 22.01
0.00 91.30 82.10 96.22 95.32 568 13.45 58 9.567 22.89
0.00 93.50 88.40 98.71 95.32 589 14.01 63 10.23 23.78
0.00 95.20 92.15 99.82 95.32 612 14.67 71 11.01 24.66
⚠️ 老年代(O)从91%涨到99%,Full GC(FGC)从47涨到71,每秒都在涨!
🤔 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
1 package com.order.cache;
2
3 import java.util.concurrent.*;
4
5 /**
6 * 本地缓存 — 罪魁祸首!只进不出 = 内存泄漏
7 */
8 public 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(设最大容量+过期时间),紧急发版。
// 修复:用 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:磁盘打满 — 隐蔽的杀手
📋 事故现象
服务突然无法写日志:java.io.IOException: No space left on device
文件上传接口报错:磁盘写入失败
MySQL无法写binlog:Slave延迟暴增,主从同步中断
更诡异的表现:无法创建新文件,但服务"看起来还在跑"
类比理解: 磁盘打满就像你家的垃圾桶满了。你不能扔垃圾(写日志),不能收快递(上传文件),连马桶都不能冲(数据库binlog写不进去)。最隐蔽的是 :垃圾堆在走廊(日志目录),但客厅看起来还正常,直到垃圾堆到天花板才发现。
🤔 磁盘打满最容易被忽略,因为服务不会立刻挂 。但日志写不进去意味着你丢了最重要的排障线索!很多团队规定:磁盘告警阈值设为 80% ,给运维留出处理时间。
定位过程:找到"磁盘杀手"
root@prod-server:~# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 48G 2G 96% /
/dev/vdb1 200G 45G 155G 23% /data
tmpfs 7.8G 0 7.8G 0% /dev/shm
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
4.2G /var/log/nginx
1.1G /var/log/mysql
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
-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
🤔 找到了!日志未做轮转 。生产环境的日志必须配置:
① 按大小切割(如100MB一个文件)
② 按天滚动 + 自动压缩
③ 只保留最近N天(如7天/30天自动清理)
Spring Boot 默认 logback 配置是单文件无限制,必须自定义!
T+5min
止血
🩹 止血:紧急清理 + 长期修复
A
紧急清理(止血) — 先释放空间恢复服务
# 删除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 日志轮转
<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
扩容磁盘 — 云盘可以在线扩容:lsblk → growpart → resize2fs。
⚠️ 关键注意: 清理正在写入的日志文件,千万不要直接 rm !rm 删除文件但Java进程仍持有文件句柄,空间不会释放,且进程仍往"已删除"的文件写日志。必须用 truncate -s 0 清空内容,或 echo "" > file。要彻底解决需重启服务。
五、事故4:数据库连接池耗尽 — 接口雪崩
📋 事故现象
接口大面积超时,RT从50ms飙到30000ms
日志疯狂报错:HikariPool-1 - Connection is not available, request timed out after 30000ms
应用日志报:所有数据库操作都卡在获取连接这一步
但MySQL服务本身CPU/内存正常,没有慢查询(慢查询可能在另一边)
核心机制: 数据库连接池大小=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 — 谁占着连接不放?
🤔 真相大白: 20个连接全部被占住!其中2个在执行"导出报表"的大SQL(跑了1240秒还没结束!),18个处于Sleep状态——这是典型的长事务不提交 :代码开了事务但忘了close/commit,连接被挂起不归还。Time=1240s 说明这个查询跑了20分钟,肯定是在做全表扫描。
ReportExportService.java — 问题代码
📁 com.order
📁 service
📄 ReportExportService
1 @Service
2 public 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
[arthas@18432] trace javax.sql.DataSource getConnection -n 5 --condition '#cost > 1000'
`---p.CobarDataSource .getConnection() cost: 30.23s
+---HikariPool .getConnection() cost: 30.23s
| `---HikariPool .pool.borrow() cost: 30.22s
| `---HikariPool .waitForConnection() timeout!
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) — 壮士断腕
思路: 关闭非核心功能(评论、推荐、积分、消息推送),把所有资源让给核心交易链路(下单、支付)。
// 降级开关:通过配置中心动态控制,无需发版
@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) — 控制流量
思路: 当流量超过系统承载能力,主动拒绝多余请求,保住系统不崩溃。
// 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
// 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) — 最快恢复
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
手段5:扩容 (Scaling) — 加机器顶住
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
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
# 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(无指责复盘) ——对事不对人。如果一个人犯了错,那说明流程/系统有漏洞,让人容易犯错。
无指责复盘的核心原则:
① 假设所有参与者都尽了最大努力,做了正确的判断;
② 聚焦系统和流程 的问题,而不是个人的错误;
③ 每一个"人为失误"背后,都有一个"系统设计缺陷";
④ 目标是改进,不是惩罚。惩罚会让人隐瞒问题。
📌 事故概述
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-14 P0
在CI流程中加入正则安全检查(使用RE2) 架构组 06-20 P1
补充边界值测试用例(恶意输入、超长字符串) QA小陈 06-18 P1
更新Code Review Checklist,增加正则安全项 架构师刘 06-25 P2
优化发布流程:P0接口发版需灰度10%流量观察30min SRE 07-01 P1
报告人: 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分钟止血,达标。复盘会聚焦流程改进:为什么这个正则能上线?我们的防线哪里漏了? 不追责,看流程。
📱
@运营-李姐
客服侧确认,用户投诉已平息,补偿券已发。大家辛苦了,早点休息 🌙
↑