Production Troubleshooting

线上排查场景集
6个生产环境实战故事

真实的生产故障,完整的排查过程。从告警到定位到修复,每一步都有真实的命令、日志和代码。收藏它,下次线上出问题时不慌。

📝 6 个实战场景 🐛 CPU / 内存 / 死锁 / 超时 / MQ / 回滚 🔧 完整排查流程 ⏱️ 约 45 分钟阅读
🔥

Story 1: CPU 飙到 100%

下午3点,线上突然所有接口都超时了。用户疯狂投诉...

生产故障 · P0 级
2026-06-10 15:00 — 接口大面积超时
1.1 收到告警

钉钉群里突然弹出一条告警消息:

🚨 CPU告警:服务器 192.168.1.100 CPU使用率 99.2%,持续时间 5分钟
钉钉 — 运维告警群
🚨 运维告警群 8人在线
机器人
监控机器人
🚨 CPU 告警
服务器: 192.168.1.100
CPU使用率: 99.2%
持续时间: 5 分钟
告警级别: P0 - 紧急
张工
张工(运维)
收到,马上SSH上去看!
我(开发)
我来一起看,可能是代码问题
1.2 SSH 登服务器

第一时间 SSH 到告警服务器,用 top 看看谁在吃 CPU。

Terminal — ssh ops@192.168.1.100
$ ssh ops@192.168.1.100
Welcome to Ubuntu 22.04 LTS

$ top
top - 15:05:33 up 45 days, 3 users, load average: 32.15, 28.43, 15.21
  PID USER   %CPU %MEM  COMMAND
 8923 java    99.2 23.1  java -jar app.jar  ← 这个进程吃了99%!
 1234 mysql    3.1 45.2  mysqld
 5678 nginx    0.5  1.2  nginx: worker
Java进程吃了99%的CPU!找到PID了:8923。接下来看是哪个线程在作怪。
1.3 看线程 — 哪个线程吃的最多?

top -Hp 可以看到进程里的每个线程。

Terminal — top -Hp 8923
$ top -Hp 8923
top - 15:06:12 up 45 days, load average: 32.15, 28.43, 15.21
  PID USER   %CPU COMMAND
 9012 java    85.3  ← 这个线程吃的最多!
 9013 java    3.1
 9014 java    2.8
 9015 java    1.2
 8923 java    0.8
线程 PID 9012 吃了 85% 的 CPU。现在需要知道它在 Java 里对应什么。
1.4 jstack 线程 dump — 定位到代码行

Java 线程 ID 是十进制的,jstack 里是十六进制。先转换,再 grep。

Terminal — jstack 分析
$ jstack 8923 > thread_dump.txt
Heap dump created.

$ printf "%x\n" 9012
2324

$ grep -A 30 "2324" thread_dump.txt
"pool-3-thread-1" #45 prio=5 os_prio=0 tid=0x00007f8c34123450 nid=0x2324 runnable
  at com.energy.service.AlertCheckService.checkLoop(AlertCheckService.java:67)
  at com.energy.service.AlertCheckService.run(AlertCheckService.java:45)
  at java.lang.Thread.run(Thread.java:748)
找到了!AlertCheckService 第67行,一个 while(true) 循环没有 sleep!
1.5 看代码 — 死循环!

打开 IntelliJ,定位到 AlertCheckService.java:

IntelliJ IDEA — AlertCheckService.java
AlertCheckService.java
AlertMapper.java
📂 com.energy
📂 service
📄 AlertCheckService.java
📄 AlertMapper.java
📂 model
📄 Alert.java
60public void checkLoop() {
61    while (running) {
62        // 没有 sleep!死循环!
63        List<Alert> alerts = alertMapper.findPending();
64        for (Alert alert : alerts) {
65            processAlert(alert);
66        }
67        // 应该在这里加 Thread.sleep(5000);
68    }
69}

这个 while(running) 循环里没有任何等待,每秒执行几万次数据库查询。数据库返回空结果也要不了多久,但循环本身不停运转,CPU 就 100% 了。

1.6 修复 — 加上 sleep

在循环里加上 sleep,每5秒检查一次:

