Appearance
问题定位步骤
定位:工程实践方法论。线上问题的排查不是灵感游戏,而是一个分层收敛的过程。从现象出发,沿"现象 → 日志 → 指标 → 链路 → 现场保留 → 假设验证"逐层缩小范围。每一步都要产出"排除掉了什么、剩下了什么"。
总体原则
- 先止血,再找根因:影响用户的问题先降级/回滚/扩容/限流,定位动作不能阻塞止血动作。
- 一次只改一个变量:边改配置边重启边换机器,等于销毁所有证据。
- 保留现场优先于修复动作:重启之前先 dump,现场没了,问题就从"可定位"变成"随缘"。
- 区分"变化"与"不变":最近发布、配置变更、流量结构、依赖版本——绝大多数问题的根因藏在最近的变化里。
第一层:现象收集
把"有问题"翻译成可操作的事实,先回答五个问题:
1)谁受影响:全部用户 / 部分用户 / 某机房 / 某版本客户端 / 内部账号。 2)什么表现:错误(错误码/堆栈)、超时、数据错误、变慢、功能不可用。 3)从什么时候开始:精确到分钟,对齐发布/配置变更时间线。 4)多大比例:错误率 0.1% 还是 100%,QPS 是否异常。 5)能否稳定复现:能复现的问题用测试环境追。不能复现的问题靠现场证据追。
实践技巧:把"全部用户都说慢"这种描述拆成"接口 P99 从 200 ms 涨到 5 s,错误率 0%,仅移动端"。量化之后,一半的候选原因已经被排除。
第二层:日志
- 按 traceId/userId/orderId 精确捞取,而不是 grep 关键字大海捞针。
- 关注第一条异常:后面的大量错误往往只是连锁反应(依赖超时导致的级联失败)。
- 对比正常与异常样本:同一接口,成功请求与失败请求的日志差异在哪一步分叉。
- 注意日志本身会说谎:被 try-catch 吞掉再打一行 warn 的异常、异步线程里丢失上下文的日志。
第三层:指标(Metrics)
日志告诉你"点"上发生了什么,指标告诉你"面"上的形状。按黄金四信号收敛:
- 延迟:P50 正常但 P99 飙高 → 长尾问题(个别慢查询/GC 停顿/个别实例异常)。
- 流量:QPS 是否突增(活动/重试风暴/被爬)。
- 错误:错误率突增的起始时间点,与变更时间线对齐。
- 饱和度:
- CPU:user 高(计算密集/死循环/正则回溯)还是 sys 高(系统调用/线程切换)。
- 内存:堆使用、GC 频率与耗时、Swap。
- 线程数、连接池使用率(DB/Redis/HTTP 各看一眼)。
- 磁盘 IO、网络带宽、文件句柄。
第四层:链路(Trace)
- 用分布式链路(如 SkyWalking/Zipkin/OpenTelemetry)拉出一条完整慢/错请求,看耗时分布在哪个 span。
- 排序法:把链路涉及的所有 RPC/DB/缓存调用按耗时排序,优先攻最大头。
- 区分两类耗时:
- 自身耗时高(进入服务后才开始慢):看该服务的 CPU/GC/锁。
- 排队/网络耗时高(发起调用到对端开始处理之间的差值):看连接池等待、序列化、网络。
第五层:现场保留
重启/回收实例之前,把证据固定下来,这是最容易被忽略的一步:
- CPU 高:
top -Hp找最耗 CPU 线程。→printf '%x'转十六进制。→jstack | grep <hex>拿到线程栈。连续抓 3 次间隔几秒,对比栈是否停在同一个位置(死循环/锁自旋会"钉"在同一行代码)。 - 内存高/OOM 前兆:
jmap -dump:live,format=b,file=heap.hprof(注意 live 会触发一次 Full GC,改变现场时先拍不带 live 的)。 - 线程异常:
jstack全量线程栈,找 BLOCKED / WAITING 堆积、死锁(jstack 末尾会直接报 Found one Java-level deadlock)。 - GC 异常:
jstat -gcutil 1000连续观察各代占用与 FGC/YGC 频率。 - 系统层:
dmesg | grep -i kill(看 OOM killer)、netstat/ss连接状态分布、iostat、sar -n DEV。 - 应用容器(K8s)环境:Pod 被重建前
kubectl describe pod(看 OOMKilled/重启原因)、kubectl cp把 dump 拷出来。
第六层:假设验证
收敛到候选原因后,用最小代价实验验证,而不是直接改了上线看效果:
- 写出假设的完整因果链:"X 变更导致 Y 资源被占满,表现为 Z 现象"。因果链解释不了所有现象的假设,多半是错的。
- 验证手段按代价从低到高:看代码/配置确认逻辑 → 测试环境复现 → 预发环境复现 → 线上灰度验证。
- 验证通过后,修复 → 观察 → 写复盘。复盘写四件事:时间线、根因、为什么没拦住(监控/发布流程/代码评审哪个环节失守)、后续 action。
JVM 常见案例
案例 A:CPU 100%
1)top 定位进程 → 2)top -Hp 定位线程 → 3)jstack 看栈。典型栈特征:
- 钉在正则匹配 → 正则回溯灾难,检查模式里可匹配空串的
(.*)*类结构。 - 钉在序列化/加密 → 大报文或算法退化。
- 钉在业务循环 → 死循环(条件永真)/ 大集合遍历。
- GC 线程 CPU 高 → 多半是内存问题(频繁 GC),不是 CPU 问题,转向看堆。
案例 B:内存泄漏 / OOM
1)jstat -gcutil 确认 Old 区只涨不降、FGC 后回收无几。2)两份间隔 dump 用 MAT 对比 Dominator Tree,找增量最大的对象。3)看 GC Roots 引用链定位持有者。经典根因:
- static Map 做缓存无淘汰(应换 Caffeine 设最大容量/过期)。
- ThreadLocal 在线程池里不 remove(线程不死,Entry 一直可达)。
- 无界队列堆积(线程池用 LinkedBlockingQueue 无界、MQ 消费积压塞进内存)。
- ClassLoader 泄漏(热部署场景,动态类加载器无法回收)。
案例 C:长尾延迟
P99 高但 CPU/错误率正常 → 抓单请求链路看长尾 span。JVM 视角查 jstat -gcutil 的停顿(G1 的 to-space exhausted、CMS 的 promotion failed/concurrent mode failure),以及 safepoint(-XX:+PrintSafepointStatistics)。
网络常见案例
- 超时但无错误日志:连接池耗尽,请求在 borrow 阶段排队。看池监控的 active/wait 指标;
ss -s看连接状态。 - TIME_WAIT 堆积:短连接高频创建销毁,
ss -s输出确认;改长连接或调内核参数。 - 偶发连接重置:中间层(LB/NAT)空闲连接超时回收,应用侧仍在用。查 keepalive 配置与中间层超时值的大小关系。
- 跨机房调用慢:RTT 基线乘以调用次数;检查是否在循环里做跨机房 RPC。
DB 常见案例
- 慢 SQL 突增:直接看慢查询日志。常见根因是执行计划切换(统计信息过期、隐式类型转换导致索引失效、深分页 limit offset)。
- 锁等待/死锁:
SHOW ENGINE INNODB STATUS看 LATEST DETECTED DEADLOCK,关注事务里更新顺序不一致 + 事务内夹带 RPC 拉长时间。 - 连接池耗尽:上游表现为获取连接超时。看
show processlist是否大量慢查询占满连接;区分"DB 真慢"与"池太小/泄漏"。 - 主从延迟:写后立即读从库读不到数据。看 Seconds_Behind_Master,读请求强制走主或半同步。
工具速查表
| 层 | 命令/工具 |
|---|---|
| 进程/线程 | top、top -Hp、pidstat -wt |
| 线程栈 | jstack、Arthas thread / thread -b(锁阻塞树) |
| 内存 | jmap -dump、jhat、MAT、jstat -gcutil、Arthas dashboard |
| 运行时热诊断 | Arthas:watch/trace/tt(不重启方法级观测) |
| 网络 | ss -s、tcpdump、ping/mtr |
| 系统 | dmesg、iostat、vmstat、sar |
| DB | 慢查询日志、EXPLAIN、SHOW ENGINE INNODB STATUS、show processlist |
| 观测平台 | Metrics + Trace + 日志三位一体联动定位 |
一句话总结
现象量化 → 日志找分叉 → 指标看形状 → 链路定位 span → 动手前保留现场 → 因果链闭合后验证。 排查能力强弱的区别不在记住了多少命令,而在于每一步是否产出明确的"排除/锁定"结论。
2026-10 依据主题重写(原文件为空壳)。