从现象到根因的黄金30分钟法则
目录导读
- 排查前的思维准备:异常分级与响应策略
- 五步排查法:从症状到根源的标准化流程
- 高频场景实战:数据库锁死、CPU飙高、内存泄漏
- 工具链活用:日志、监控、链路追踪的协同
- 常见问答(Q&A):新手易踩的6个雷区
- 总结与自查清单:用模板固化排查动作
排查前的思维准备:异常分级与响应策略
问题:系统告警突然响起,你应该先做什么?
回答: 先看影响范围,再动代码,90%的崩溃源于“先重启再说”的冲动,对于S级(全站不可用)或A级(核心功能受阻)异常,立即执行以下动作:

- 止血:回滚最近一次变更、降级非核心服务、限流(如Nginx限制单IP QPS)。
- 取证:保留进程快照(如
jstack -l pid)、内存dump(注意:大内存场景需提前配置-XX:+HeapDumpOnOutOfMemoryError)。 - 同步:在钉钉/飞书群发送“当前影响、已执行动作、预计修复时间”模板。
踩坑案例:某团队在双11大促期间,因“错误率突然升高”直接重启了订单服务,结果导致购物车数据丢失,事后查明是配置中心推送了一个空值,但重启后旧配置被清空,反而加重了故障。正确处理:应该先切流至备用版本,保留现场再分析配置变更日志。
五步排查法:从症状到根源的标准化流程
第1步:可视化“病人状态”
- 看大盘:登录Grafana/Prometheus,观察CPU、内存、磁盘IO、网络IO的“四维曲线”,如果某指标突然从平滑变为脉冲式抖动,这就是切入点。
- 查日志:重点看ERROR级别日志的时间段与量级,用
grep "ERROR" app.log | tail -200配合awk '{print $1}' | sort | uniq -c统计错误频率。
第2步:定位“病灶区域”
- 如果是API接口慢:用
top命令找出高CPU进程,再用strace -p pid追踪系统调用,常见凶手是“无索引SQL”或“死循环代码”。 - 如果是数据库负载高:执行
show processlist(MySQL)或查询pg_stat_activity(PostgreSQL),找出state为Lock或Active超过30秒的会话,直接KILL(kill query id)。
第3步:深度诊断“病因”
- CPU飙高:
top -H -p pid找出高线程ID,然后jstack pid | grep -A 30 nid=0x转换的十六进制,定位到具体代码行。 - 内存泄漏:
jmap -histo pid | head -20查看对象分布,如果char[]或byte[]数量爆炸,大概率是日志错误或JSON序列化遗漏了释放。
第4步:根因验证
- 模拟复现:在测试环境构造相同条件(如并发数、数据集大小),确认修复后不再触发。
- 观察指标:修复上线后,监控页面“红色曲线”是否在5分钟内恢复基线。
第5步:复盘归档
- 在Confluence/飞书文档中记录:
异常时间→首次发现现象→排查链路→根因→修复方案,经验值+1。
高频场景实战:数据库锁死与CPU飙高
场景A:数据库“锁等待”导致接口超时
现象:业务监控显示“下单接口”超时率从0.1%飙升至30%,但CPU和内存正常。
排查:
- 查MySQL
information_schema.INNODB_TRX,发现事务TRX_ID=12345已运行120秒未提交。 - 根据
trx_mysql_thread_id,连接该线程:select * from processlist where id=xxx,发现它在执行UPDATE inventory SET stock=stock-1 WHERE product_id='A'。 - 查看
INNODB_LOCK_WAITS,发现被另一个正在执行SELECT ... FOR UPDATE的会话阻塞(该会话由手工数据导出触发)。
解决:KILL阻塞会话,并给业务加lock_timeout=5s保护。
场景B:Java服务CPU占用100%
现象:告警群通知“订单服务CPU超90%”,top看到进程PID=9876。
排查:
top -H -p 9876找到线程PID=12345占用300% CPU。printf '%x\n' 12345转换为十六进制0x3039。jstack 9876 | grep -A 100 'nid=0x3039'发现线程卡在:while(true) { // 某缓存框架的自动重试逻辑,未设置最大重试次数 redisTemplate.opsForValue().increment(key); }解决:添加
RetryTemplate的最大重试次数和指数退避策略。
工具链活用:日志、监控、链路追踪的协同
| 工具类型 | 推荐方案 | 排查场景 | 核心命令/配置 |
|---|---|---|---|
| 日志 | ELK + Filebeat | 查错误堆栈、慢查询日志 | grep "Error" \| less;配置slow_query_log=1 |
| 监控 | Prometheus + Grafana | 看CPU/内存的“时间序列突变” | 关注irate(rate)导数,而非绝对值 |
| 链路追踪 | SkyWalking / Jaeger | 查调用链中耗时最长的环节 | 看span的status、duration、error
|
| 远程调试 | Arthas | 在线查看方法入参、返回值 | watch com.example.service.* invoke '{params,returnObj}' |
技巧:日志中加“标记字符串”(如["traceId":"xid_123"]),方便Grep过滤,同时启用AsyncAppender避免日志I/O阻塞业务线程。
常见问答(Q&A):新手易踩的6个雷区
Q1:告警来了,要不要先重启服务?
A:只有在“无法登录服务器”或“服务已完全不可用”时才重启,否则先保留现场,执行jstack和jmap,重启会丢失临时文件,可能掩盖根因。
Q2:如何区分是代码Bug还是基础设施故障?
A:用“排除法”:
- 检查同一集群的其他实例——如果只有一台出问题,大概率是硬件/本地配置。
- 检查最近变动的代码版本——用
git log --oneline看最近提交。 - 检查依赖服务(数据库、Redis、MQ)——如果它们都报错,可能是网络分区。
Q3:日志太多了,如何快速找到关键信息?
A:用tail -f app.log | grep -E "(ERROR|FATAL)" 实时过滤;或设置日志级别为WARN来减少噪音(但在排查期间可临时改到DEBUG,注意回滚)。
Q4:磁盘空间占满,如何定位罪魁祸首?
A:du -sh /var/log/* | sort -hr | head -10 看日志文件大小;lsof | grep deleted 看已被删除但未释放的文件句柄(常见于日志滚动时未重启应用)。
Q5:为什么“重启大法”在某些场景会失效?
A:因为重启会清空内存中的热点数据,导致缓存雪崩——请求全部打向数据库,反而引发更严重的故障,正确做法是限流+分批重启。
Q6:微服务场景下,如何快速定位“跨服务慢调用”?
A:关闭所有服务的本地缓存,用链路追踪工具(如SkyWalking)查看调用链,重点看span的duration和responseStatus,若某服务dubbo://order-service/send耗时2秒,则进入该服务内部继续追踪。
总结与自查清单:用模板固化排查动作
标准化排查步骤(打印贴键盘旁)
- 1分钟:确认影响范围→通知团队→启用降级/限流。
- 3分钟:拉取服务日志+系统监控+最近变更列表。
- 5分钟:定位是“代码变更”、“配置变更”还是“外部依赖”导致。
- 10分钟:如果包含数据库,执行
show processlist和INNODB_LOCK_WAITS。 - 20分钟:通过
jstack或strace定位具体代码行。 - 30分钟:制定修复方案(回滚、改配置、加限流阈值)并上线验证。
避坑清单
- [ ] 是否开启了
-XX:+HeapDumpOnOutOfMemoryError? - [ ] 日志中是否包含了
traceId或requestId? - [ ] 数据库的
max_connections和wait_timeout是否合理? - [ ] 是否已关闭“自动重启”策略(如K8s的
restartPolicy: Always)? - [ ] 是否有“排查文档”模板,避免每个人重新造轮子?
系统异常排查的效率,取决于你平时积累的“排查肌肉记忆”,从今天起,把每个异常当作一次演习——记录时间、记录步骤、记录教训,当故障再次降临时,你会感谢那些曾经被耐心记录的自查清单。