尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

徒手挖掘:线上故障排查的核心能力,从jstack到EXPLAIN的实战指南

徒手挖掘:线上故障排查的核心能力,从jstack到EXPLAIN的实战指南 你有没有遇到过这种情况线上一个接口突然变慢日志只有一句timeout监控图上一片飘红大家靠“重启大法”恢复了三次每次都管用三四天然后它在另一个时段再次出现。第四次的时候所有人都沉默了因为你已经找不到可以继续“重启”的借口了。很多团队把这类问题叫“偶发故障”。但真正的偶发故障其实很少大部分只是你还没有往下挖到那层必然原因而已。这两年的技术圈特别推崇“脚手架”、“低代码”、“智能运维”这当然提升了产出效率。但有一项能力被悄悄低估了——在工具失效、日志不够、文档不对的时候你愿不愿意关掉舒服的仪表盘蹲下来亲自扒开泥土看一眼最底层的东西。**真正的工程师是可以徒手挖掘的人。**他们不会停在“现象已经恢复”而是会一路挖到“根因已经确认、修复已经验证、同类问题不会再犯”。本文不打算讨论什么宏大叙事而是想通过两个非常典型的线上问题拆解“徒手挖掘”到底是怎么回事。你会发现它既不是玄学也不完全依赖经验它是一套可以训练的思路先看现象再分层再定位再验证。我会把具体命令、代码片段、判断路径都写出来哪怕你现在就在值班也能照着做一遍。1. 什么是“徒手挖掘”为什么工程师必须具备这项能力先统一一下概念。我这里说的“徒手挖掘”不是指不借助任何工具而是指在既定工具链断掉之后你还能依靠操作系统、运行时、接口现场和源码去定位问题。它本质上是从现象层跨越到机制层的一种排障方式。现在很多应用是分层的前端有网关后端有框架框架下面有容器容器里面是操作系统操作系统再往下是 CPU、内存和磁盘。日常开发中绝大多数工程师只需要和最上面两层打交道。业务变化太快框架帮你封装了事务、连接池、线程调度与配置管理你根本不需要关心底层细节。但问题恰恰出在“不需要关心”四个字上。当框架替你处理了 99% 的逻辑剩下的 1% 一旦出错你既没有处理经验也缺少必要的排查直觉。比如为什么连接池明明设置了maxActive20请求量没到 20 就开始报获取连接超时为什么 JVM 堆内存看起来只用了 30%接口却卡在高延迟附近为什么 MySQL 有索引条件也写了查询还是要扫描全表为什么 Redis 慢查询监控显示平均延迟不到 1ms业务链路却出现偶发超时这些问题有一个共同点**应用层给出的信息是“结果”不是“原因”。**你盯着报错日志看一百遍也只能看到“我等不到连接”或“我超时了”至于谁把连接拿走了、为什么迟迟不归还、此刻线程在哪个位置等待日志基本不会告诉你。只有当你愿意往下挖一层去看线程栈、看堆内对象分布、看执行计划、看系统调用你才能从“现象”里走出来。这种往下挖的动作就是徒手挖掘。它不是某个岗位的专属技能而是所有后端开发、运维、测试甚至前端工程师在关键时候救场的能力。2. 那些年我们挖过的最典型的“三层土”实战里我总结了一个排障经验当你面对一个线上故障不要急着改代码先把自己的视角从业务逻辑切到运行环境。大部分问题都埋在三层土里你必须一层一层往下刨。**第一层进程层。**应用进程是否活着、线程数是否异常、GC 频率如何、文件描述符是否泄漏、日志是否在刷同一个错误。**第二层资源层。**CPU 使用率、内存占用、磁盘 IO、网络连接数、TCP 重传率、连接池/线程池水位。**第三层依赖层。**数据库、缓存、消息队列、外部接口等下游服务的状态以及它们与当前链路之间的协议交互。很多人遇到问题先怀疑数据库这是倒着挖。顺序应该是先看自己的进程再看本机资源最后才追依赖。如果一上来就登录数据库看往往会绕一大圈最后发现是自己的连接池配置写错了。举个例子。有一个服务每天凌晨会报一批Connection is closed白天一切正常。开发团队查了很长时间数据库甚至考虑过是不是 DBA 改了最大连接数。然后有人去看了应用日志的时间分布发现报错全部集中在定时任务启动后的 3 秒内。再往后挖发现定时任务里有个工具类在重复创建数据库连接任务并发执行时把连接数打到了上限后续连接全部被判拒绝。问题不在数据库而在于连接管理方式。但如果只看数据库层面这个结论可能永远出不来。这就是分层排查的意义。**每一层有每一层该看的信息不要跳过当前层直接去怀疑下一层。**跳过意味着你失去了本可以定位到根因的证据。3. 徒手挖掘的标准动作从进程和系统调用入手下面列几个我几乎每次深挖问题都会用到的“标准动作”。它们不是最深的原理但却是最容易让你看到真相的第一步。3.1 先看进程在哪如果你负责的是一个 Java 应用第一步不是打开 IDE而是找到进程并确认它的状态jps -l # 输出示例 # 12345 /opt/app/order-service.jar如果jps找不到进程说明应用可能根本没起来如果找到了就可以继续看线程情况。3.2 看线程在等什么线程卡住、响应变慢、CPU 飙高这些症状最终都要落在线程栈上。但jstack不能乱用生产环境它会把进程暂停一下不过对绝大多数应用来说短暂停顿可以接受。建议先 dump 两到三次间隔 5 秒对比线程状态变化。jstack 12345 /tmp/jstack_1.txt sleep 5 jstack 12345 /tmp/jstack_2.txt然后重点看这几类线程RUNNABLE正在执行的线程不代表没毛病要看它执行到哪个栈帧。WAITING/TIMED_WAITING在等待锁或条件变量如果大量业务线程都在等待说明池子已经耗尽。BLOCKED被某个锁挡住需要进一步找持锁线程。3.3 看 JVM 堆内存分布如果你怀疑内存或 GC 问题可以先看堆内对象的分布快速判断是不是某个业务对象被大量创建jmap -histo:live 12345 | head -30这条命令会触发一次 Full GC生产环境要小心。如果实在不能接受就换用jmap -histo不加:live别硬来。3.4 用系统调用看底层行为很多在应用里看不到的细节到了系统调用层面就会暴露出来。strace是 Linux 下排查系统调用问题的利器适合看文件读写、网络连接、信号处理等。不过在高并发 Java 应用上使用时要谨慎因为它会严重拖慢进程通常先在测试环境复现再决定是否在生产上短时间采样。strace -f -p 12345 -e tracenetwork -o /tmp/strace_network.log网络类问题还可以抓包tcpdump -i eth0 host 10.0.0.1 and port 3306 -w /tmp/mysql.cap抓包产生的文件可能很大实际使用时要加上-c限制包数量或者按时间窗口切片。4. 实战案例一一个“三天一挂”的 Java 服务我们来看一个典型的徒手挖掘过程。这个服务的现象非常规律运行三天后接口响应越来越慢随后服务直接不可用重启之后立刻恢复再过三天又复现。团队先按惯性看了日志发现大量RedisConnectionFailureException和Unable to connect to Redis。于是怀疑 Redis 有问题联系中间件团队反复排查结论是 Redis 本身没有任何异常。随后又怀疑网络抖动但网络监控也没有发现问题。真正开始往下挖是从一次服务“假死”但还没有完全崩溃时开始的。当时先执行了jstack看到大量业务线程处于如下状态http-nio-8080-exec-7 #27 daemon prio5 os_prio0 java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(...) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(...) ... at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(...) at redis.clients.jedis.JedisPool.getResource(...)这里描述的不是某个具体项目的源码而是一类经典结构。你真正在现场看到的堆栈可能类名不同但模式几乎一致**大量线程停在一个连接池的borrowObject方法上说明连接获取已经阻塞。**接着再看 Redis 连接池的配置spring.redis.jedis.pool.max-active8 spring.redis.jedis.pool.max-idle8 spring.redis.jedis.pool.min-idle0max-active8也就是最多同时 8 个连接。如果有 8 个线程分别持有一个连接并且它们都在等待一个永远不会返回的 Redis 命令响应那么第 9 个线程会一直卡在borrowObject上。一旦卡住业务线程积压Tomcat 线程池被占满服务自然“假死”。但还有一个疑问为什么是三天后才挂这要靠下一层挖掘来解释。继续使用jstack看那 8 个持锁线程发现它们都阻塞在类似这样的代码上ListString values redisTemplate.opsForList().range(key, 0, 1000);这个操作本身不至于卡死除非它在等待一个永远不会到达的网络响应。而 Redis 命令之所以不返回是因为客户端和 Redis 之间可能已经在 TCP 层断连但连接池中的连接对象还认为自己是可用的。于是应用会卡到 socket 读超时而读超时时间又设置得非常长。当请求流量逐渐堆积某个时刻恰好 8 个连接全部进入这种“僵尸”状态整个服务就被击穿了。这个问题要真正根除需要修改的是# 设置合理的读超时避免线程长时间阻塞 spring.redis.timeout2000ms # 如果使用 Lettuce可以开启连接校验 spring.redis.lettuce.pool.max-active16 # 取用连接前校验连接可用性 spring.redis.jedis.pool.test-on-borrowtrue这整个链条靠应用日志根本看不到。你只有看见线程栈才能知道“阻塞发生在连接池”只有进一步看见持锁线程才能知道“僵尸连接来自 Redis”只有再追到 TCP 层和超时配置才能解释“为什么是三天”。这就是徒手挖掘的真实路径每个问题都至少要往下挖三次才能看到全貌。5. 实战案例二一条越来越慢的 SQL第二个案例同样典型。业务反馈某个报表查询越来越慢刚开始是 200ms半年后变成 3 秒。数据库团队看slow_log确认有一条 SQL 占到慢查询总数的 80%。表结构非常简单CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) DEFAULT NULL, buyer_uid bigint NOT NULL, status tinyint DEFAULT NULL, amount decimal(10,2) DEFAULT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB;慢 SQL 长这样SELECT order_no, amount FROM t_order WHERE buyer_uid 12345 AND status 1 ORDER BY create_time DESC LIMIT 10;第一眼看上去很冤buyer_uid是查询条件status有索引create_time参与排序为什么不快很多人这时候会选择加一个联合索引解决。如果你开始动手加索引就已经是在盲改了。正确的做法是先让它告诉你原因先执行EXPLAIN SELECT order_no, amount FROM t_order WHERE buyer_uid 12345 AND status 1 ORDER BY create_time DESC LIMIT 10;执行计划往往会显示typeindexkeyidx_status_create_timerows非常大。typeindex表示它虽然用了索引但实际上是在按索引顺序做全索引扫描相当于把status1的所有行都读出来再进行create_time排序。因为status的区分度太低了这个索引根本没有有效地过滤数据。走到这一步很多人会得出“索引不对改成idx_buyer_uid_status_create_time”的结论。这个方向没错但还没挖到底。真正需要回答的问题是为什么这条 SQL 用了半年才开始变慢继续挖发现buyer_uid这一列存在隐式类型转换的风险。继续看表结构时才发现buyer_uid在业务代码里被传成了字符串String buyerUid userService.getBuyerUid(request);MySQL 在比较时会对字段做类型转换导致buyer_uid上的索引失效。但因为查询里还有status和create_time优化器可能选了一条看似“还有救”的路径实际却是在一大片数据里做过滤。半年过去订单数据量翻了数倍这条“假索引”路径终于撑不住了。正确的修复有两步第一步修改业务代码把类型对齐long buyerUid Long.parseLong(userService.getBuyerUid(request));第二步根据实际业务查询模式把联合索引建对ALTER TABLE t_order ADD INDEX idx_buyer_uid_status_create_time (buyer_uid, status, create_time);这个案例想说明的是**慢 SQL 的根源往往不在 SQL 本身而在 SQL 前后那几层。**如果只停留在“给这个 SQL 加索引”的层面你确实能暂时解决问题但半年后订单量再涨一波新的慢 SQL 会出现在另一个查询上因为根子是类型混乱和索引设计没有跟上业务变化。6. 比工具更重要的是建立一套底层的因果思维工具只是“手”因果思维才是“大脑”。我在看很多工程师排障时发现他们往往不是不会用工具而是没有建立起一套“现象 - 原因 - 证据 - 修复”的闭环。比如看到Connection is closed直接反手加大连接池看到 CPU 高就盲目扩容看到慢 SQL就加索引。这些操作在表面上有用但往往掩盖了更深的因果关系。徒手挖掘时建议始终带着下面这个因果链想问题**现象是什么**精确描述包括开始时间、持续时间、影响范围、触发条件。**哪一层最先出现异常**先看进程层、资源层、依赖层的顺序别跳层。**这个异常上游发生了什么**比如连接池耗尽上游是否真的有过高并发还是因为某个连接泄漏**这个异常会导致什么结果**比如线程堆积会导致 GC 压力增大GC 又会导致更多超时形成雪崩。**修复后如何验证**单独修一个点不算完要观察一个完整业务周期确保没有复发。利用jstack你应该能画出一张业务线程状态的图利用EXPLAIN你应该能画出数据扫描路径利用strace你应该能画出系统调用序列。能画出这些图你才真正明白了系统在干什么。很多问题看起来是随机跳动其实是固有缺陷在某个边界条件下被激活。你要找的不是那个让你“这次侥幸恢复”的按钮而是那个在特定条件下必然触发的开关。7. 徒手挖掘中常见的误区与反思“徒手挖掘”不是鼓励你把所有东西都推倒重来更不是让你不信任任何框架。真正的徒手挖掘是在工具和框架失效时还不至于失去判断力。这里有几个常见误区值得反思。7.1 把“表面修改”当成“根因修复”修改连接池参数、增大超时时间、加一台机器这些操作本身没有错但如果没找到根因它们的有效时间通常不会太长。比如前面那个 Redis 连接池“僵尸连接”的问题如果你只是把max-active从 8 调到 80那么很快会出现 80 个线程同时卡住的情况。你也只是把故障阈值推迟了一点。7.2 只验证“现象消失”不验证“机制正确”有些时候现象消失了但不一定是你的修复生效了也可能是高峰期过了或者下游服务恢复了。怎么区分用机制来判断。你的修复是不是对着因果链的某一环下的手如果是即使现象暂时不消失也值得继续观察如果不是哪怕现象消失了也要警惕下一次爆发。7.3 在错误的一层浪费太多时间有的问题你开了几十次会都查不出来是因为团队一直在应用层讨论而问题出在操作系统或网络层。反之亦然一些明显是业务逻辑 bug 的问题被一堆人拉到中间件层面验证了半天。在排障之前花 10 分钟先确定“这个问题最可能存在于哪一层”往往比直接动手更高效。7.4 拿着工具乱挖没有形成记录还有一种情况是日志已经打出来了但是没人把它和前面的监控图对照起来。jstack打了但没保存EXPLAIN看了但没复制tcpdump抓了但没分析。挖掘出来的证据到不了团队其他人手里下一次复现时又要从头再挖。每一次排障过程都应该像一次现场考古挖掘每一步的“土层样本”都必须记录下来。8. 如何保持“徒手挖掘”的能力这项能力不是天生的它会随着你接触业务越深、越依赖框架越弱。要保持住需要刻意训练。8.1 维护自己的“排障工具箱”我建议每个后端工程师都建立一个自己的排障笔记包含JVM 常用命令jps、jstack、jmap、jstat、jcmdLinux 常用命令top、vmstat、iostat、ss、strace、lsof数据库常用命令EXPLAIN、SHOW ENGINE INNODB STATUS、SHOW PROCESSLIST抓包工具tcpdump、Wireshark的基本过滤语法。这些命令不需要每天用但要用的时候你得能流畅地组合起来。建议在测试环境模拟几个经典故障线程池耗尽、死锁、慢查询、内存泄漏然后逼自己只靠命令行把根因找出来。8.2 每个月读一段核心框架源码不要只把源码当成晋升面试的谈资它可以帮你建立“机制感”。比如你读过AbstractQueuedSynchronizer就能理解线程阻塞在await和park上分别代表什么你读过连接池的borrowObject源码就能明白为什么“拿到连接”和“使用连接”之间还有一大段校验逻辑你读过 MyBatis 的Executor实现就能理解#{}和${}在 SQL 分析阶段到底改变了什么。这些机制知识会在你看到线程栈上的某个类名时自动唤醒。8.3 复盘时不只讲“怎么修的”要讲“怎么挖到的”团队复盘时建议大家把重点放在“发现路径”而不是“修复方案”上。比如你可以这样记录第一次发现是什么时间通过什么指标发现的当时采用了哪些手段哪些有效哪些无效什么时候开始使用jstack/EXPLAIN/tcpdump最终是哪一条日志、哪一行堆栈、哪一个执行计划提供了决定性证据这种复盘比“我们加了索引恢复了以后注意”有价值得多。它能训练大家的问题定位思路也能积累团队自己的“土层地图”。9. 结论技术与责任感都要在泥土里练出来回到文章标题。真正的工程师为什么是“徒手挖掘”的因为总有一天工具会变得不够聪明日志会被打得残缺框架会在某个边界上露出不可预测的行为。那一瞬间决定你能不能解决问题的不是你会几个 API、背过多少八股文而是你有没有办法在混乱里往下挖到真相。同时徒手挖掘也是一种责任感。它意味着你不满足于“服务恢复了”也不满足于“领导没追问了”你坚持要看到原因看到机制看到证据链条的完整性你还愿意把自己的挖掘路径记录下来让下一个遇到类似问题的人少走一段长路。这篇文章不是让你推翻框架、回归底层重复造轮子而是想让你在享受云原生和低代码带来的便利时不要丢掉最基本的工程直觉所有抽象层都会泄漏真正的工程师是那个不怕弯下腰、伸手摸到底、再把整块土壤翻出来给大家看清楚的人。如果你现在正处于一个排障无头绪的时刻不妨从执行jstack、EXPLAIN、lsof这样的命令开始。一次只挖一层一层只找一条线索线索链完整了问题自然就浮出水面。方法不难难的是你愿意蹲下来。
返回列表