IntelliJ IDEA — AlertCheckService.java (修复后)
AlertCheckService.java
📂 com.energy
📂 service
📄 AlertCheckService.java
60public void checkLoop() {
61    while (running) {
62        List<Alert> alerts = alertMapper.findPending();
63        for (Alert alert : alerts) {
64            processAlert(alert);
65        }
66        try {
67            Thread.sleep(5000); // 5秒检查一次
68        } catch (InterruptedException e) {
69            Thread.currentThread().interrupt();
70            break;
71        }
72    }
73}
diff --git a/AlertCheckService.java b/AlertCheckService.java @@ -64,6 +64,11 @@         } +        try { +            Thread.sleep(5000); // 5秒检查一次 +        } catch (InterruptedException e) { +            Thread.currentThread().interrupt(); +            break; +        }     } }
1.7 验证 — CPU 下来了

发版后验证:

Terminal — top -Hp 8923 (修复后)
$ top -Hp 8923
top - 15:45:12 up 45 days, load average: 2.31, 3.45, 4.12
  PID USER   %CPU %MEM  COMMAND
 8923 java    3.2 23.1  java -jar app.jar  ← CPU下来了!
 9012 java    1.8
 9013 java    0.5
99.2%
修复前 CPU
3.2%
修复后 CPU
32.15
修复前 Load
2.31
修复后 Load
15:00
收到CPU告警,99.2%
15:05
SSH登服务器,top 定位到 Java 进程 PID 8923
15:08
top -Hp 找到线程 PID 9012(85.3% CPU)
15:12
jstack + printf "%x" 定位到 AlertCheckService.java:67
15:20
加 Thread.sleep(5000),发版验证,CPU 降到 3.2%

💾

Story 2: 内存泄漏

服务运行3天后越来越慢,GC也回收不了。每次重启只管3天...

性能劣化 · P1 级
2026-06-10 10:00 — 服务又变慢了(第3天)
📈 堆内存增长曲线 — 内存泄漏的"温水煮青蛙"
启动时
256MB
✅ 正常
运行1小时
512MB
⚠️ 上升
运行2小时
768MB
⚠️ 警告
运行3小时
1024MB — OOM! 💥
🔥 挂了!
💡 内存只涨不降,Full GC 也回收不了 → 典型内存泄漏特征。ThreadLocal 未清理导致对象一直被引用。
2.1 现象 — GC越来越频繁,回收不了

服务每次刚启动很快,但运行3天后就越来越慢。看 GC 日志:

Terminal — GC 日志分析
$ tail -100 /var/log/app/gc.log

[GC (Allocation Failure) [PSYoungGen: 655360K->655358K(655360K)] 1560120K->1560118K(2097152K), 0.9876543 secs]
[Full GC (Ergonomics) [PSYoungGen: 655358K->655358K(655360K)] 1560118K->1560118K(2097152K), 4.3210987 secs]
[GC (Allocation Failure) [PSYoungGen: 655360K->655356K(655360K)] 1560116K->1560114K(2097152K), 0.8765432 secs]
[Full GC (Ergonomics) [PSYoungGen: 655356K->655356K(655360K)] 1560114K->1560114K(2097152K), 5.1234567 secs]

# Full GC 越来越频繁,但回收不了任何内存!
# Young GC 后内存几乎没变:655360K -> 655358K
# Full GC 也回收不了:1560118K -> 1560118K
Full GC 越来越频繁,而且回收不了什么内存。这是内存泄漏的典型特征——对象被引用着,GC无法回收。
2.2 jmap dump — 堆快照

先用 jmap 把堆内存 dump 下来:

Terminal — jmap heap dump
$ jmap -dump:format=b,file=heap.hprof 8923
Dumping heap to /tmp/heap.hprof ...
Heap dump file created (823456789 bytes)

# 文件 785MB,用 MAT (Memory Analyzer Tool) 分析
# 下载到本地:scp ops@192.168.1.100:/tmp/heap.hprof ./
2.3 MAT 分析 — 找到泄漏嫌疑人

用 Eclipse MAT 打开 heap.hprof,查看 Leak Suspects:

Eclipse MAT — Leak Suspects Report
🔥 Problem Suspect 1:
java.util.concurrent.ConcurrentHashMap @ 0x7f8c34123450
├─ 856,321 items ← 这个Map无限增长!
├─ Retained Heap: 456 MB
├─ Key: com.energy.cache.DeviceCacheEntry
└─ At com.energy.service.DeviceCacheManager.deviceMap (DeviceCacheManager.java:23)

✅ Problem Suspect 2 (minor):
java.util.ArrayList @ 0x7f8c34098765
├─ 12,345 items
├─ Retained Heap: 23 MB
🔥 ConcurrentHashMap 有 856,321 个 Entry,占 456MB!而且一直在增长,永远不清理。
2.4 看代码 — 只加不删的 Map

打开 DeviceCacheManager.java:

IntelliJ IDEA — DeviceCacheManager.java
DeviceCacheManager.java
DeviceCacheEntry.java
📂 com.energy
📂 cache
📄 DeviceCacheManager.java
📄 DeviceCacheEntry.java
18@Component
19public class DeviceCacheManager {
20
21    // 问题:只往里加,从来不清理!
22    private Map<String, DeviceCacheEntry> deviceMap =
23        new ConcurrentHashMap<>();
24
25    public void updateCache(String deviceId, DeviceData data) {
26        deviceMap.put(deviceId, new DeviceCacheEntry(data));
27    }
28    // 没有 remove/evict 方法!
29}
找到了!ConcurrentHashMap 只 put 不 remove。每次有设备数据更新就往里塞,设备下线了也不清理。3天下来,85万个 Entry 堆在内存里。
2.5 修复 — 用 Guava Cache 加过期清理

替换成 Guava Cache,自动过期 + 最大容量限制:

IntelliJ IDEA — DeviceCacheManager.java (修复后)
DeviceCacheManager.java
📂 com.energy
📂 cache
📄 DeviceCacheManager.java
18@Component
19public class DeviceCacheManager {
20
21    private static final long EXPIRE_MINUTES = 30;
22    private Cache<String, DeviceCacheEntry> deviceCache =
23        CacheBuilder.newBuilder()
24            .expireAfterWrite(EXPIRE_MINUTES, TimeUnit.MINUTES)
25            .maximumSize(10000)
26            .build();
27}
diff --git a/DeviceCacheManager.java b/DeviceCacheManager.java @@ -20,8 +20,8 @@ - private Map<String, DeviceCacheEntry> deviceMap = - new ConcurrentHashMap<>(); + private static final long EXPIRE_MINUTES = 30; + private Cache<String, DeviceCacheEntry> deviceCache = + CacheBuilder.newBuilder() + .expireAfterWrite(EXPIRE_MINUTES, TimeUnit.MINUTES) + .maximumSize(10000) + .build();

关键改动:1) 30分钟自动过期,设备下线后缓存会自动清理;2) 最大10000条,防止无限增长;3) Guava Cache 线程安全,不需要 ConcurrentHashMap。


