面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 通用排障 | 影响、止损、分层、证据、复盘 | 为什么先保现场、何时重启 |
| CPU 高 | 进程、线程、栈、代码热点 | GC 高还是业务计算高 |
| 线程池耗尽 | 活跃数、队列、阻塞点 | 扩大线程池为何可能更糟 |
| 内存上涨 | 堆/非堆/native、引用链 | 泄漏与正常缓存如何区分 |
| Full GC | 分配、晋升、回收效果 | 大对象、元空间、晋升失败 |
| 接口超时 | 分位延迟、Trace、池化资源 | 单实例/全局、重试放大 |
| 慢 SQL | 慢日志、计划、扫描、锁 | 参数不同为何计划不同 |
| Redis 延迟 | 慢命令、大 Key、fork、网络 | 单分片热点如何迁移 |
| MQ 积压 | 生产/消费速率、失败消息 | 临时扩容后的顺序和幂等 |
| 发布故障 | 变更对比、灰度、回滚 | 数据库变更无法回滚怎么办 |
| 死锁排查 | 线程 dump、锁顺序、等待图 | 死锁 vs 活锁、jstack 分析 |
排障题必须给出“下一条命令或下一张图看什么”,避免只说笼统的检查日志和监控。
一、通用排障方法
线上问题的通用排查流程?
难度 🔴
确认现象 → 判断影响 → 先止损 → 分层定位 → 验证根因 → 修复与复盘- 确认现象:时间范围、错误码、受影响接口、用户和机房。
- 判断影响:全部请求还是部分请求,单实例还是集群。
- 先止损:回滚、降级、限流、扩容、切流或关闭非核心功能。
- 分层定位:客户端、网关、应用、缓存、数据库、MQ、下游、基础设施。
- 验证根因:用日志、指标、Trace、复现或对照实验形成证据链。
- 修复复盘:区分临时恢复和长期治理。
排障时先保留现场,避免一开始就重启服务导致关键证据丢失。
如何快速判断问题在哪一层?
| 现象 | 优先检查 |
|---|---|
| 所有接口都慢 | CPU、GC、线程池、网络、公共下游 |
| 单接口慢 | Trace、业务日志、SQL、远程调用 |
| 单实例异常 | 实例日志、机器资源、配置差异 |
| 集群整体异常 | 流量、依赖服务、数据库、缓存 |
| 错误率突然上涨 | 发布变更、配置变更、下游故障 |
| 延迟上涨但错误率稳定 | 慢 SQL、锁竞争、连接池、资源饱和 |
排障为什么要同时看日志、指标和 Trace?
- 日志:解释具体请求发生了什么。
- 指标:判断问题规模、趋势和资源瓶颈。
- Trace:定位调用链中耗时或报错的节点。
三者结合才能从“哪里异常”走到“为什么异常”。
二、CPU 与线程问题
Java 进程 CPU 飙高怎么排查?
top 找进程
↓
top -H -p <pid> 找高 CPU 线程
↓
将线程 ID 转为十六进制
↓
jstack <pid> 中匹配 nid
↓
结合代码、火焰图和调用频率定位实战命令:
# 1. 找 CPU 最高的 Java 进程
top -c | grep java
# 2. 找进程中 CPU 最高的线程
top -H -p 12345
# 3. 线程 ID 转十六进制(jstack 用 nid 匹配)
printf "%x\n" 12345 # 输出 0x3039
# 4. 查看该线程的堆栈
jstack 12345 | grep -A 30 "0x3039"
# 输出示例:
# "http-nio-8080-exec-3" #27 daemon prio=5 os_prio=0 tid=0x...
# java.lang.Thread.State: RUNNABLE
# at com.example.MyService.process(MyService.java:42)
# at com.example.MyService.lambda$handle$0(MyService.java:35)
# at java.util.concurrent.ThreadPoolExecutor.runWorker(...)
# 锁定对象:0x00000007...(如显示 locked 说明在持锁等待)
# 5. 火焰图(快速发现热点)
# 生产环境用 async-profiler
./asprof -d 30 -f profile.html 12345常见原因:
- 死循环或高频空轮询。
- 正则表达式回溯。
- 大量序列化、压缩或加密计算。
- 频繁 GC。
- 锁竞争导致线程自旋。
- 流量突增或批处理任务同时执行。
线程池耗尽有什么表现?
- 活跃线程数达到最大值。
- 队列持续增长或拒绝任务。
- 接口延迟上涨,CPU 不一定很高。
- 大量线程停在远程调用、数据库或锁等待。
处理顺序:
- 找出线程在等待什么。
- 检查下游超时、连接池和慢 SQL。
- 必要时限流、降级或隔离故障依赖。
- 不要只通过扩大线程池掩盖阻塞问题。
如何排查死锁?
jstack 发现死锁:
# 执行 jstack,自动检测死锁
jstack 12345
# 输出中搜索 "deadlock"(不区分大小写)
jstack 12345 | grep -A 30 "deadlock"jstack 死锁输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000001e8c800 (object 0x00000007d5f6c8a0, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000001e8d600 (object 0x00000007d5f6c8e0, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads:
Thread-1:
at com.example.DeadlockExample.lambda$main$1(DeadlockExample.java:25)
- waiting to lock <0x00000007d5f6c8a0> (a java.lang.String)
- locked <0x00000007d5f6c8e0> (a java.lang.String)
Thread-0:
at com.example.DeadlockExample.lambda$main$0(DeadlockExample.java:15)
- waiting to lock <0x00000007d5f6c8e0> (a java.lang.String)
- locked <0x00000007d5f6c8a0> (a java.lang.String)
处置:
- 统一加锁顺序,缩小锁粒度
- 避免锁内执行网络调用或长耗时操作
- 必要时使用带超时的
tryLock
注意:死锁不一定每次都能复现,排查时要看线程 dump 中多个线程的 waiting to lock 是否构成循环。
三、内存与 GC
内存持续上涨怎么排查?
先区分:
- Java 堆。
- 元空间。
- 直接内存。
- 线程栈。
- native 内存或 mmap。
常用步骤:
- 查看堆、GC 和进程 RSS 趋势。
- 使用
jstat判断各代增长和 GC 频率。 - 使用
jmap或诊断工具导出堆快照。 - 用 MAT 分析大对象、引用链和支配树。
- 检查缓存、集合、ThreadLocal、监听器和类加载器。
频繁 Full GC 怎么排查?
第一步:看 GC 统计
# 每 5 秒输出一次 GC 统计(S0/S1/Eden/Old/Metaspace 用量)
jstat -gcutil 12345 5000
S0 S1 E O M YGC YGCT FGC FGCT GCT
0.00 50.00 30.00 95.00 85.00 120 3.200 15 10.50 13.70
# ↑ ↑
# Survivor 使用率 老年代 95% → 频繁 Full GC 的根因第二步:看 GC 日志确认触发原因
# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 11+
-Xlog:gc*:file=gc.log
# 日志中搜索 Full GC 触发原因
grep -E "Full GC|Metadata GC Threshold|Allocation Failure" gc.log
# Metadata GC Threshold → 元空间满了
# Allocation Failure → 堆空间不足
# Ergonomics → JVM 自适应策略触发常见原因:
- 老年代空间不足(晋升太快或泄漏)
- 大对象直接进入老年代
- 元空间不足(动态生成类太多)
- 堆设置不合理或晋升失败
不能只调大堆。应结合 GC 日志确认对象分配速率、晋升情况、停顿原因和回收效果。老年代水位在 Full GC 后:
- 能降回去 → 容量问题,扩堆有效
- 降不回去且持续上涨 → 内存泄漏,走 MAT 支配树分析
OOM 发生后要保留什么信息?
- OOM 类型和完整异常栈。
- 堆转储文件。
- GC 日志。
- 当时的流量、发布和配置变更。
- 进程内存、容器限制和系统日志。
建议提前配置 OOM 自动导出堆快照,但要预留磁盘空间并保护敏感数据。
四、接口超时与依赖故障
接口突然变慢怎么排查?
难度 🔴
- 查看延迟分位数、错误率和流量是否同时变化。
- 通过 Trace 拆分网关、应用、SQL、缓存和远程调用耗时。
- 检查线程池、连接池和队列是否饱和。
- 检查慢 SQL、锁等待、缓存未命中和下游超时。
- 核对近期发布、配置、数据量和流量变化。
重试为什么可能放大故障?
下游变慢时,如果每层都立即重试,会增加下游负载并占用更多线程,形成重试风暴。
治理方式:
- 设置明确的连接和读取超时。
- 只对可重试错误重试。
- 限制次数,采用指数退避和随机抖动。
- 保证写操作幂等。
- 配合熔断、限流和隔离。
连接池耗尽怎么处理?
排查:
- 活跃连接、空闲连接和等待线程数。
- 是否存在连接泄漏。
- SQL 是否变慢或事务持有连接过久。
- 数据库最大连接数是否达到上限。
连接池大小不是越大越好,过大会把压力转移到数据库。HikariCP 的连接数公式、参数体系与 maxLifetime 陷阱见MySQL。
五、数据库、缓存与消息队列
慢 SQL 的排查路径?
慢查询日志 → EXPLAIN → 扫描行数/索引 → 锁等待 → SQL 与表结构优化实战命令:
# 1. 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; # 超过 1 秒的 SQL 记录
SET GLOBAL log_queries_not_using_indexes = ON;
# 2. 查看慢查询日志
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log # 按时间排序前 10 条
# 3. EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY create_time DESC;
# 输出(关注以下字段):
# +----+-----------+-------+------+-----------------------------+
# | id | table | type | rows | Extra |
# +----+----------+-------+------+-----------------------------+
# | 1 | orders | ref | 10 | Using index condition; |
# | | | | | Using filesort |
# +----+----------+-------+------+-----------------------------+
# type != ALL 说明用了索引 ✓
# Extra 有 Using filesort → 需要优化排序重点检查:
- 是否命中合适索引
- 是否出现全表扫描、回表过多或临时表
- 返回数据是否过大
- 是否存在大事务和锁等待
- 数据分布变化是否导致执行计划变化
Redis 延迟突然上涨怎么排查?
- 慢命令和大 Key。
- 热 Key 导致单分片过载。
- 持久化 fork 或磁盘抖动。
- 网络和连接数。
- 集中过期或缓存穿透。
- 集群迁移、故障转移和客户端重试。
MQ 消息积压怎么处理?
- 判断生产速度、消费速度和积压增长趋势。
- 检查消费端错误、慢依赖、线程池和批量参数。
- 临时增加消费者或分区,但先确认顺序性和下游承载能力。
- 对失败消息隔离,避免单条坏消息阻塞。
- 消费端必须保证幂等,并监控消费延迟。
六、发布与复盘
发布后错误率上涨怎么办?
- 立即对比发布前后指标。
- 判断是否只影响新版本实例。
- 满足回滚条件时优先回滚。
- 检查数据库变更、配置兼容和缓存格式。
- 修复后重新灰度,不直接全量发布。
什么是有效告警?
有效告警应做到:
- 与用户影响或系统容量相关。
- 有明确阈值、持续时间和告警级别。
- 包含服务、实例、时间和排查入口。
- 能聚合重复告警,避免告警风暴。
- 有负责人和处理手册。
故障恢复后为什么还要复盘?
恢复只解决当前问题,复盘需要解决系统性缺陷:
- 监控为什么没有提前发现。
- 降级和回滚为什么没有自动生效。
- 测试和压测为什么没有覆盖。
- 相同模式是否存在于其他服务。