第七章 线上故障排查与治理(第163-184题)
本章导览(元问题与表面主题逐字取自 《核心考点与元理论》§六)
- 本章真正考的那一个问题: 如何用可观测性把「系统坏了」收敛到「根因在哪」?
- 表面主题: CPU/OOM/GC/超时
- 题号与题量: 第 163–184 题,共 22 题;刷题时不要按知识点记,要按上面那一个元问题记。
163. 凌晨 Java 服务 CPU 100%,接口全超时(CPU 飙高排查)
【考察内容】CPU 飙高排查是线上问题第一高频题
【题目】凌晨线上 Java 服务 CPU 持续 100%,所有接口超时,告警响成一片。你作为值班工程师,怎么一步步定位到具体线程和代码行?给出完整排查命令链(top、top -Hp、jstack、日志核对)。
【参考答案】标准四步法:
- 定位进程:
top查看 CPU 占用最高的进程,确认是 Java 进程(记 PID); - 定位线程:
top -Hp <PID>查看该进程内 CPU 最高的线程,记下 TID; - 转十六进制:
printf "%x\n" <TID>得到十六进制 nid; - 定位代码:
jstack <PID> | grep -A 30 <nid>查看该线程堆栈——锁定具体类和方法;再回到日志核对(把命令链走完的第四步):拿栈顶类名/方法名去 grep 业务日志与 access 日志(grep -E '类名|方法名' app.log,或按 TraceID 反查这一批请求的入参与耗时),确认「是哪个接口、哪类入参」在烧 CPU——只有栈、没有日志,定位到方法也说不清是谁触发的,处置就没有抓手。 常见根因:
- 死循环/正则回溯(消耗型);
- 频繁 Full GC(GC 线程 CPU 高,配合 jstat -gcutil 看 GC 频率);
- 锁竞争/自旋(大量线程 BLOCKED 重试);
- 大量序列化/加解密(业务代码密集计算);
- 兜底:Arthas
thread -n 3直接列出最忙的 3 个线程(免手工换算)。
- 容量估算与阈值: 单实例按 4C8G 估算,业务线程池常见 200–500;若 CPU 持续 100% 且 P99>1s,按“每核可承载约 X QPS”反推——先用压测基线(如单核 500–2000 QPS,视接口复杂度)估算缺口。告警线:CPU>80% 持续 1–3 分钟;单线程>80% 一核连续 2 个采样周期即可疑死循环。jstack/jmap 现场保留窗口建议 ≥15 分钟流量特征。
- 失败与降级: 定位期间先止血:网关限流/摘流(摘流量、但保留进程与现场:留 1 台不重启以便取 jstack/dump,可它必须先摘出负载均衡——该机 CPU 已 100% 还挂在 LB 后面,1/N 用户会继续超时;「保留现场」不等于「保留流量」)→ 非核心接口降级返回兜底 → 可回滚则先回滚。若 dump/jstack 本身导致更卡,改用 Arthas 低频采样或
kill -3输出到文件。止血失败且影响面扩大:启动预案切换只读/排队页,并行拉内核/DBA。
【原理溯源】
为什么要 top → top -Hp 两层?
top只看到进程级 CPU,Java 进程可能有几百线程,必须下钻到线程。top -Hp把该 PID 下每个线程(LWP)的 CPU 占用拆开,找到真正烧 CPU 的那一个(或几个)。为什么 TID 要转十六进制? Linux 内核用十进制 LWP 标识线程,HotSpot 的
jstack输出里 nid(native thread id)是十六进制。不转换就 grep 不到。printf "%x\n"就是做这个映射。jstack 堆栈里在看什么? 看两件事:① 线程状态(RUNNABLE=在执行;BLOCKED=等锁;WAITING=等资源);② 栈顶方法——RUNNABLE 且栈顶落在业务代码/正则/序列化,就是计算型热点;栈顶落在
Object.wait/ 连接池,是等待型(CPU 通常不高)。GC 型 CPU 高和代码型怎么区分? 代码型:业务线程 RUNNABLE 占 CPU;GC 型:
GC task thread/G1 Conc/G1 Young等 GC 线程占 CPU,且jstat -gcutil显示 YGC/FGC 频率异常。两者处置完全不同——前者改代码,后者看内存/GC(见 165 题)。为什么正则会烧 CPU? 典型是灾难性回溯(backtracking):嵌套量词如
(a+)+在不匹配时指数级尝试。输入稍长就把单核打满。定位到Pattern.matcher栈顶即可确认。为什么 GC 安全点会让 CPU 高、jstack 却抓不到业务热点线程? STW 之前所有线程要先跑到 safepoint 才能停:线程数越多、syscall 越密,这段同步本身就烧 CPU。此时
top看进程很高,jstack里业务线程却都不是 RUNNABLE——判据是去看GC task thread/G1 Conc的占用与jstat -gcutil的 FGC 频率,而不是找栈顶代码行。Arthas
thread -n 3为什么快? 它直接读线程 CPU 时间并排序,免去 top 转十六进制再 grep jstack 的手工链路,且不用重启、可在线 attach。
【选型判断树】
CPU 100%,先分清"谁在烧":
├─ GC 线程占 CPU(jstat FGC/YGC 频繁)
│ → 走 Full GC 排查(165 题):回收效果差=泄漏;分配过快=调新生代
├─ 单个业务线程 RUNNABLE 占满一核
│ → 死循环 / 正则回溯 / 密集计算
│ → jstack 定位栈顶代码行
├─ 多个业务线程 RUNNABLE 且互相 BLOCKED
│ → 锁竞争 / 自旋重试
│ → 看锁持有者,减锁粒度或改无锁结构
└─ 大量线程 WAITING 但 CPU 仍高
→ 可能是大量短任务切换 / 序列化
→ 抽样多次 jstack,看栈顶是否聚在同一方法判断口诀: 先分 GC 还是业务;业务里再分单线程死循环 vs 多线程锁竞争。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “CPU 100% 排查核心是三层下钻:进程→线程→代码行” |
| 0:30–1:30 | 命令链 | top → top -Hp → printf %x → jstack grep nid → 回到日志核对(栈顶类名 grep 业务/access 日志或按 TraceID 反查入参),每步一句为什么 |
| 1:30–2:30 | 堆栈怎么读 | RUNNABLE/ BLOCKED/ WAITING 分别指向计算/锁/等待 |
| 2:30–3:30 | 根因分类 | 死循环、GC、锁竞争、序列化——各自怎么确认 |
| 3:30–4:30 | 工具兜底 | Arthas thread -n;不重启在线诊断 |
| 4:30–5:00 | 收尾 | “先摘流量止血、留 1 台不重启保现场,取完 jstack/dump 再重启恢复” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| top 采样 | 默认 3s 刷新,可用 top -d 1 | 突发问题用 1s |
| printf 转换 | printf "%x\n" <TID> | 十进制→十六进制对应 jstack 的 nid |
| jstack 采样 | 连续 2–3 次,间隔 5–10s | 区分瞬时热点与持续热点 |
| 危险线程占比 | 单线程持续 >80% 一核 | 典型死循环 |
| Arthas thread -n | 直接出 Top N 忙线程 | 免手工换算 |
| 正则回溯 | 输入 >1KB 时可能秒级到分钟级 | 灾难性回溯 |
【追问链】(三层)
L1|“top 里 CPU 高但 jstack 看不到对应线程,为什么?” → 可能是:① GC 线程(名字不是业务线程,要搜 GC/G1);② 线程已结束/状态切换,jstack 与 top 不是同一瞬间;③ 内核态 CPU(如频繁 GC 的安全点、syscall)。多次采样 + 看 GC 线程 + jstat 交叉验证。
L2|“为什么不能只重启?” → 重启只恢复可用,现场没了,根因还在。正确顺序:先摘流量/限流止血 → 保留现场(jstack、jstat、日志、必要时 dump)→ 再重启恢复 → 事后分析。生产应配 HeapDumpOnOutOfMemoryError 和持续线程采样。
L3|“Arthas 和 jstack 比,优势在哪、风险在哪?” → 优势:在线 attach、不用重启;thread -n/-b 直接排序;watch/trace 动态看入参和耗时;jad 反编译确认代码版本。风险:attach 本身有短暂开销;watch 表达式写错可能影响性能;严格环境需审批。jstack 更“轻”,适合快速第一眼。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 能说出 top → jstack,知道要找线程 |
| 80 分 | 完整命令链 + 十六进制转换原因;能区分代码型 / GC 型 / 锁型 |
| 95 分 | 能读懂堆栈状态与栈顶含义;说出正则回溯、安全点等机制;给出 Arthas 替代路径;有“先止血留现场再重启”的纪律 |
【关联题】
- 同一知识簇: 第 165 题(Full GC)、第 167 题(内存泄漏)、第 168 题(死锁)、第 172 题(服务 Hang)、第 181 题(内存上涨)
- 工具延伸: Arthas thread / watch / trace
- 场景呼应: 第 225 题(生产库 CPU 100%)
【自测】
top -Hp拿到的 TID 是 12345,jstack里该搜什么? 参考答案:搜十六进制3039(12345 的 hex),即 nid。- 判断:CPU 100% 一定是死循环。 参考答案:错。还可能是频繁 Full GC、锁自旋、密集序列化/加解密、正则回溯等。
- 如何快速区分“业务线程烧 CPU”和“GC 线程烧 CPU”? 参考答案:jstack 里看线程名是否为 GC task thread / G1 等;同时用 jstat 看 YGC/FGC 频率。GC 型要走内存/GC 排查,不是改业务代码。
164. 服务报 OutOfMemoryError,堆被什么东西占满了(OOM 排查)
【考察内容】OOM 排查是线上问题必考题
【题目】线上服务频繁报 OutOfMemoryError,重启后过几小时又 OOM。怎么确认是堆溢出、还是元空间/直接内存溢出?怎么拿到并分析堆 dump,定位是哪些对象、哪段代码在占用内存?
【参考答案】
应急:先保留现场(jmap -dump:format=b,file=heap.hprof <PID> 或配置 -XX:+HeapDumpOnOutOfMemoryError 自动 dump)→ 重启恢复服务(先保可用);
分析 dump:用 MAT/Eclipse MAT 或 jvisualvm 打开:
- Leak Suspects(泄漏嫌疑):自动分析最大的对象与 GC Roots 引用链;
- 按 Retained Heap(保留集)排序,看大对象;
- 对比多个 dump(不同时间点):找出持续增长的对象(泄漏特征);
常见泄漏点:
- 静态集合持有对象不放(static Map/List 当缓存);
- 未关闭的资源(连接、流、线程池);
- ThreadLocal 未 remove(线程池复用线程,值不释放);
- 监听器/回调未反注册;
- 大对象缓存无淘汰策略;
区分泄漏 vs 峰值:
- 内存泄漏:dump 之间对象持续增长,GC 后仍不下降;
- 内存峰值:某次大请求/大任务短时占满(排查大集合、批处理),GC 后能恢复;
配置预防:-Xmx 合理设置、GC 日志、内存监控告警(堆使用率 80% 告警)。
容量估算: 堆经验值:4C8G 服务
-Xmx常取物理内存 50–70%(8G 即 4–5.6G,实践常配 4–5G),元空间 256–512MB;堆 dump 只装活跃对象,上界不会超过 -Xmx(8G 堆实践常见 6–8G,含少量元数据)。告警:老年代使用率>80% 或 Full GC 后仍>70%。对象增长速率:对比两次jmap -histo(间隔 5–10 分钟),某类实例数/字节数线性上涨即可判定泄漏嫌疑。失败与降级: OOM 高发实例先摘流再 dump;dump 失败/超时则用不带
:live的jmap -histo粗定位——:live会先触发一次 Full GC 来统计存活对象,堆正要耗尽时这一步最可能失败、也会把服务彻底拖死;只有堆尚有余量、想近似看 live 集时才用:live。另外 dump 前先df -h校验目标分区剩余空间(本条自估 6–8G,与日志同盘时会把盘写满、制造二次故障),dump 完核对文件大小并 gzip 校验。业务侧降级:关掉大导出/大报表/大缓存构建;必要时临时-Xmx扩容争取时间,但必须同步排泄漏。若 DirectBuffer/Metaspace 型,堆 dump 无用——切 NMT/类加载排查,避免空转。
【原理溯源】
OOM 有几种,为什么要先分类型? 异常信息不同,根因完全不同:
Java heap space:堆内对象太多/太大;Metaspace:类加载过多(动态代理、热部署、重复 ClassLoader);Direct buffer memory:NIO/Netty 直接内存未释放;unable to create new native thread:线程数超限(与堆无关,是 OS/ulimit)。 不分类型就 dump 堆,可能完全找错方向。
为什么 dump 要“先留再重启”? OOM 现场只存在于当前进程内存。重启后对象图消失。
-XX:+HeapDumpOnOutOfMemoryError在 OOM 瞬间自动 dump,是最省事的保现场手段。注意 dump 会 STW,大堆可能停数秒到数十秒——可接受,因为反正要挂了。MAT 的 Retained Heap 是什么? 某对象的保留集 = 若该对象被 GC,能一并释放的内存总和(独占支配的子图)。比 Shallow Heap(对象自身大小)有用得多——静态 Map 本身不大,但 Retained 可能是几个 G。
Path to GC Roots 为什么要 exclude weak/soft? 弱/软引用在内存紧张时会被回收,不构成“泄漏持有”。真正要找的是强引用链:静态字段、ThreadLocal value、未关闭资源。排除弱软后剩下的路径才是必须改代码的持有者。
为什么 ThreadLocal 会泄漏? ThreadLocalMap 的 key 是对 ThreadLocal 的弱引用,value 是强引用。线程池线程长期复用不销毁 → ThreadLocal 对象被 GC 后 key 变 null,但 value 仍挂在 map 里 → 越积越多。根治是用完
remove()。
【选型判断树】
报 OOM,先读异常类型:
├─ Java heap space
│ ├─ 重启后几小时又满 → 疑似泄漏 → 自动 dump + MAT 支配树
│ └─ 特定请求后突然满 → 峰值/大对象 → 查批处理、大查询、大集合
├─ Metaspace
│ → 排查动态代理/热部署/重复 ClassLoader;调 MaxMetaspaceSize 只是缓解
├─ Direct buffer memory
│ → NMT + 排查 Netty/ByteBuffer 未 release;调 MaxDirectMemorySize
└─ unable to create native thread
→ 查线程数暴涨原因(线程池配置、每请求一线程);调 ulimit 是下策判断口诀: 先看异常字符串分类型,再决定 dump 堆还是查元空间/直接内存/线程。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “OOM 排查第一步是分类型,第二步是保现场,第三步才是分析” |
| 0:30–1:30 | 分类型 | heap / Metaspace / Direct / native thread 四类 |
| 1:30–2:30 | 保现场 | HeapDumpOnOutOfMemoryError + jmap;先 dump 再重启 |
| 2:30–3:30 | MAT 分析 | Leak Suspects → Retained Heap → Path to GC Roots |
| 3:30–4:30 | 典型泄漏 | 静态集合、ThreadLocal、未关闭资源、无界缓存 |
| 4:30–5:00 | 收尾 | “泄漏 vs 峰值看 GC 后是否回落、多 dump 是否持续增长” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 自动 dump | -XX:+HeapDumpOnOutOfMemoryError | OOM 瞬间自动 dump |
| dump 命令 | OOM 场景用 jmap -dump:format=b,file=heap.hprof <PID>(不带 live);堆尚有余量才考虑 :live | :live 会先触发一次 Full GC,堆正要耗尽时最可能失败并拖死服务;dump 前先 df -h 校验目标分区 |
| dump 耗时 | 小堆秒级;大堆(>8G)可能数十秒 | 会 STW |
| 告警线 | 堆使用率持续 >80% | 提前介入,别等 OOM |
| MAT 关键视图 | Leak Suspects / Dominator Tree / Histogram | 对比多份 dump |
| ThreadLocal 修复 | finally 中 remove() | 线程池场景必做 |
【追问链】(三层)
L1|“Retained Heap 和 Shallow Heap 差在哪?” → Shallow 是对象头+字段本身大小;Retained 是该对象被释放后能连带回收的全部内存。找泄漏看 Retained——一个静态 Map 的 Shallow 可能只有几 KB,Retained 却可能是整块缓存的几个 G。
L2|“为什么不直接调大 -Xmx?” → 若是泄漏,调大只是推迟 OOM,且更大的堆会让单次 GC 标记范围更大、STW 可能更长。必须先定位谁在持有引用。若是峰值且业务合理,才考虑扩容堆或优化单次加载量。
L3|“dump 文件几个 G,MAT 打不开怎么办?” → ① 用服务器上的 jmap -histo(或 jcmd <PID> GC.class_histogram)先看对象计数粗定位(jhat 自 JDK 9 已移除,且打不开 GB 级 hprof);② MAT 配置大内存(MemoryAnalyzer.ini 改 -Xmx);③ 用 jmap -histo(不带 live;:live 会先 Full GC,堆紧时不可靠)看 Top 类;④ 云上可传对象存储后在分析机打开。生产更推荐自动 dump + 离线分析流水线。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 jmap dump、MAT 打开 |
| 80 分 | 先分 OOM 类型;知道先保现场再重启;能说 Leak Suspects / Retained Heap |
| 95 分 | 讲清 ThreadLocal 弱引用 key 强引用 value 机制;用多 dump 对比区分泄漏与峰值;知道 Path to GC Roots 排除弱软;有自动 dump 与告警预防 |
【关联题】
- 同一知识簇: 第 165 题(Full GC)、第 167 题(内存泄漏定位)、第 181 题(内存上涨不 OOM)
- 对比: 直接内存泄漏看 NMT;元空间看类加载
- 预防: 缓存用 Caffeine/Guava 自动淘汰(缓存章)
【自测】
- OOM 信息是
Metaspace,该 dump 堆吗? 参考答案:堆 dump 帮助有限。应排查类加载(动态代理/热部署/重复 ClassLoader),并确认 MaxMetaspaceSize。 - ThreadLocal 泄漏的根因一句话? 参考答案:线程池复用线程不销毁,ThreadLocalMap 的 key 弱引用被 GC 后 value 仍强引用滞留,不 remove 就堆积。
- 判断:OOM 后先分析再重启,以免现场丢失。 参考答案:顺序反了。应先自动/手动 dump 保留现场,再重启恢复服务;分析可以离线做。
165. 监控显示 Full GC 每 2 分钟一次,RT 飙升(Full GC 排查)
【考察内容】GC 排查是 JVM 调优高频题
【题目】监控告警:Full GC 每 2 分钟一次,接口 RT 从 50ms 飙到 5s。Full GC 为什么导致 RT 飙升(STW)?怎么定位是内存泄漏、大对象、还是参数配置问题?从 GC 日志和堆分析怎么看?
【参考答案】
确认现象:
jstat -gcutil <PID> 1000观察 FGC 次数与 FGCT,确认 Full GC 频率与耗时;分析原因(Full GC 通常是老年代/元空间满了,或大对象直接进老年代):
- 对象分配过快(大量请求创建临时对象,晋升到老年代)——查是否有大流量 / 循环创建大对象;
- 内存泄漏(老年代对象只增不减)——dump + MAT 分析(见第 164 题);
- 大对象 / 缓存:缓存无界(本地缓存放太多)、大数组 / 大 List 直接进老年代;
- 元空间满(类加载过多,
-XX:MaxMetaspaceSize); - 晋升阈值问题(对象过早晋升);
调优手段:
- 定位后优先修代码(泄漏 / 大对象 / 缓存淘汰),调参是辅助;
- 调整堆大小(
-Xmx/-Xms一致)、新生代比例(-Xmn/-XX:NewRatio)、晋升阈值(-XX:MaxTenuringThreshold); - 换 GC 策略(G1 调
-XX:MaxGCPauseMillis;大堆用 ZGC 低停顿); - 排查外部因素:慢 SQL 导致连接持有大结果集、批量任务并发;
监控:GC 日志(
-Xlog:gc*)、GC 耗时与 RT 曲线关联。容量估算: Full GC 目标频率:健康系统 FGC 应接近 0(G1/CMS 日均个位数或更低);每 2 分钟一次属于严重异常。STW 预算:核心交易接口 P99 目标常 <200ms,单次 FGC STW>500ms 即直接打爆超时。新生代/老年代经验:对象多为短命时新生代可占堆 1/3–1/2;若 Young GC 频率>1 次/秒且 STW 可感知,先看分配速率。
失败与降级: GC 风暴中优先限流降 QPS,减少分配压力;关闭非核心异步任务/大批量 Job。确认泄漏后热修无法立即上时:临时扩堆+提前摘流发布。若为 CMS 碎片(或 G1 晋升失败)导致 Full GC,可临时切 G1/ZGC——但这不是「应急动作」:换收集器要改 JVM 参数并滚动重启,GC 风暴里重启等于把止血做成一次发布,止血窗口反而被拉长;应急仍按「限流降 QPS+摘流+(能不重启就)临时扩堆」,切 GC 归入当晚变更窗口的根治项并配回滚预案(需评估升级窗口),并保留 GC 日志供事后调优。
【原理溯源】
为什么 Full GC 会导致 RT 飙升? Full GC 多数收集器是 STW(Stop-The-World):GC 期间所有应用线程暂停。RT = 排队 + 处理 + GC 暂停。一次 1–2 秒的 Full GC,意味着这段时间内所有请求都被冻结,P99 直接被打到秒级。
为什么“频繁”比“单次耗时长”更危险? 每 2 分钟一次 × 2 秒 = 每小时停 60 秒,可用性直接损失约 1.7%。而且频繁 Full GC 说明老年代回收效果差(对象大量存活),通常预示内存泄漏或堆配置不合理。
为什么“调参不能根治”? 参数只能改变 GC 触发的时机和代价,改不了“对象是否该存活”。如果是内存泄漏(引用没释放),堆再大也只是把 OOM 推迟。所以必须先用堆快照定位到“谁在持有引用”。
为什么
jstat是第一眼工具? 它开销极低(读的是 JVM 内部计数器,不触发 GC),可以在生产环境持续观察,先确认“是否真频繁、单次多久、回收效果如何”,再决定是否要 dump(dump 会 STW,代价高)。
【选型判断树】
Full GC 频繁,先分类根因(关键看“回收效果”):
├─ Full GC 后老年代占用明显下降(如降 50%+)
│ → 不是泄漏,是“对象分配 / 晋升过快”
│ → 手段:调大新生代、调晋升阈值、减少大对象、降低分配速率
├─ Full GC 后老年代几乎不降(< 10%)
│ → 内存泄漏特征
│ → 手段:dump + MAT 支配树找持有者 → 改代码(根治)
├─ 元空间满(Metaspace OOM)
│ → 类加载过多(动态代理 / 热部署)
│ → 手段:调 MaxMetaspaceSize + 排查重复类加载
└─ 大对象直接进老年代
→ 大数组 / 大 List / 无界缓存
→ 手段:拆大对象、给缓存设上限与淘汰策略【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “Full GC 问题的排查路径是:先用 jstat 确认现象 → 再用 GC 日志和堆快照定位根因 → 最后区分是代码问题还是参数问题” |
| 0:30–1:30 | 讲清危害 | STW 导致 RT 飙升;用“2 分钟一次 × 2 秒 = 每小时停 60 秒”量化影响 |
| 1:30–3:00 | 根因分类 | 分配过快 / 内存泄漏 / 大对象 / 元空间,每类给判断依据 |
| 3:00–4:30 | 处置 | 代码优先、调参其次;G1/ZGC 选型依据 |
| 4:30–5:00 | 收尾 | “一句话:Full GC 是症状不是病因,必须找到是哪个对象在制造压力” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 观察命令 | jstat -gcutil <PID> 1000 | 每秒一次,开销极低 |
| 健康线 | Full GC 频率 < 每天几次;单次 STW < 200ms | 超过需关注 |
| 危险线 | Full GC ≥ 每 5 分钟 1 次,或单次 STW > 1s(核心交易接口按第 5 条取 >500ms 就算打爆 P99 预算,两条是「通用处置线」与「接口打爆线」两个口径) | 需立即处置(题干“每 2 分钟一次”已远越过这条线,属严重异常) |
| 堆 dump | jmap -dump:format=b,file=heap.hprof <PID>(:live 仅堆有余量时用) | 会 STW,大堆可能数秒;先摘流再 dump |
| 推荐配置 | -XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump,无需人工介入 |
| 新生代比例 | -XX:NewRatio 默认 2(老:新 = 2:1) | 或用 -Xmn 直接指定 |
| G1 目标停顿 | -XX:MaxGCPauseMillis=200 | 默认 200ms |
| 大堆建议 | > 16GB 考虑 ZGC | 停顿 < 10ms |
【追问链】(三层)
L1|“Full GC 后内存只降了 5%,说明什么?” → 说明绝大部分对象都还活着,是内存泄漏的典型特征。下一步必须 dump 堆快照,用 MAT 的支配树(Dominator Tree)找到持有大量对象的根引用,再回到代码定位。
L2|“G1 和 CMS 有什么区别?该怎么选?” → G1 是分区(Region)化的,可预测停顿(按 MaxGCPauseMillis 目标挑选回收 Region),且是标记-整理,不会像 CMS 那样碎片化。CMS 是并发标记-清除,有碎片问题,且 JDK 14 已移除。新项目默认 G1;大堆(>16G)且停顿敏感用 ZGC。
L3|“线上真出 Full GC,你按什么顺序操作?” → ① jstat 确认频率与耗时(低开销);② 看 GC 日志确认是哪种 GC、回收效果;③ 如果回收效果差 → 低峰期 dump + MAT 分析(或开 HeapDumpOnOutOfMemoryError 等它自己 dump);④ 定位到代码后先热修复 / 降级止血;⑤ 根治 + 加 GC 监控告警。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 Full GC 会 STW 导致 RT 飙升,能说出 jstat |
| 80 分 | 能分类根因(分配过快 / 泄漏 / 大对象 / 元空间)并给出对应手段;知道“调参不能根治泄漏” |
| 95 分 | 能用“回收效果”区分泄漏与分配过快;说出 MAT 支配树这个具体工具;给出 G1/CMS/ZGC 的选型依据与参数;有“先低开销观察、再高代价 dump”的顺序意识 |
【关联题】
- 同一知识簇(JVM 线上排查): 第 163 题(CPU 100%)、第 164 题(OOM)、第 167 题(内存泄漏)、第 169 题(线程池打满)、第 172 题(服务 Hang)、第 181 题(内存上涨排查)
- 工具延伸: 第 168 题(死锁排查,jstack)
- 国企 / 金融版: 第 223 题(现场分析 GC 日志)、第 225 题(生产库 CPU 100% 应急)
【自测】
jstat和jmap的开销差别在哪?生产上先用哪个? 参考答案: jstat 读 JVM 内部计数器,开销极低、不触发 GC;jmap dump 会 STW,大堆可能停顿数秒。所以先用 jstat 确认现象,再决定是否需要 dump。- 判断:把
-Xmx调大就能解决 Full GC 频繁。 参考答案: 错。若根因是内存泄漏,调大堆只是推迟 OOM;若根因是分配过快,调大堆可能略缓解但会增加单次 GC 耗时(标记扫描范围更大)。 - Full GC 每 2 分钟一次、每次 2 秒,可用性损失多少? 参考答案: 每小时停 60 秒,可用性损失约 1.7%;若按 99.99% 目标(全年 52.6 分钟),这个频率一天就吃掉 24 分钟、2.2 天耗尽全年预算(当天可用性只有 98.33%)。
166. 平时 100ms 的接口,现在 60% 请求超过 1s(接口超时排查)
【考察内容】接口超时是全链路排查最高频题
【题目】某核心接口平时 P50 100ms,今天开始 60% 的请求超过 1s,但服务没重启、也没发版。可能的原因有哪些(依赖变慢、DB 慢查询、GC、资源竞争、网络)?你按什么顺序排查、用什么工具?
【参考答案】按“现象确认→分层定位→根因→修复”:
确认范围:单接口还是全站?(全站=基础设施问题);超时分布(P50/P99);是否有发布/流量变化时间点;
调用链定位:SkyWalking/Zipkin 看请求在哪个环节耗时最长(自身逻辑/DB/Redis/下游服务/MQ);
分层排查:
- 应用层:线程池是否打满(等待线程池队列=并发超限)、锁等待(jstack 看 BLOCKED)、GC 停顿;
- 中间件:Redis 慢查询/大 key、连接池耗尽;
- 数据库:慢 SQL(慢日志+EXPLAIN)、锁等待(processlist 看 Waiting);
- 下游:依赖服务 RT 飙升(调用链看下游耗时);
- 机器:CPU/内存/磁盘 IO/带宽/网络(丢包、TCP 重传);
常见根因:慢 SQL、下游变慢、线程池/连接池打满、GC、热点 key、网络;
修复:对应优化+压测验证+监控告警(RT 阈值告警,防再次发生)。
容量估算: 先画延迟预算:网关→应用→缓存→DB→下游,假设正常 100ms=网关5+应用20+缓存5+DB50+下游20。60% 请求>1s 说明是共享资源排队(DB/连接池/线程池/下游)而非单接口逻辑突变。超时配置经验值:内部 RPC 超时应 < 上游超时(避免上游已断下游仍占资源);连接池等待>200ms 即应告警。排查样本:按接口/实例/下游三维交叉,定位是否局部。
失败与降级: 应急:对慢接口限流或超时快速失败(fail-fast);非核心依赖降级/熔断;必要时扩容应用或只读副本。根因未明时禁止盲目调大超时——会放大排队、拖垮线程池。保留:慢查询日志、链路 Trace、线程池队列深度,供事后定位。
【原理溯源】
为什么“没发版也会变慢”? 性能是供需关系:要么需求涨了(流量、大 key、慢查询),要么供给降了(下游变慢、资源被抢、GC 变差、磁盘 IO 争用)。发版只是变更之一,不是唯一变量。
为什么必须先看调用链而不是直接看代码? 超时可能发生在自身逻辑、DB、Redis、下游任一环。没有 Trace 分层,会陷入“代码没改怎么变慢”的错误假设。Trace 把 RT 拆成各 Span 耗时,直接指认瓶颈层。
仍有约 40% 请求正常、60% 超过 1s 说明什么? 分布已双峰/长尾化:一部分请求仍快,另一批被某共同因素拖住——典型是连接池排队、锁等待、慢 SQL 命中、GC 停顿、热点分片。要按“超时请求的共同特征”(同一接口/同一参数/同一时段)聚类。
线程池打满为什么表现为超时而不是报错? 任务进队列排队,等待时间叠加到 RT。只有队列满才触发拒绝策略。所以监控要看队列深度和活跃线程,不能只看错误率。
连接池耗尽的因果链: 某慢 SQL/下游拖长连接持有时间 → 池中可用连接下降 → 新请求阻塞在 borrow → RT 整体抬升 → 看起来“全站变慢”,根因却可能只是一条 SQL。
【选型判断树】
接口变慢,先问范围与时间点:
├─ 刚有发布/配置变更 → 优先回滚验证(174/180 题)
├─ 全站都慢 → 基础设施:网络/磁盘/宿主机/网关/公共下游
└─ 单接口或部分接口慢
├─ 调用链显示某下游/DB Span 变长 → 查该依赖(慢SQL/慢服务/大key)
├─ 自身逻辑 Span 变长
│ ├─ GC 停顿(jstat 对齐时间点)
│ ├─ 锁等待(jstack BLOCKED)
│ └─ 线程池/连接池排队(池监控 active=max)
└─ 无明显 Span 变长但整体慢 → 网络抖动/丢包/TCP 重传判断口诀: 先范围再时间点,再用 Trace 分层,最后对池/锁/GC/慢SQL逐项排除。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “超时排查本质是供需分析:需求涨了还是供给降了” |
| 0:30–1:30 | 现象确认 | 范围、分布、时间点、是否变更 |
| 1:30–3:00 | 调用链分层 | 自身/DB/Redis/下游/Machine 五层,每层典型根因 |
| 3:00–4:00 | 池与锁 | 线程池排队、连接池耗尽的传导机制 |
| 4:00–5:00 | 收尾 | “先 Trace 定层,再对症;修完必须压测+告警” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 超时设置原则 | > 下游 P99,< 上游容忍 | 反向传导会雪崩 |
| 线程池危险 | 活跃=最大 且队列持续涨 | 已打满 |
| 连接池危险 | active=max 且 wait 时间长 | 借不到连接 |
| GC 对齐 | STW 时间点与超时点重叠 | 证明是 GC |
| 慢 SQL 阈值 | 通常 100ms–1s(按业务) | 先看长尾 |
| 排查目标 | 10 分钟内给分诊结论 | 先止血再根治 |
【追问链】(三层)
L1|“P99 高但平均不高说明什么?” → 存在长尾请求。平均被大量快请求稀释,尾部用户体验已受损。常见原因:慢 SQL 偶发命中、GC 停顿、大请求污染、热点用户。要优化 P99 而不是平均。
L2|“超时时间怎么定?” → 链路自上而下:最外层用户超时最长,每往内一层递减,保证内层先超时,快速失败并释放资源。经验值:HTTP 客户端连接 200–500ms、读 1–3s(按业务);必须大于下游 P99,否则自己制造超时风暴。
L3|“调用链显示都正常,但用户仍超时,查什么?” → 查链路外因素:① 客户端到接入层网络/DNS/TLS;② 网关限流/排队;③ 容器 CPU throttle(cgroup 限制);④ 安全组/防火墙丢包;⑤ 前端资源加载。服务端指标“正常”不等于用户路径正常。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 能列出慢SQL、下游、GC 等几个原因 |
| 80 分 | 有“范围→Trace→分层→根因”顺序;能说线程池/连接池传导 |
| 95 分 | 供需分析框架;超时时间链式设置原则;P99 长尾洞察;能指出链路外因素 |
【关联题】
- 同一知识簇: 第 165 题(GC)、第 169 题(线程池)、第 170 题(Redis)、第 172 题(Hang)、第 182 题(间歇抖动)
- 方法论: 调用链/TraceID 关联;第 178 题(错误率分诊)
- 上游: 慢 SQL 与 EXPLAIN(数据库章)
【自测】
- 接口超时,第一步看代码还是看调用链? 参考答案:看调用链。先确认耗时在哪一层,避免盲目改代码。
- 判断:线程池打满一定会报错。 参考答案:错。队列未满时任务排队,表现为 RT 上升;队列满才触发拒绝策略。
- 超时时间应该内层更长还是外层更长? 参考答案:外层更长、内层更短。内层先超时快速失败,避免资源被拖死。
167. 服务内存持续上涨,GC 后也不回落(内存泄漏)
【考察内容】内存泄漏定位是 JVM 排查进阶必考题
【题目】服务运行一周后堆内存从 2G 涨到 6G,Full GC 后也不回落,怀疑内存泄漏。怎么确认是泄漏而不是缓存/正常增长?怎么用 MAT/Arthas 定位到具体对象和代码位置?常见泄漏模式有哪些?
【参考答案】
判断泄漏:监控堆使用率——GC 后曲线持续上升不回落到基线=泄漏;若 GC 后回落=只是压力大;
定位步骤:
- 连续 dump 两份堆快照(间隔一段时间,相同负载下);
- MAT 打开:比较两份 dump(Histogram 对比)——找出增长最快的类;
- 不想先拖 6G 大 dump 时,用 Arthas 在线取证(MAT 与 Arthas 两条路都要答):
dashboard/memory看各区占用与 GC 趋势,thread -b看阻塞,jmap -histo(不带 live)或 Arthasheapdump前先看对象排行,vmtool --action getInstances --className com.x.Foo --limit 10直接取实例、再用ognl看它的字段,确认「谁持有」再上 MAT 的 Path to GC Roots; - 对增长类右键 → Path to GC Roots(exclude weak/soft references)——看是谁持有它不放(静态字段、ThreadLocal、缓存、监听器);
- 定位到代码后修复(释放引用/remove/加淘汰);
常见泄漏模式:静态集合当缓存(无界)、ThreadLocal 不 remove、连接/流未关闭、事件监听未注销、内部类持有外部类引用(匿名内部类/非静态内部类);
预防:
- 代码审查关注静态集合/缓存生命周期;
- 缓存用 Guava/Caffeine(自带淘汰)而非裸 HashMap;
- 资源用 try-with-resources;
- 上线前压测观察内存曲线、堆 80% 告警、定时 dump 基线对比。
容量估算: 内存曲线判据:GC 后回落值(老年代水位)应长期平稳;若每次 GC 后水位抬升 50–200MB 且呈线性,按“每小时涨幅×重启周期”推算何时 OOM。监控采样:heap used after GC、线程数、FD 数、连接数、本地缓存条目数。经验:线程数每涨 1MB 栈(
-Xss默认约 1MB)×N 线程也是堆外内存的主要来源(1000 线程≈1GB);ThreadLocal缓存常见涨幅可达 MB/天量级。失败与降级: 未定位前:定时摘流滚动重启(治标,换时间);关闭可疑功能开关(同步导出、调试日志、本地大缓存)。若泄漏在第三方 SDK:升级/替换或外挂隔离进程。重启策略必须滚动,且重启前尽量留下 histo/diff 证据。
【原理溯源】
为什么“GC 后不回落”是泄漏金标准? Full GC 会回收所有不可达对象。若回收后老年代占用仍高于历史基线且逐次抬升,说明存在强引用链持续持有本应释放的对象——这就是泄漏的定义。缓存/高峰是“可达但可淘汰”,GC 后会下来。
为什么要双 dump 对比而不是单份? 单份只能看到“现在谁大”,分不清是合法缓存还是泄漏。两份间隔 dump 做 Histogram diff,增量才是泄漏嫌疑。相同负载、相同流量阶段对比,减少噪声。
Path to GC Roots 在找什么? 从泄漏对象向上回溯到 GC Root(静态字段、活跃线程栈、JNI)。路径上第一个“不该长期持有”的节点就是代码缺陷点。排除 weak/soft 是因为它们内存紧张时会被回收,不构成硬泄漏。
匿名内部类为什么容易泄漏? 非静态内部类/匿名类持有外部类 this 引用。若该内部类被静态集合或长生命周期监听器持有,外部类整棵对象图都无法回收——经典 Android/回调泄漏,后端同样存在(异步回调、事件总线)。
WeakReference 能根治吗? 只适合“可丢弃缓存”。若业务强依赖该对象(如会话、连接),弱引用会在 GC 时丢数据。根治是控制生命周期:明确谁创建谁释放,而不是把引用改弱。
【选型判断树】
内存涨,先看 GC 后是否回落:
├─ 回落到基线 → 非泄漏,是流量/缓存高峰
│ → 调容量或缓存上限,不是修"泄漏"
├─ 不回落且持续抬升 → 泄漏
│ ├─ 能稳定复现增长 → 双 dump Histogram diff → Path to GC Roots
│ └─ 增长很慢(天级)→ 定时基线 dump + 自动化对比告警
└─ 堆没涨但 RSS 涨 → 堆外(见 181 题)
→ NMT / 线程数 / 元空间 / 直接内存判断口诀: 先问“GC 后回不回落”,再决定是容量问题还是引用问题。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “泄漏判定看 GC 后是否回落,定位靠双 dump 对比” |
| 0:30–1:30 | 判定方法 | 监控曲线 + jstat;回落 vs 不回落 |
| 1:30–3:00 | 定位步骤 | Arthas 在线取证(dashboard/vmtool getInstances)→ 双 dump → Histogram diff → Path to GC Roots |
| 3:00–4:00 | 典型模式 | 静态集合、ThreadLocal、未关闭资源、监听器、内部类 |
| 4:00–5:00 | 预防 | Caffeine、try-with-resources、压测基线、80% 告警 |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 双 dump 间隔 | 数小时~1 天(同负载阶段) | 太短增量噪声大 |
| 告警线 | 堆使用率 80%;GC 后低点逐日抬升 | 泄漏早期信号 |
| 线程栈内存 | 默认 -Xss 约 1MB | 1000 线程≈1GB |
| 缓存框架 | Caffeine/Guava 必须设 maximumSize 或 expire | 裸 Map=无界 |
| ThreadLocal | finally 中 remove | 线程池必做 |
| MAT 入口 | Histogram → Group by package → diff | 先粗后细 |
【追问链】(三层)
L1|“缓存越跑越大算泄漏吗?” → 看有无淘汰与业务预期。有界缓存(Caffeine maximumSize)涨到上限后稳定,是正常;无界静态 Map 只增不减,即使业务叫“缓存”,本质也是泄漏。判据是是否可回落到基线,不是名字。
L2|“Arthas 能定位泄漏吗?” → 可以辅助:memory 看内存区域;vmtool --action getInstances 拿实例;jad 确认代码。但精确引用链仍靠 MAT。Arthas 适合现场快速确认“哪类对象多”,MAT 适合根因引用链。
L3|“修完泄漏后如何防止回归?” → ① 代码评审清单:静态集合、ThreadLocal、监听器注册/反注册、资源关闭;② 压测内存曲线门禁;③ 定时 heap 基线对比告警;④ 关键缓存强制有界框架;⑤ 故障演练中加入“内存缓慢增长”场景。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 dump + MAT,或 Arthas 在线取实例(MAT 与 Arthas 两条路) |
| 80 分 | 会用 GC 后回落判定;能说双 dump 对比与 Path to GC Roots |
| 95 分 | 讲清 ThreadLocal/内部类机制;区分缓存高峰与泄漏;有预防体系(有界缓存、评审清单、基线告警) |
【关联题】
- 同一知识簇: 第 164 题(OOM)、第 165 题(Full GC)、第 181 题(内存上涨)
- 工具: MAT 支配树、Arthas memory
- 代码规范: try-with-resources、有界缓存
【自测】
- 如何用一句话区分“内存压力大”和“内存泄漏”? 参考答案:Full GC 后占用回落=压力大;不回落且低点持续抬升=泄漏。
- Path to GC Roots 为什么要排除 weak/soft references? 参考答案:它们在内存紧张时可被回收,不构成必须修复的强持有;要找的是强引用链上的持有者。
- 判断:把静态缓存改成 WeakHashMap 就彻底解决泄漏。 参考答案:不彻底。只适合可丢弃数据;业务强依赖时会丢数据。根治是控制生命周期与有界淘汰。
168. 服务部分请求卡死,怀疑死锁(死锁排查)
【考察内容】并发故障定位
【题目】线上部分请求一直不返回(卡死),但服务没崩。怀疑发生了死锁。怎么确认(jstack 看什么)?怎么定位是哪两把锁互相等?解决与预防(锁顺序、超时、减少锁粒度)怎么做?
【参考答案】
现象:某些线程永久 BLOCKED,接口超时,重启恢复但复发;
确认:
jstack <PID>查看线程状态——发现大量java.lang.Thread.State: BLOCKED且互相等待(jstack 会直接打印 Found one Java-level deadlock 及参与线程和锁);或jstack -l看锁信息;定位:死锁报告会列出两个线程各自持有的锁和等待的锁——定位到代码(两个锁获取顺序不一致,如 A 拿锁1等锁2、B 拿锁2等锁1);
解决:
- 统一加锁顺序(所有线程按固定顺序获取多把锁);
- 用
tryLock(timeout)(ReentrantLock 超时放弃,替代 synchronized 无限等待); - 减少锁嵌套(一把锁搞定/锁粒度拆分);
- 数据库死锁类似(见 60 题:统一顺序/短事务);
预防:代码审查(多锁获取顺序)、压测并发场景、死锁监控(jstack 定时扫描/Arthas thread -b 找阻塞线程)。
容量估算与检测: 死锁在 jstack 中表现为
Found one Java-level deadlock或环形 BLOCKED。线程池场景:若活跃线程≈max 且 BLOCKED 占比>50%,高度可疑锁等待。数据库死锁与 JVM 死锁要分开:InnoDB 会自动回滚一方并报错;Java 死锁则永久卡死。告警:同接口成功率骤降+线程数打满+无 GC 异常 → 优先怀疑锁。失败与降级: 应急:重启卡住实例(先摘流);DB 死锁可 kill 持锁会话(需 DBA 流程)。临时缓解:减小事务/锁粒度、超时获取锁(
tryLock)、热点资源分片。业务降级:关闭并发写热点功能入口,改为排队/异步。
【原理溯源】
死锁的四个必要条件是什么? 互斥、持有并等待、不可剥夺、循环等待。破坏任一即可防死锁。工程上最常用:统一加锁顺序破坏循环等待;tryLock 超时部分破坏“无限等待”。
jstack 为什么能自动报死锁? JVM 通过对象监视器的持有者关系构建等待图,发现环即判定死锁,并打印
Found one Java-level deadlock,列出每个线程locked <A> / waiting to lock <B>的锁归属。看到这段就是铁证,不必自己推。BLOCKED 和 WAITING 有什么区别? BLOCKED:在
synchronized入口排队等监视器;WAITING:在Object.wait()/LockSupport.park()上无限等,需要被唤醒。死锁典型是 BLOCKED 环;连接池耗尽更多是 WAITING(等 condition)。为什么 ReentrantLock 更利于规避死锁?
synchronized只能阻塞等,无法超时退出;ReentrantLock.tryLock(timeout)可在拿不到锁时放弃并走降级/重试,切断无限等待。还可配合公平锁、可中断锁。数据库死锁与 JVM 死锁同构吗? 同构:都是多事务/多线程以不同顺序持有并等待资源形成环。DB 由引擎检测并牺牲一个事务回滚;JVM 不会自动牺牲,靠应用改代码。所以 DB 死锁有“自动解除+重试”,JVM 死锁会卡死到重启。
【选型判断树】
请求卡死,jstack 看状态:
├─ 打印 Found one Java-level deadlock
│ → 真死锁:统一锁顺序 / tryLock / 减锁嵌套
├─ 大量 BLOCKED 但无死锁报告
│ → 热点锁竞争(非环):减粒度、读写锁、无锁结构、异步
├─ 大量 WAITING/TIMED_WAITING
│ → 等资源:连接池/线程池/下游(见 172 题)
└─ RUNNABLE 且 CPU 高
→ 死循环/计算(见 163 题),不是死锁判断口诀: 先搜 deadlock 关键字,再按 BLOCKED/WAITING/RUNNABLE 分流。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “死锁是循环等待,jstack 能直接报出来” |
| 0:30–1:30 | 四条件 | 互斥、持有等待、不可剥夺、循环等待 |
| 1:30–2:30 | 确认方法 | jstack deadlock 段落怎么读 |
| 2:30–3:30 | 解决 | 统一顺序、tryLock、减锁 |
| 3:30–4:30 | 与 DB 对比 | 同构但 DB 自动牺牲一个事务 |
| 4:30–5:00 | 收尾 | “防死锁优先统一顺序,兜底 tryLock” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 确认命令 | jstack <PID> | 搜 deadlock |
| 阻塞线程 | jstack -l / Arthas thread -b | 找最忙阻塞线程 |
| tryLock 超时 | 100ms–2s(按业务) | 超时走降级 |
| 采样 | 多次 jstack 间隔 5–10s | 确认长期卡住 |
| DB 死锁 | 引擎自动回滚一个事务 | 应用需重试 |
【追问链】(三层)
L1|“synchronized 和 ReentrantLock 在死锁场景差在哪?” → ReentrantLock 可 tryLock 超时中断等待,可公平排队,可响应中断;synchronized 只能无限阻塞,无法超时。要规避死锁优先 ReentrantLock。
L2|“jstack 没报死锁,但线程一直 BLOCKED,为什么?” → 可能是热点锁竞争而非循环等待(只有等待没有环),或死锁发生在 DB/其他进程。看锁持有者是否长期不释放(慢操作持锁),减临界区或改并发结构。
L3|“线上已死锁,怎么最快恢复?” → ① 先分清是哪种锁,再决定能不能「打断」:synchronized 监视器上的 BLOCKED 线程是叫不醒的——interrupt() 只置标志位,线程要等持锁方释放监视器才会走(实测 interrupt 后 state 仍是 BLOCKED),本题上文也写了「synchronized 无法超时退出」;能中断的只有 ReentrantLock.lockInterruptibly()、LockSupport.park/wait 这类 WAITING 线程以及 DB 会话(kill 数据库 session 有效)。拿不准就直接走 ②;② 先抓现场(连续 2–3 次 jstack,必要时 jmap -histo),再摘流量重启该实例——顺序反了现场就没了;③ 多实例只重启卡死的那台;④ 事后统一锁顺序根治。多实例时先摘故障实例,避免全量重启。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 jstack,听说过死锁 |
| 80 分 | 会读 deadlock 报告;能说统一顺序和 tryLock |
| 95 分 | 讲清四条件与破坏方式;区分 BLOCKED/WAITING;对比 DB 死锁;有应急摘流量+留现场流程 |
【关联题】
【自测】
- jstack 输出哪一行是死锁铁证? 参考答案:
Found one Java-level deadlock,以及随后的 locked / waiting to lock 列表。 - 破坏死锁最常用的条件是哪一个? 参考答案:循环等待——所有线程按相同顺序获取多把锁。
- 判断:synchronized 比 ReentrantLock 更容易做超时规避。 参考答案:错。ReentrantLock 支持 tryLock(timeout),synchronized 无法超时。
169. 线程池活跃线程=最大线程数,队列持续增长(线程池打满)
【考察内容】并发资源治理
【题目】监控显示某个线程池活跃线程数已等于最大线程数、任务队列持续增长、接口 RT 飙升。线程池为什么被打满(上游流量涨、任务变慢、下游阻塞)?应急怎么处理?根治怎么做?
【参考答案】
根因判断(线程池满=任务处理速度跟不上提交速度):
- 任务本身变慢(依赖下游变慢/DB 慢/锁竞争);
- 任务量突增(流量上涨);
- 线程池参数不合理(核心线程过小/队列过小);
- 任务中有阻塞(线程被阻塞在 IO/锁上,看似忙实际在等);
应急:限流入口(挡住新任务,让队列消化);排查并修复下游/慢 SQL(根因);必要时扩容实例;
根治:
- 调参数(核心/最大线程数、队列长度——与任务类型匹配);
- 优化任务执行(异步化、批量、减少锁);
- 拒绝策略:生产用自定义策略(告警+落库/MQ 补偿),避免静默丢弃;
- 监控:队列深度、拒绝次数、任务耗时告警;
关键认知:线程池满通常是“结果”不是“原因”——找到任务变慢的源头(Arthas thread 看线程在干什么:WAITING 在 IO?RUNNABLE 在计算?)。
容量估算: 线程池参数起点(IO 密集):核心线程≈CPU核数×(1+等待/计算)≈核数×2–10(等待/计算比≈9 时是 10 倍;要到 20 倍得比值≈19);队列长度按“可容忍排队时间×峰值TPS”估算(如可忍 500ms、峰值 2000 TPS → 队列≈1000,过大反而雪崩)。监控:queue remaining capacity、active、completed task、reject count。经验值:队列持续增长且 active=max 连续 1 分钟 → 必须介入。
失败与降级: 立即限流入口 QPS 到“处理能力×0.8”;应急期不要选 CallerRuns:本条第 1 项已判定根因多是下游变慢,CallerRuns 会把被拒任务搬回提交线程(Tomcat/RPC 线程)执行,实测能让调用线程被占 2s 以上,等于把线程池过载扩散成全站超时;应急只做「落 MQ 延迟补偿+快速失败告警」,禁止 Discard 静默丢。CallerRuns 的适用面是「不允许丢且提交方确有余量」的离线/批处理,不是过载时的降级手段。下游熔断,避免线程被慢调用占死。扩容应用实例可线性分摊线程池压力(注意 DB/下游是否成为新瓶颈)。
【原理溯源】
线程池打满的供需等式: 稳态要求
完成速率 ≥ 提交速率。打满要么提交变多(流量),要么完成变慢(任务 RT 上升)。加线程只是提高并行度,若根因是下游慢/锁竞争,加线程会加剧资源争抢(更多线程抢同一连接池/锁),反而更糟。为什么无界队列危险? 无界队列下最大线程数几乎不生效(任务永远先入队),队列可耗尽堆内存导致 OOM,且故障被无限推迟。生产必须有界队列 + 明确拒绝策略。
拒绝策略为什么不能直接用默认的 AbortPolicy? AbortPolicy 是显式失败——往提交线程抛
RejectedExecutionException,异常常被上层吞成一条日志、任务照样没执行;真正静默丢弃、连异常都不抛的是 DiscardPolicy / DiscardOldestPolicy,两者别混。生产应自定义:告警 + 降级 + 落库/MQ 补偿,保证“丢得起且可追”。WAITING vs RUNNABLE 指向什么根因? WAITING/TIMED_WAITING:线程在等 IO/锁/连接——查下游、连接池、锁;RUNNABLE 且 CPU 高:在计算——查慢逻辑、序列化、正则。分流后再深挖,避免盲目加线程。
CPU 密集 vs IO 密集怎么配线程? CPU 密集:线程数 ≈ 核数(再多只是切换开销);IO 密集:可更高(线程大部分时间在等),经验
核数 * (1 + 等待/计算),或直接压测定。错配是常见人为故障。
【选型判断树】
线程池满,先看任务在等什么(Arthas/jstack):
├─ 大量 WAITING 在下游/DB/Redis
│ → 根因在依赖:修慢SQL/扩下游/加超时熔断
│ → 应急限流,不要盲目加线程
├─ RUNNABLE 且 CPU 高
│ → 任务计算慢:优化算法/异步化/拆批
├─ 提交速率暴涨(流量)
│ → 接入层限流 + 扩容实例
└─ 参数不合理
├─ CPU 密集 → 线程≈核数,有界小队列
└─ IO 密集 → 可高于核数,压测确定判断口诀: 池满是结果;先问任务慢在哪,再决定限流还是扩依赖还是加机器。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “线程池满是供需失衡的结果,不是原因” |
| 0:30–1:30 | 根因四类 | 任务变慢、流量涨、参数差、任务阻塞 |
| 1:30–2:30 | 状态分流 | WAITING=等资源;RUNNABLE=在计算 |
| 2:30–3:30 | 应急 | 限流止血、修依赖、扩容 |
| 3:30–4:30 | 根治 | 有界队列、拒绝策略、监控 |
| 4:30–5:00 | 收尾 | “先定位慢源,再谈加线程” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| CPU 密集线程 | ≈ CPU 核数 | 再多只增切换 |
| IO 密集线程 | 核数的 2–10 倍 | 压测确定 |
| 队列 | 必须有界 | 无界会 OOM 且 max 失效 |
| 拒绝策略 | 告警+落库/MQ | 禁止静默丢 |
| 危险信号 | active=max 且 queue 持续涨 | 已打满 |
| 观测 | Arthas thread / 池监控 | 看状态分布 |
【追问链】(三层)
L1|“队列用有界还是无界?” → 有界。无界会让最大线程数失效,任务堆积吃内存,故障从“拒绝”变成 OOM。有界+拒绝策略才能快速失败并暴露容量问题。
L2|“直接加最大线程数行不行?” → 若任务是 IO 等待且下游还有余量,适当加可以;若是下游慢/锁竞争/连接池耗尽,加线程会让更多线程卡在同一点,恶化争抢。必须先看线程在等什么。
L3|“拒绝的任务怎么不丢?” → 自定义拒绝策略:① 告警;② 可降级的直接降级返回;③ 必须做的写本地表/MQ 由补偿任务重试;④ 对实时性要求高的直接快速失败让上游重试。关键是可观测、可补偿,不是吞掉。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道加线程或扩机器 |
| 80 分 | 能分流量/任务慢/参数三类;说有界队列和拒绝策略 |
| 95 分 | “池满是结果”的根因思维;WAITING/RUNNABLE 分流;CPU/IO 密集配参;自定义拒绝+补偿闭环 |
【关联题】
【自测】
- 为什么无界队列会让“最大线程数”失效? 参考答案:任务优先入队,队列不满就不会创建超过核心数的线程,max 形同虚设,且可能 OOM。
- 线程池满时第一反应应该是加线程吗? 参考答案:不是。先看任务在等什么(下游/锁/计算),常需限流或修依赖,盲目加线程会加剧争抢。
- CPU 密集型任务线程数大约配多少? 参考答案:约等于 CPU 核数;再多只是上下文切换开销。
170. 线上 Redis 报连接超时、命令超时(Redis 故障排查)
【考察内容】中间件故障定位
【题目】线上应用开始报 Redis 连接超时、命令超时,部分接口受影响。可能的原因有哪些(网络、大 key 阻塞、慢命令、连接池耗尽、Redis 高负载、主从切换)?你按什么顺序排查?
【参考答案】
现象分类:
- 连接超时(连不上):Redis 宕机/网络不通/连接数打满(maxclients)/客户端连接池耗尽/主从切换与故障转移(failover 秒级不可写,表现为建连闪断,靠重试窗口收敛);
- 命令超时(连上但命令慢):大 key 阻塞、慢命令(KEYS/大范围)、CPU 打满、网络抖动、fork 阻塞(RDB/AOF 重写);
排查步骤:
- 监控看 Redis 节点 CPU/内存/网络/连接数/慢日志(SLOWLOG);
- 慢日志:SLOWLOG GET 看是否有慢命令(大 key 操作、KEYS、批量);
- 大 key 扫描:--bigkeys(阻塞与带宽问题);
- 客户端侧:连接池参数(maxTotal 是否够)、是否有泄漏(借用未归还);
- 网络:延迟/丢包(redis-cli --latency);
- 集群:某个分片倾斜(热 key)导致该分片慢;
解决:大 key/热 key 治理(见缓存章)、慢命令改 scan/分页、连接池调优、Redis 扩容(内存/CPU)、客户端重试+熔断(Redis 不可用降级本地缓存/直连 DB 限流);
预防:监控告警(连接数 80%、慢日志、CPU)、压测容量。
容量估算: Redis 单机经验值:简单命令 8–10 万 QPS 起,复杂命令/大 value 显著下降。连接数:客户端连接池一般每实例每应用 20–50,总连接 < maxclients(默认 1 万)的 70%。超时排查维度:① 命令本身慢(O(N) 大 key);② 网络/带宽;③ 连接池耗尽;④ 持久化/主从同步阻塞。慢日志
SLOWLOG GET阈值建议 10–50ms。失败与降级: 应急:摘掉读 Redis 的非核心路径,改查 DB(要限流防击穿);本地缓存兜底热点;必要时主从切换。若大 key/热 key:拆分、本地缓存、只读副本。故障期间禁止全量重建缓存,避免雪上加霜。
【原理溯源】
为什么先分“连接超时 vs 命令超时”? 根因层完全不同:连接超时在握手/建连阶段(网络、宕机、maxclients、客户端池);命令超时在执行阶段(慢命令、大 key、CPU、fork)。不分清会在错误方向排查。
Redis 单线程下慢命令为什么全局拖死? 命令在单线程内串行执行。一条
KEYS *或大 hashHGETALL会阻塞所有后续命令,表现为集群性命令超时,而不只是那一个客户端慢。fork 为什么会阻塞? RDB/AOF 重写时
fork子进程,父进程 COW(写时复制)。fork 瞬间要拷贝页表,大内存实例(几十 G)fork 可能阻塞百毫秒级;之后写入多时 COW 复制也会增加延迟与内存。连接池耗尽和服务端 maxclients 差在哪? 客户端池耗尽:应用侧借不到连接,报的是池超时,Redis 服务端连接数可能不高。maxclients 打满:服务端拒绝新连接,所有客户端建连失败。监控要两侧都看。
热 key 为什么表现为部分超时? Cluster 下热 key 落在单个分片,该分片 CPU/带宽打满,只有打到该分片的命令慢——所以“部分接口”受影响,符合症状。
【选型判断树】
Redis 异常,先分类型:
├─ 连接超时
│ ├─ 服务端不可达 → 宕机/网络/安全组
│ ├─ 服务端连接满 → maxclients / 客户端泄漏
│ └─ 客户端池满 → maxTotal 调优 / 泄漏归还
└─ 命令超时
├─ SLOWLOG 有大命令 → 治理大key/KEYS/HGETALL
├─ CPU 高但无慢日志 → 热key分片倾斜 / 高QPS
├─ 与持久化时间重叠 → fork 阻塞
└─ redis-cli --latency 高 → 网络问题判断口诀: 连接还是命令;服务端还是客户端;慢命令还是热 key 还是网络。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “先分连接超时和命令超时,根因层不同” |
| 0:30–1:30 | 连接类 | 宕机、网络、maxclients、客户端池、主从切换与故障转移 |
| 1:30–3:00 | 命令类 | 慢命令、大key、热key、fork、CPU |
| 3:00–4:00 | 工具 | SLOWLOG、--bigkeys、--latency、池监控 |
| 4:00–5:00 | 收尾 | “降级:本地缓存兜底 + DB 限流保护” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| maxclients | 默认 10000 | 按连接模型调整 |
| SLOWLOG | 默认记录 >10ms | SLOWLOG GET |
| 大 key | String >10KB;集合元素 >5000 | 需拆分 |
| 连接池告警 | active/max >80% | 提前扩容 |
| --latency | 持续 >1ms 需关注 | 网络抖动 |
| 降级 | 本地缓存 + DB 限流 | 缓存不可用预案 |
【追问链】(三层)
L1|“只重启 Redis 能好吗?” → 可能暂时恢复,但若根因是大 key/慢命令/连接泄漏/容量不足,会复发。重启前保留 SLOWLOG、监控、客户端日志。应急可重启,但必须事后根治。
L2|“Redis 挂了业务怎么保?” → 多级缓存:本地缓存兜底热点;关键路径降级静态化;DB 层限流防止雪崩;非核心功能熔断。预案要在故障前演练,不能故障时现写。
L3|“怎么区分是客户端池问题还是服务端问题?” → 看两侧指标:客户端池 active/wait、建连错误;服务端 connected_clients、rejected_connections、CPU、SLOWLOG。服务端正常而客户端 wait 高=池配置/泄漏;服务端 rejected>0=maxclients 或服务端故障。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道看 Redis 日志/重启 |
| 80 分 | 区分连接/命令超时;会用 SLOWLOG |
| 95 分 | 客户端池 vs maxclients;热key分片;fork 阻塞;降级预案(本地缓存+DB限流) |
【关联题】
【自测】
- 连接超时和命令超时排查方向有何不同? 参考答案:连接超时查网络/宕机/maxclients/客户端池;命令超时查慢命令/大key/热key/CPU/fork。
- 为什么一条 KEYS * 会导致大量接口超时? 参考答案:Redis 单线程串行执行,该命令阻塞期间其他命令全部排队。
- Redis 不可用时业务侧第一道防线是什么? 参考答案:本地缓存兜底热点 + DB 限流保护,非核心功能熔断降级。
171. 磁盘 100% 写满,服务开始报错(磁盘故障)
【考察内容】基础设施故障
【题目】监控告警:某台机器磁盘使用率 100%,服务开始报“No space left on device”。什么文件把磁盘写满了(日志、dump、数据文件)?怎么应急释放空间?长期怎么治理(日志轮转、容量监控)?
【参考答案】
磁盘空间满排查:
df -h看挂载点使用率;du -sh */ du 按目录定位大文件;- 常见大文件:日志文件(应用日志/GC 日志/Nginx 日志无轮转)、dump 文件(OOM 自动 dump)、临时文件(未清理)、Kafka/ES 数据目录(保留策略问题)、Docker 镜像/容器日志;
应急:清理可删文件(旧日志/临时文件/过期备份)→ 释放空间;配置日志轮转(logrotate:按大小/天切割+保留 N 份)根治;
磁盘 IO 高排查:
iostat -x 1看%util(饱和)与r_await/w_await(延迟)——列名带百分号与读写前缀,util/await这种老写法在新版 sysstat 里找不到;iotop定位进程:数据库(慢 SQL 全表扫/刷盘)、ES(索引/合并)、大文件读写、日志写太多;- 数据库 IO 高:慢 SQL/无索引/缓存命中率低(buffer pool 小);
根治:日志治理(级别/采样/轮转)、索引优化、冷数据迁移(归档到低频盘/OSS)、Kafka/ES 保留策略调整、SSD 扩容或分层存储;
监控:磁盘使用率 80% 告警、IO util 告警。
容量估算: 磁盘水位:>80% 告警,>90% 高危,>95% 随时可能写失败。日志增长估算:单实例应用日志常见 1–10GB/天,access log 更高;core dump 可能=堆大小。inodes 也要看(大量小文件会 inode 满而 df 显示仍有空间)。保留策略:热日志 3–7 天,冷日志对象存储,压缩后归档。
失败与降级: 立即止血:清安全可删文件(旧日志、临时文件、容器 dangling image);日志切换到其他盘/对象存储;必要时只保留 error 级。应用降级:关闭 debug 日志、关闭本地文件缓存写入。根因修复后加磁盘巡检与自动清理任务。
【原理溯源】
磁盘满和 IO 高是两条线,为什么要分开? 空间满=容量问题,写失败报
No space left;IO 高=吞吐/延迟问题,服务变慢但不一定满。处置完全不同:前者删文件/扩容/轮转;后者找写放大源头(慢SQL、合并、日志风暴)。为什么“只删文件”会复发? 删的是存量,产生速率没变。若日志无轮转、dump 无清理策略、Kafka 保留过长,几小时后又满。必须同时降产生速率(级别/采样)+ 限存量(轮转/保留策略)。
为什么删了文件空间不释放? 进程仍持有已删除文件的 fd(如日志句柄),空间直到进程释放 fd 或重启才回收。
lsof | grep deleted可见。这是经典坑。iostat 的 %util / r_await / w_await 怎么读?
%util接近 100% 表示设备时间被 IO 占满;r_await/w_await是读写平均等待时间,持续升高说明排队。新版 sysstat 的列名就带百分号与读写前缀(本机 sysstat 12.6.1 实测表头:r_await … w_await … aqu-sz %util),裸写util/await在新版里找不到列。注意多盘 RAID/SSD 上 util 100% 不一定无余力,要结合 await 和业务 RT。数据库为什么容易打满 IO? 无索引全表扫描、buffer pool 偏小导致频繁读盘、redo/脏页刷盘、大事务写放大。先看慢 SQL 与命中率,而不是直接换盘。
【选型判断树】
磁盘告警,先分空间还是 IO:
├─ 使用率 100%,报 no space
│ ├─ df 定位挂载点 → du 找大目录/大文件
│ ├─ 常见:日志/dump/临时文件/Kafka-ES数据/容器日志
│ └─ 应急删可删文件 + 配轮转;deleted 未释放则重启进程
└─ 使用率不高但 IO util 高、服务慢
├─ iostat -x 看 %util / r_await / w_await
├─ iotop 找进程
└─ DB:慢SQL/命中率;ES:merge;日志:写风暴判断口诀: 先 df 分空间/IO,再 du/iotop 定位,最后轮转+保留策略根治。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “磁盘问题分空间满和 IO 高两条线” |
| 0:30–1:30 | 空间满 | df/du;常见大文件类型 |
| 1:30–2:30 | 应急与坑 | 删文件、deleted 未释放、轮转 |
| 2:30–3:30 | IO 高 | iostat/iotop;DB 慢SQL 关联 |
| 3:30–4:30 | 根治 | 级别采样、保留策略、冷数据、扩容 |
| 4:30–5:00 | 收尾 | “80% 告警,不要等 100%” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 告警线 | 使用率 80% | 留缓冲 |
| df / du | df -h → du -sh * | 先挂载点后目录 |
| logrotate | 按天或 100–500MB 切,保留 7–30 份 | 按合规 |
| iostat | iostat -x 1 | 看 %util 与 r_await/w_await(新版 sysstat 列名,本机 12.6.1 实测) |
| Kafka/ES 保留 | 按容量规划,禁无限保留 | 数据目录易满 |
| deleted 文件 | lsof | grep deleted | 重启才释放 |
【追问链】(三层)
L1|“为什么日志会打满磁盘?” → 无轮转 + 全量 debug 日志 + 异常堆栈在循环里反复打印 + 高 QPS 未采样。治理:级别、轮转、采样、集中采集本地少留。
L2|“磁盘 IO 高要不要直接换 SSD?” → 先找源头。若是无索引全表扫或日志风暴,换 SSD 只是推迟瓶颈且浪费成本。用 iotop/slowlog 定位写放大,优化 SQL/合并策略/日志后仍有需求再升配。
L3|“容器环境磁盘满常见原因?” → 容器日志(json-file 无上限)、镜像层堆积、emptyDir/临时卷、节点上多 Pod 共享盘。用 docker system prune、配置 log-opts max-size、节点容量监控与驱逐策略。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 df/du 删文件 |
| 80 分 | 分空间/IO 两条线;会 iostat;知道轮转 |
| 95 分 | deleted 未释放、DB IO 与慢SQL、日志风暴治理、容器日志;80% 告警体系 |
【关联题】
【自测】
df和du分别看什么? 参考答案:df 看文件系统/挂载点使用率;du 统计目录/文件占用,用于定位大文件。- 为什么删除大日志后空间可能没减少? 参考答案:进程仍持有该文件 fd,空间要等进程释放或重启。lsof|grep deleted 可确认。
- 磁盘 IO 高与空间满的排查分叉点是什么? 参考答案:空间满看容量(df/du/清理/轮转);IO 高看吞吐延迟(iostat/iotop/写放大源头)。
172. 服务既不报错也不响应,jstack 看到大片线程卡住(服务 Hang)
【考察内容】服务 Hang 是最棘手的线上问题之一
【题目】服务“假死”:不报错、不响应、不崩溃,健康检查都过不去。jstack 看到大量线程卡在同一个地方。可能是什么(锁等待、外部调用阻塞、IO 阻塞)?怎么快速定位卡点并恢复?
【参考答案】
先区分线程状态(jstack):
- 大量 RUNNABLE 且 CPU 高:死循环/密集计算(见 CPU 排查);
- 大量 BLOCKED:锁竞争/死锁(见死锁排查);
- 大量 WAITING/TIMED_WAITING(在锁/条件变量上):常见——业务线程都在等待某个资源:连接池(DB/Redis/HTTP 连接池耗尽)、线程池队列(任务排队)、下游服务(同步调用无超时);
- WAITING 在 socketRead:都在等下游/网络(下游不返回,超时未设置);
定位方法:
- jstack 多次采样(间隔几秒),看线程是否长时间停留在同一位置;
- 看“等待什么”:堆栈里的 Locked ownable synchronizers / waiting on 信息;
- 结合调用链/连接池监控:连接池 active=最大=池耗尽;
常见根因与解决:
- 连接池耗尽(DB/下游):请求全在等连接——查慢 SQL/泄漏/下游 RT,调池大小;
- 下游无超时:同步调用没设 timeout,下游挂起拖死上游——所有远程调用必须设超时;
- 线程死锁(见 168);
- GC 长时间停顿(STW):看 GC 日志;
应急:Arthas thread -b(找阻塞线程)/thread -n(最忙线程);必要时摘流量/重启,恢复后分析根因。
容量估算与判据: Hang 的典型信号:进程在、端口在,但 RT 趋近超时或线程全部 WAITING/BLOCKED;健康检查若只探端口会漏检,应探“业务就绪”接口。jstack 大片 WAITING 常见栈:
Object.wait、连接池、take()、ZK/注册中心、死锁环。判据:连续 3 次 jstack(间隔 5s)同一堆栈聚类 → 非瞬时抖动。失败与降级: 先摘流防扩大;对 Hang 实例滚动重启;下游不可达则熔断降级走兜底数据。不要对 Hang 实例做重量级 dump(可能更卡),先
kill -3/轻量 jstack。恢复后必须复盘:是依赖、锁、还是资源泄漏导致。
【原理溯源】
为什么“不报错”也是故障? 线程阻塞在等待时不会抛异常,进程仍存活,健康检查若只探活不探就绪会通过。用户侧表现为超时。这是“假死”——比崩溃更难发现,必须 readiness + RT 告警。
为什么 jstack 状态能分流根因? 状态机直接映射行为:RUNNABLE=占用 CPU 执行;BLOCKED=等监视器锁;WAITING=无限等(wait/park/连接池);TIMED_WAITING=带超时等。大片同栈 WAITING 说明在等同一类资源。
socketRead 卡住意味着什么? 线程阻塞在读下游响应。若未设 SO_TIMEOUT,可以永久卡住。一个下游故障 + 无超时 = 上游线程池被慢慢抽干,级联雪崩。
连接池耗尽的因果闭环: 下游慢 → 连接持有时间变长 → 池中空闲连接减少 → 新请求排队借连接 → 线程全部 WAITING 在池 → 服务假死。根因往往不在池参数,而在“为什么持有变长”。
为什么必须所有远程调用设超时? 超时=快速失败,切断故障传导。没有超时的依赖是雪崩放大器。原则:连接超时短、读超时按下游 P99 设,且小于上游超时。
【选型判断树】
服务 Hang,jstack 按状态分流:
├─ RUNNABLE + CPU 高 → 死循环/计算(163 题)
├─ BLOCKED → 锁/死锁(168 题)
├─ WAITING 大片在同一连接池/condition
│ → 池耗尽:查持有者(慢SQL/下游/泄漏)
├─ WAITING 在 socketRead
│ → 下游无超时或下游挂:补超时+熔断
└─ 全体卡住且 GC 日志有长 STW
→ GC 停顿(165 题)判断口诀: 状态分流 → 看栈顶等什么 → 对照池监控/下游/GC。
【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “Hang 是线程集体等待,用 jstack 状态分流” |
| 0:30–2:00 | 四态分流 | RUNNABLE/ BLOCKED/ WAITING/ socketRead |
| 2:00–3:00 | 池耗尽闭环 | 下游慢→持有长→池满→假死 |
| 3:00–4:00 | 超时原则 | 所有远程调用必须设超时 |
| 4:00–5:00 | 应急 | Arthas thread -b;摘流量;留现场 |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| jstack 采样 | 2–3 次,间隔 5–10s | 确认长期停留 |
| 连接超时 | 200–500ms | 建连 |
| 读超时 | > 下游 P99,1–3s 常见 | 必须设置 |
| 池危险 | active=max 且 wait 上升 | 耗尽 |
| Arthas | thread -b 阻塞;thread -n 忙 | 快速定位 |
| 健康检查 | liveness ≠ readiness | Hang 时 readiness 应失败 |
【追问链】(三层)
L1|“怎么确认是连接池耗尽?” → 线程堆栈大量 waiting on 连接池的 condition/await + 监控 active 连接接近 max + 借用等待时间飙升。三者同时出现即可确认。
L2|“为什么必须设超时?” → 超时=快速失败,防止故障无限传导。无超时时,下游挂起会逐步抽干线程池与连接池,把局部故障放大成全站雪崩。
L3|“应急选摘流量还是直接重启?” → 单实例:先摘流量(避免继续堆积),保留 jstack 后重启。多实例:只摘/重启故障实例,观察是否复发。重启是止血不是根治,必须回捞现场做根因。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道 jstack 看线程 |
| 80 分 | 能按 RUNNABLE/ BLOCKED/ WAITING 分流;说出池耗尽 |
| 95 分 | socketRead 与无超时雪崩;池耗尽因果闭环;readiness 与 liveness 区别;应急留现场纪律 |
【关联题】
【自测】
- 服务 Hang 时 jstack 最常见的线程状态是? 参考答案:WAITING/TIMED_WAITING(等连接池、下游、条件变量);也可能是 BLOCKED(锁)。
- 所有远程调用必须设置什么?为什么? 参考答案:必须设超时。实现快速失败,避免下游挂起拖死上游线程池,防止雪崩。
- liveness 和 readiness 在 Hang 场景分别应如何表现? 参考答案:进程活着 liveness 可通过;但不能接流量时 readiness 应失败,避免继续打到假死实例。