排障题考的是「有没有一套稳定的动作」,而不是背工具名。通用顺序是:先确认影响面和最近变更,再用指标缩小范围,然后取证,最后止血与复盘。下面所有命令都应在测试环境先验证,生产执行前评估影响。
1. 线上 CPU 飙高怎么排查?
先看整体:top 确认是哪个进程,观察 us、sy、wa 哪个高。用户态高通常是业务计算或 GC;系统态高可能是系统调用、锁竞争或上下文切换。
再定位线程:top -Hp <pid> 找到最耗 CPU 的线程,把十进制 id 转成十六进制,在 jstack <pid> 里按 nid=0x... 搜索。连续抓两三次,看它是否停在同一个方法。
业务代码的常见原因:死循环、正则灾难性回溯、超大集合排序、频繁 JSON 序列化、日志拼接、加密计算。如果是 GC 线程占用高,转去看 GC 日志和堆使用。
止血手段包括限流、回滚最近发布、摘除异常实例;不要直接重启了事,先保留线程栈和堆信息,否则根因就丢了。
2. 内存持续上涨怎么定位?
先区分堆内和堆外。看 GC 后老年代是否回落:如果每次 Full GC 后仍然上涨,怀疑泄漏;如果 GC 后能回落但很快又涨,可能是流量或缓存策略问题。
堆内取证用 jcmd <pid> GC.heap_dump 导出,配合 -XX:+HeapDumpOnOutOfMemoryError 自动保存;用 MAT 看支配树、对象数量和 GC Root 路径,重点找「本该释放却被集合或线程持有」的对象。
堆外要看直接内存(NIO、Netty)、元空间(动态生成类)、线程栈和 JIT 代码缓存。jcmd VM.native_memory summary 需要启动时开启 NMT,Netty 可以开泄漏检测。
常见根因:本地缓存无上限、ThreadLocal 未清理、监听器只增不减、批量查询一次加载过多数据。修复前先确认对象的生命周期,不要用「调大堆」掩盖泄漏。
3. 接口突然变慢,排查顺序是什么?
先看影响范围:是所有接口还是某个接口,是单实例还是全部实例,是否与发布、配置变更、流量高峰同时发生。这一步能排除大半方向。
再看链路:用 traceId 找到耗时最长的那一跳。如果时间花在数据库,看慢查询、锁等待和连接池;花在远程调用,看下游 RT、超时和重试;花在应用内,看线程池队列、锁竞争和 GC 停顿。
注意平均值会骗人,要看 P95、P99 和错误率。重试和超时配置不当会让下游变慢放大成雪崩,表现为「所有服务一起慢」。
还要检查资源:CPU 是否打满、连接数是否耗尽、磁盘是否打满、网络是否有丢包。定位后用限流、扩容、降级或回滚止血,再做根因修复。
4. 频繁 Full GC 怎么处理?
先确认频率和原因:用 GC 日志看每次回收前后的占用、停顿时间、晋升量,以及是否伴随 GC overhead limit exceeded 或 promotion failed。
如果是内存泄漏,堆 dump 会显示老年代对象持续增长,必须修代码;如果是堆太小或大对象过多,评估扩容和对象生命周期;如果是元空间不足,检查动态代理、脚本引擎和热部署。
G1 下还要关注 Humongous 对象:超过 Region 一半的大对象会占用连续 Region,频繁创建大数组会触发并发周期甚至 Full GC。
处理顺序是:先止血(扩容、限流、回滚),再取证(GC 日志、堆 dump),最后修复。不要只调 -Xmx,那只是把问题推迟。
5. 线程池打满有什么表现?怎么止损?
表现通常是接口 RT 上升、超时增多、队列长度持续增长、拒绝数上升,严重时上游开始熔断。异步任务则表现为「提交成功但很久不执行」。
先看监控:活跃线程数、队列长度、完成任务数、拒绝次数、任务耗时分布。再看线程栈,确认线程卡在数据库、远程调用还是锁上。
止血可以临时扩容、提高消费能力、把非核心任务摘出去,或者对入口限流。根因通常是下游变慢、任务无界提交、线程池之间互相阻塞,或者同步调用被放进了异步池。
长期要做池隔离(核心与非核心分开)、有界队列、明确拒绝策略、按下游容量设计并发,并给队列等待时间设上限。
6. 数据库连接池耗尽怎么办?
先看连接池指标:活跃连接、空闲连接、等待线程数、获取连接超时次数。活跃连接长期占满且不释放,通常是长事务或连接泄漏。
长事务的常见来源:事务里做远程调用、批量处理过多数据、手动开启事务后异常路径没提交/回滚。连接泄漏则是借出后没有在 finally 中归还。
用数据库侧的证据交叉验证:SHOW PROCESSLIST 看连接状态和执行的 SQL,慢查询日志找长时间运行的语句,锁等待也会让连接一直占用。
止血可以回滚、杀掉异常会话、临时扩容,但根本解决要缩短事务、修复泄漏、优化慢 SQL。连接池不是越大越好,超过数据库承载能力只会让争用更严重。
7. Redis 或 MQ 故障时怎么降级?
先明确依赖等级:哪些功能可以短时间不可用,哪些必须保底。缓存故障时,可以用本地缓存兜底、限流保护数据库、对非核心读请求返回默认值;同时防止「缓存失效后全部打到数据库」的雪崩。
消息队列故障时,生产端可以降级为同步调用、写入本地消息表稍后补发,或暂时拒绝非核心消息;消费端要保证重复消费幂等,恢复后按积压量平滑追平,避免瞬间打满下游。
降级开关要提前做好并演练,包含触发条件、影响范围、恢复步骤和责任人。不要等故障时才临时改代码。
所有降级都要有监控和告警:降级本身是「有损可用」,必须让团队知道当前处于降级状态。
8. 发布出问题怎么回滚?如何减少这类事故?
先判断能否快速回滚:应用版本回滚通常最快;数据库变更往往不可逆,所以要做「扩展-收缩」式变更,先加字段、双写、再切读、最后删旧字段,保证新旧版本兼容。
发布策略上,灰度或金丝雀发布先放少量流量,观察错误率、延迟和关键业务指标,再逐步扩大。功能开关可以把「发布」和「上线」分开,出问题时关开关比回滚更快。
回滚前要保留现场:日志、指标、trace、异常堆栈。回滚后确认数据一致性,尤其是消息、缓存和异步任务是否留下中间状态。
事后复盘关注可行动的改进:补充监控、加自动化检查、缩小变更批次、完善回滚脚本。把「这次是谁的错」换成「哪个环节让错误没被更早发现」。
易错点与自测
- 排障先止血再根因,但止血前尽量保留线程栈、堆 dump、GC 日志等现场。
- 堆内存上涨不等于堆泄漏,直接内存、元空间和线程栈要单独看。
- 扩连接池、扩线程池常常只是把压力转移到更下游。
自测:① 给出一次 CPU 高的完整命令链;② 说明如何区分内存泄漏和容量不足;③ 设计一个缓存故障时的三级降级方案。
参考资料
- jcmd 手册(Java 21) — 线程栈、堆信息、堆转储与 JFR
- Java SE 21 故障排查指南 — OOM、GC 与原生内存问题
- Google SRE Book:应急响应 — 事件分级、止血与复盘
- HikariCP 配置说明 — 连接池容量与超时参数