Skip to content

问题定位步骤 ​

定位:工程实践方法论。线上问题的排查不是灵感游戏,而是一个分层收敛的过程。从现象出发,沿"现象 → 日志 → 指标 → 链路 → 现场保留 → 假设验证"逐层缩小范围。每一步都要产出"排除掉了什么、剩下了什么"。

总体原则 ​

  • 先止血,再找根因:影响用户的问题先降级/回滚/扩容/限流,定位动作不能阻塞止血动作。
  • 一次只改一个变量:边改配置边重启边换机器,等于销毁所有证据。
  • 保留现场优先于修复动作:重启之前先 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 依据主题重写(原文件为空壳)。