🔒

Story 3: 数据库死锁

几个用户同时操作,业务全部卡住了。数据库报"锁等待超时"。

数据库故障 · P0 级
2026-06-10 11:30 — 死锁告警
🔒 死锁环形等待图 — 循环依赖是死锁的本质
事务 A(placeOrder)
✅ 持有锁: id=1(订单行)
⏳ 等待锁: id=3(库存行)
循环等待!
事务 B(adjustStock)
✅ 持有锁: id=3(库存行)
⏳ 等待锁: id=1(订单行)
⚠️ A 等 B 释放锁,B 等 A 释放锁 → 永远等不到 → MySQL 只能回滚其中一个事务
✅ 解决方案:统一加锁顺序(都先锁库存再锁订单),就不会出现循环等待
3.1 现象 — 用户反馈操作失败
钉钉 — 业务支持群
📋 业务支持群 12人在线
客服
客服小李
好几个用户反馈操作失败,提示"锁等待超时" Lock wait timeout exceeded
运维
运维老赵
我看了数据库,有死锁!MySQL 自动检测到了
我(开发)
我来看看 INNODB STATUS,查死锁详情
3.2 查死锁 — INNODB STATUS

在 DataGrip 里执行 SHOW ENGINE INNODB STATUS

DataGrip — 192.168.1.100/energy_db
SHOW ENGINE INNODB STATUS;
LATEST DETECTED DEADLOCK

Transaction 1:
  HOLDING LOCK: t_order row id=1001 (X lock)
  WAITING FOR: t_stock row id=5001 (X lock)

Transaction 2:
  HOLDING LOCK: t_stock row id=5001 (X lock)
  WAITING FOR: t_order row id=1001 (X lock)

WE ROLL BACK TRANSACTION 2
经典死锁!事务1锁了订单等库存,事务2锁了库存等订单。互相等待,MySQL 只能回滚其中一个。

死锁的本质是"循环等待"。A等B,B等A。解决方案:让所有事务按相同顺序加锁,就不会出现循环等待。

3.3 看代码 — 加锁顺序不一致

两个方法操作同样的表,但加锁顺序不同:

IntelliJ IDEA — OrderService.java
OrderService.java
StockService.java
📂 com.energy
📂 service
📄 OrderService.java
📄 StockService.java
30// 方法1:先锁订单,再锁库存
31@Transactional
32public void placeOrder(Long orderId, Long stockId) {
33    orderMapper.selectForUpdate(orderId);    // 锁订单
34    stockMapper.updateStock(stockId);         // 锁库存
35}
36
37// 方法2:先锁库存,再锁订单(反了!)
38@Transactional
39public void adjustStock(Long stockId, Long orderId) {
40    stockMapper.updateStock(stockId);         // 锁库存
41    orderMapper.selectForUpdate(orderId);    // 锁订单
42}
placeOrder 先锁订单再锁库存,adjustStock 先锁库存再锁订单——顺序反了,死锁!
3.4 修复 — 统一加锁顺序

原则:所有事务都按相同顺序加锁,就不会死锁。 规定统一先锁库存再锁订单:

IntelliJ IDEA — OrderService.java (修复后)
OrderService.java
📂 com.energy
📂 service
📄 OrderService.java
30// 修复前:先锁订单再锁库存
31// @Transactional
32// public void placeOrder(Long orderId, Long stockId) {
33// orderMapper.selectForUpdate(orderId); // 锁订单
34// stockMapper.updateStock(stockId); // 锁库存
35// }
36
37// 修复后:统一先锁库存再锁订单
38@Transactional
39public void placeOrder(Long orderId, Long stockId) {
40    stockMapper.updateStock(stockId);         // 先锁库存
41    orderMapper.selectForUpdate(orderId);    // 再锁订单
42}
diff --git a/OrderService.java b/OrderService.java @@ -30,7 +30,7 @@ @Transactional public void placeOrder(Long orderId, Long stockId) { - orderMapper.selectForUpdate(orderId); // 锁订单 - stockMapper.updateStock(stockId); // 锁库存 + stockMapper.updateStock(stockId); // 先锁库存 + orderMapper.selectForUpdate(orderId); // 再锁订单 }
11:30
用户反馈操作失败,提示锁等待超时
11:35
SHOW ENGINE INNODB STATUS 发现死锁:T1等T2,T2等T1
11:42
看代码发现两个方法加锁顺序不一致
11:50
统一加锁顺序:先库存后订单,发版后死锁消失

⏱️

Story 4: 接口偶发超时

不是每次都慢,但偶尔就超时。排查了好几天,最终发现是缓存击穿。

性能问题 · P1 级
2026-06-10 09:00 — 又有用户反馈超时
4.1 现象 — P99 偶尔超慢

看 APM 监控,大部分请求都很快,但偶尔有超慢的:

APM 监控 — /api/devices 响应时间
Performance
Traces
Errors
/api/devices 响应时间分布
PercentileResponse TimeStatus
P50120ms正常
P95850ms偏慢
P995,200ms ← 偶尔超慢!超时
P50
120ms
P95
850ms
P99
5.2s!
P50 只要 120ms,但 P99 要 5.2 秒。说明大部分请求都很快,偶尔有几个特别慢。
4.2 排查过程 — 一步步缩小范围
1. 看应用日志
2. 看数据库慢查询
3. 发现规律
4. 定位缓存
Step 1
看应用日志 → 慢的请求没有报错,正常返回只是耗时长
Step 2
看数据库慢查询日志 → 偶尔出现慢查询,查询本身没优化空间
Step 3
发现规律 → 慢查询都发生在同一个时间点,而且都查同一张表
Step 4
联想到缓存 → 是不是缓存失效的瞬间,请求全打到数据库了?

查 Redis 缓存命中率:

Terminal — Redis 缓存分析
$ redis-cli info stats | grep keyspace
keyspace_hits:123456
keyspace_misses:8765

# 命中率 = 123456 / (123456 + 8765) = 93.4%
# 7% 的请求打到数据库了!
缓存命中率只有 93%,7% 的请求打到数据库了。看看到底是什么 key 失效。
4.3 发现缓存击穿

用 redis-cli monitor 观察,发现热点 key 过期时,大量请求同时打到数据库:

Terminal — redis-cli monitor
$ redis-cli monitor | grep "device:detail:1001"

1718000000.123 [0 10.0.0.5:54321] "get" "device:detail:1001" ← 缓存命中
1718000000.156 [0 10.0.0.5:54322] "get" "device:detail:1001" ← 缓存命中
1718000030.001 [0 10.0.0.5:54321] "get" "device:detail:1001" ← 过期了!miss
1718000030.003 [0 10.0.0.5:54323] "get" "device:detail:1001" ← miss
1718000030.005 [0 10.0.0.5:54324] "get" "device:detail:1001" ← miss
1718000030.007 [0 10.0.0.5:54325] "get" "device:detail:1001" ← miss
1718000030.009 [0 10.0.0.5:54326] "get" "device:detail:1001" ← miss
# 某个热点 key 过期了,大量请求同时打到 DB!

缓存击穿:热点 key 过期的瞬间,几百个并发请求同时发现缓存没了,全部去查数据库,数据库瞬间被打爆。这就是 P99 飙到 5 秒的原因!

4.4 修复 — 互斥锁重建缓存

只让一个请求去查数据库并重建缓存,其他请求等一下再重试:

IntelliJ IDEA — DeviceService.java (修复后)
DeviceService.java
📂 com.energy
📂 service
📄 DeviceService.java
50public DeviceVO getDeviceDetail(Long id) {
51    String key = "device:detail:" + id;
52    String cached = redisTemplate.opsForValue().get(key);
53    if (cached != null)
54        return JSON.parseObject(cached, DeviceVO.class);
55
56    // 互斥锁:只让一个请求去查DB
57    String lockKey = "lock:" + key;
58    if (redisTemplate.opsForValue().setIfAbsent(
59            lockKey, "1", 10, TimeUnit.SECONDS)) {
60        try {
61            DeviceVO result = deviceMapper.selectDetail(id);
62            redisTemplate.opsForValue().set(key,
63                JSON.toJSONString(result),
64                30, TimeUnit.MINUTES);
65            return result;
66        } finally {
67            redisTemplate.delete(lockKey);
68        }
69    }
70    // 其他请求等一下重试
71    Thread.sleep(100);
72    return getDeviceDetail(id);
73}
setIfAbsent 就是 Redis 的 SETNX 命令——只有第一个请求能获得锁去查 DB,其他请求 sleep 100ms 后重试(此时缓存已经重建好了)。
93%
修复前缓存命中率
99.8%
修复后缓存命中率
5.2s
修复前 P99
180ms
修复后 P99

📨

Story 5: 消息队列积压

RabbitMQ 消息堆积 50 万条,消费者处理不过来。订单一直在排队...

消息积压 · P1 级
2026-06-10 14:00 — MQ 监控告警
5.1 发现问题 — 消息堆积50万

打开 RabbitMQ Management UI,看到 order.process 队列的消息数:

RabbitMQ Management — Queues
Queue: order.process ALERT
order.process
523,841
523,841
Messages (堆积了!)
2
Consumers
500/s
Publish rate (in)
80/s
Deliver rate (out)
⚠️ 生产速度 (500/s) 远大于消费速度 (80/s)!消息一直在堆积!
消费速度 80/s 远远跟不上生产速度 500/s。消息越积越多,6小时了还没消化完。
5.2 查消费者日志 — 处理太慢

为什么消费速度只有 80/s?看消费者日志:

Terminal — 消费者日志
$ grep -i "slow" /var/log/app/app.log | tail -5

2026-03-20 14:30:22 WARN OrderConsumer - Processing took 12.5s for order ORD20260320001
2026-03-20 14:30:35 WARN OrderConsumer - Processing took 15.2s for order ORD20260320002
2026-03-20 14:30:50 WARN OrderConsumer - Processing took 11.8s for order ORD20260320003
2026-03-20 14:31:05 WARN OrderConsumer - Processing took 14.6s for order ORD20260320004
2026-03-20 14:31:18 WARN OrderConsumer - Processing took 13.1s for order ORD20260320005
每条消息处理要 12-15 秒!正常应该 100ms 以内。2个消费者,每秒只能处理约 80/12 ≈ 6.6 条。看代码。
5.3 看消费者代码 — 同步调外部 API

消费者里每条消息都同步调用一个外部 API,这个 API 很慢:

IntelliJ IDEA — OrderConsumer.java
OrderConsumer.java
📂 com.energy
📂 consumer
📄 OrderConsumer.java
25@RabbitListener(queues = "order.process")
26public void onMessage(OrderMessage msg) {
27    // 同步调用外部物流API — 每次要10-15秒!
28    LogisticsResult result = logisticsClient.createOrder(msg);
29    orderService.updateLogistics(msg.getOrderId(), result);
30}
logisticsClient.createOrder() 是同步调用,外部 API 响应慢,每条消息要等 12-15 秒。消费者就这样被阻塞了。
5.4 临时解决 — 增加消费者

先紧急增加消费者实例,提高消费速度:

Terminal — 扩容消费者
$ docker-compose up -d --scale consumer=5
Creating consumer_3 ... done
Creating consumer_4 ... done
Creating consumer_5 ... done

# 消费速度从 80/s 提升到约 200/s
# 但还是不够!根本问题:每条消息太慢

加消费者只是临时方案,治标不治本。根本解决要把同步调用改成异步,或者做批量处理。

5.5 永久修复 — 异步化 + 批量处理

把同步调用改成异步,消费者只做消息接收和入库,物流 API 调用改为异步:

IntelliJ IDEA — OrderConsumer.java (修复后)
OrderConsumer.java
📂 com.energy
📂 consumer
📄 OrderConsumer.java
25@RabbitListener(queues = "order.process")
26public void onMessage(OrderMessage msg) {
27    // 1. 先入库,状态设为"待处理"
28    orderService.savePending(msg);
29    // 2. 异步调用物流API(不阻塞消费者线程)
30    CompletableFuture.runAsync(() -> {
31        LogisticsResult result = logisticsClient.createOrder(msg);
32        orderService.updateLogistics(msg.getOrderId(), result);
33    }, asyncExecutor);
34}
diff --git a/OrderConsumer.java b/OrderConsumer.java @@ -25,6 +25,10 @@ @RabbitListener(queues = "order.process") public void onMessage(OrderMessage msg) { - LogisticsResult result = logisticsClient.createOrder(msg); - orderService.updateLogistics(msg.getOrderId(), result); + // 1. 先入库,状态设为"待处理" + orderService.savePending(msg); + // 2. 异步调用物流API + CompletableFuture.runAsync(() -> { + LogisticsResult result = logisticsClient.createOrder(msg); + orderService.updateLogistics(msg.getOrderId(), result); + }, asyncExecutor); }
80/s
修复前消费速度
3,200/s
修复后消费速度
523K
修复前积压
0
修复后积压
消费者只做入库(很快),物流API调用在独立线程池异步执行。消费速度从 80/s 提升到 3,200/s!

Story 6: 发布出问题→紧急回滚

新版本上线5分钟就发现数据错乱,紧急回滚!

线上故障 · P0 级
2026-06-10 16:00 — 新版本发布
6.1 发布 — v1.2.3 上线

下午4点,按计划发布新版本 v1.2.3:

Terminal — 部署 v1.2.3
$ docker-compose pull app
Pulling app (myshop-app:v1.2.3)...
Downloaded newer image for myshop-app:v1.2.3

$ docker-compose up -d app
Recreating myshop_app_1 ... done

$ curl -s http://localhost:8080/actuator/health
{"status":"UP","version":"1.2.3"}

# 发布完成!看起来一切正常...
6.2 发现问题 — 5分钟后
钉钉 — 紧急故障群
🚨 紧急故障群 6人在线
客服
客服小王 (2分钟后)
有客户说看到的订单数据不对,金额全变成0了!
测试
测试小李
我试了一下,确认金额显示0。订单列表接口返回的 amount 字段都是 0!
我(开发)
可能和刚才发布有关,我先回滚!
⚠️ 线上数据异常!金额全变0,影响所有用户。优先回滚恢复服务,再排查原因!
6.3 紧急回滚 — 回到 v1.2.2
Terminal — 紧急回滚
# 查看历史版本
$ docker images | grep myshop
myshop-app v1.2.3 a1b2c3d4e5f6 2 hours ago
myshop-app v1.2.2 f6e5d4c3b2a1 Yesterday ← 回滚到这个
myshop-app v1.2.1 e5d4c3b2a1f6 3 days ago

# 回滚
$ docker-compose down app
Stopping myshop_app_1 ... done

$ docker tag myshop-app:v1.2.2 myshop-app:latest

$ docker-compose up -d app
Creating myshop_app_1 ... done
回滚完成!从发现问题到回滚完成,只用了8分钟。关键:平时就保留历史版本镜像,回滚时才快。
6.4 验证回滚
Terminal — 验证
$ curl -s http://localhost:8080/actuator/health
{"status":"UP","version":"1.2.2"}

$ curl -s http://localhost:8080/api/orders/1001 | jq '.amount'
128.50

# 金额回来了!不是0了!服务恢复正常。
6.5 排查原因 — 哪个 commit 引入的 bug?

回滚后用户恢复了,但得找到 bug 根因,否则下次还会出问题。

Terminal — git log 分析
$ git log --oneline v1.2.2..v1.2.3
a1b2c3d fix: order amount display issue
e5f4g3h feat: add order discount
c7d8e9f refactor: order DTO mapping

# c7d8e9f 看起来是重构相关的,先看这个

$ git show c7d8e9f
commit c7d8e9f
Author: developer <dev@energy.com>
Date: Wed Jun 10

    refactor: order DTO mapping

    --- a/OrderDTO.java
    - private BigDecimal amount;
    + private BigDecimal discount;
        // 字段名改了,但前端还是读 amount!
找到了!c7d8e9f 这个 commit 在重构 DTO 时把 amount 字段改名为 discount,但前端和 API 返回还在读 amount。导致 amount 字段为 null,序列化成 JSON 时变成 0。
VS Code — OrderDTO.java (有 bug 的版本)
OrderDTO.java
Order.java
📂 src
📂 main
📂 java
📂 dto
📄 OrderDTO.java
📂 entity
📄 Order.java
public class OrderDTO {
    // BUG: amount 被改名为 discount,但前端还读 amount
    private BigDecimal amount;
    private BigDecimal discount; ← 新加的字段

    // amount 字段没了!前端读 amount = null → 序列化成 0
}
6.6 修复再发 — 加回 amount 字段

在 OrderDTO 里同时保留 amount 和 discount 两个字段:

VS Code — OrderDTO.java (修复后)
OrderDTO.java
📂 src
📂 main
📂 java
📂 dto
📄 OrderDTO.java
public class OrderDTO {
    private BigDecimal amount; ← 加回来了!
    private BigDecimal discount;

    // 金额 = 原价 - 折扣
    public BigDecimal getAmount() {
        return originalPrice.subtract(discount);
    }
}
diff --git a/OrderDTO.java b/OrderDTO.java @@ -3,6 +3,11 @@ + private BigDecimal amount; + private BigDecimal discount; + + // 金额 = 原价 - 折扣 + public BigDecimal getAmount() { + return originalPrice.subtract(discount); + } }

测试后重新部署:

Terminal — 重新部署 v1.2.4
# 运行测试
$ mvn test -Dtest=OrderDTOTest
Tests run: 5, Failures: 0, Errors: 0

# 构建新版本
$ docker build -t myshop-app:v1.2.4 .
Successfully built a1b2c3d4e5f7

# 部署
$ docker tag myshop-app:v1.2.4 myshop-app:latest
$ docker-compose up -d app
Recreating myshop_app_1 ... done

$ curl -s http://localhost:8080/api/orders/1001 | jq '.amount'
128.50

# 金额正确!v1.2.4 部署成功。
16:00
发布 v1.2.3
16:05
客服反馈金额全变成0
16:08
确认 bug,紧急回滚到 v1.2.2
16:13
回滚完成,服务恢复
16:30
git show 找到 bug commit c7d8e9f
17:00
修复代码,测试通过,部署 v1.2.4

📊

排查心法总结

6个实战故事的排查方法论,收藏它,下次出问题不慌

🔥 CPU 飙高

  • top 找进程 → top -Hp 找线程
  • jstack + printf "%x" 定位代码行
  • 常见原因:死循环、正则回溯、频繁GC
  • 修复:加 sleep、优化算法

💾 内存泄漏

  • GC 日志看 Full GC 频率和回收效果
  • jmap -dump 导出堆快照
  • MAT 分析 Leak Suspects
  • 常见原因:Map只加不删、监听器未注销

🔒 数据库死锁

  • SHOW ENGINE INNODB STATUS 查死锁
  • 看两个事务的锁等待关系
  • 常见原因:加锁顺序不一致
  • 修复:统一加锁顺序

⏱️ 偶发超时

  • APM 看 P99 分布,不要只看平均
  • 查缓存命中率(info stats)
  • 常见原因:缓存击穿/雪崩
  • 修复:互斥锁重建缓存

📨 MQ 积压

  • MQ Management UI 看消息数和消费速度
  • 查消费者日志,看处理耗时
  • 常见原因:同步调外部API
  • 临时:加消费者;根治:异步化

⏪ 发布回滚

  • 保留历史版本镜像,回滚才快
  • 先回滚恢复,再排查原因
  • git log 找 bug commit
  • 修复→测试→重新部署
排查通用 Checklist
1. 确认现象 — 看监控、看告警、看用户反馈。确认是"什么"出了问题。
2. 定位范围 — 是前端还是后端?是应用还是数据库?是网络还是服务?
3. 查日志 — 应用日志、GC日志、慢查询日志、Nginx访问日志。
4. 看指标 — CPU、内存、磁盘IO、网络IO、线程数、连接数。
5. 临时止血 — 回滚、扩容、限流、降级。先恢复服务。
6. 根因修复 — 找到代码问题,修复、测试、上线。
常用排查命令速查
排查命令速查表
# ===== CPU 问题 =====
top                    # 看整体CPU
top -Hp <PID>         # 看进程内线程
jstack <PID>           # 线程dump
printf "%x\n" <TID>     # 十进制转十六进制

# ===== 内存问题 =====
jmap -dump:format=b,file=heap.hprof <PID>  # 堆dump
jstat -gc <PID> 1000               # 实时GC统计

# ===== 数据库问题 =====
SHOW ENGINE INNODB STATUS  # 查死锁
SHOW PROCESSLIST          # 查当前连接
SELECT * FROM information_schema.INNODB_LOCKS  # 查锁

# ===== 缓存问题 =====
redis-cli info stats         # 缓存命中率
redis-cli monitor            # 实时监控命令

# ===== 部署回滚 =====
docker images | grep <app>  # 查历史版本
docker tag <old> <app>:latest  # 回滚版本
git log --oneline A..B       # 查版本间commit
🛠️

排查工具场景还原

用浏览器 DevTools、APM 等工具排查前端和网络问题的真实场景

浏览器端排查 — 接口返回 500

前端同事反馈页面加载失败,用 Chrome DevTools 看 Network:

Chrome DevTools — Network Tab
🔒 https://app.energy.com/devices
Network
Console
Elements
Sources
NameMethodStatusTimeSize
devicesGET200120ms15.2 KB
device-list.jsGET20045ms8.4 KB
/api/ordersGET5008,523ms128 B
logo.pngGET20032ms4.1 KB
Response: {"status":500,"message":"NullPointerException","trace":"at OrderService.getAmount(OrderService.java:45)"}
一眼看出 /api/orders 接口返回 500,耗时 8.5 秒,而且 Response 里有 NullPointerException 和代码行号。
数据库端排查 — 慢查询分析

在 DataGrip 里查慢查询日志,找到耗时最长的 SQL:

DataGrip — Slow Query Analysis
SELECT * FROM mysql.slow_log WHERE start_time > NOW() - INTERVAL 1 HOUR ORDER BY query_time DESC LIMIT 10;
10 rows returned in 0.023s
Query TimeRows ExaminedSQL
12.567s4,523,120SELECT * FROM t_order WHERE status = 0 AND create_time > '2026-01-01'
5.234s1,234,500SELECT * FROM t_order WHERE user_id = 1001 ORDER BY create_time DESC
2.123s567,800SELECT COUNT(*) FROM t_order_item WHERE order_id IN (...)
第一条 SQL 扫描了 450 万行!没有 status + create_time 的联合索引。加上索引后查询时间降到 0.05 秒。
DataGrip — 加索引
-- 加联合索引 ALTER TABLE t_order ADD INDEX idx_status_create_time (status, create_time);
✅ OK, 0 rows affected (3.456s)
APM 链路追踪 — 定位慢在哪一环

用 SkyWalking / Jaeger 看请求链路,一目了然:

SkyWalking — Trace Detail
Trace: /api/devices/list — 总耗时 3,256ms
API Gateway
45ms
DeviceService
856ms
Redis GET
3ms
MySQL SELECT
2,350ms ←!
Response
5ms
🔥 MySQL SELECT 耗时 2,350ms,占总时间的 72%!瓶颈在数据库查询。
🎓 最终心法

记住这三句话:

1. 先恢复,再排查 — 不要在线上调试,先回滚/降级恢复服务

2. 看监控,看日志 — 不要猜,让数据和日志告诉你发生了什么

3. 写事后复盘 — 每次故障都是学习机会,记录下来避免重犯

🔥 止血三板斧

  • 回滚 — 有镜像就能秒回
  • 扩容 — 加机器扛住流量
  • 降级 — 关非核心功能保主链路

🔍 排查三步曲

  • 确认现象 — 看监控看日志
  • 缩小范围 — 前后端、应用DB、网络
  • 定位根因 — jstack/jmap/EXPLAIN