Arthas实战:从CPU飙高到秒定位线上Java问题,十分钟救火指南

发布时间:2026/10/22 11:40:51
Arthas实战:从CPU飙高到秒定位线上Java问题,十分钟救火指南
先说结论Arthas 这东西我一开始是拒绝的。命令行工具嘛命令又多参数又长光看文档就够劝退的。直到有次线上应用突然 CPU 飙到 99%接口大面积超时流量还在源源不断进来——按老办法先重启再说可重启只能暂时缓解第二天问题必然复现。就在我满头大汗翻监控、查日志的时候旁边某老哥打开终端敲了几行指令不到十分钟就锁定了罪魁祸首一段正则表达式在极端输入下触发了灾难性回溯。那一刻我才真正明白为什么身边人都在喊“真香”。这篇东西不是官方文档的复述而是我踩了一堆坑之后攒下来的实战经验。Arthas 到底能干什么、适合谁来用、有哪些命令是日常排查的救命稻草、哪些坑必须避开我都会写清楚。如果你还在用“加日志 重启 等复现”的土办法排查线上问题这篇文章值得你花十分钟读完。1. 为什么我一直没碰 Arthas直到线上事故把我按在地上摩擦1.1 传统排查方式的死穴上线后你就是个瞎子大部分 Java 服务出问题第一反应是查日志。但日志里没打参数、没打异常堆栈的情况太常见了等你想补日志又要发版、重启、复现一轮下来两三小时没了。监控面板能告诉你 CPU 高、响应慢但它不会告诉你“到底是哪行代码导致 CPU 高”。更尴尬的是 dump 线程快照生产环境几十个线程堆栈堆在一起光看那些at行就能看得人眼冒金星而且 dump 完通常还要重启才敢去分析现场早就变了。我做过一个类比传统排查就像打着手电筒在黑暗的机房找一根保险丝你知道保险丝肯定在某处烧了但机柜那么高、线缆那么多你只能一根根摸。而且摸的时候机器还在继续烧。Arthas 做的事情就是站在机柜前告诉你保险丝在哪、为什么烧、现在温度多少。1.2 不需要重启的在线诊断为什么是刚需Arthas 的核心价值是四个字在线增强。它通过 Attach 机制挂到已经运行的 JVM 上在不修改代码、不重启服务的前提下动态增强目标类的字节码然后实时获取方法调用参数、返回值、异常、执行耗时甚至可以反编译线上 class、调整日志级别、热更新代码。你想线上问题最值钱的是“现场”。请求参数是什么、当时线程栈是什么、内部调用的哪条分支慢这些东西一旦重启就全没了。Arthas 不管这些进程还活着它就有办法把现场给你保留下来让你像开着 debugger 一样去观察一个生产环境。举一个很简单的例子某个接口偶发超时但没异常、没错误日志你怎么查传统做法是等下次复现抓包、开 debug。但如果用 Arthas 的watch命令可以在方法入口和出口把参数和返回值打印出来配合trace看方法内部每个子调用耗时基本上一轮就能定位到是 Redis 慢了还是数据库慢了。1.3 什么场景适合 Arthas什么场景要绕道先说说哪些场景是我觉得“非它不可”的线上 CPU 飙升、线程卡死、死锁问题需要快速定位到具体代码行。方法入参、出参、异常没有打印日志但重启代价高不能随意发版。接口偶发超时、调用链路上某个中间步骤耗时异常想看清楚耗时分布。线上 class 版本和本地不一致需要反编译确认到底部署的是什么代码。临时修改逻辑做止血比如绕过一个有问题的分支且不想立刻发版。但也有绕道的场景如果你本地能稳定复现问题那本地 debug 显然比线上折腾更轻松如果目标进程启动时没有足够的 JDK 诊断权限或运行在非常受限的容器环境里Arthas 也连不上去这时候只能靠日志、APM 那些了。另外它不适合做性能测试工具。虽然它本身的额外开销很小但你在压测时用watch高频打点反而会干扰结果。2. 五分钟跑通 Arthas安装、启动和第一个命令的真香瞬间2.1 安装比想象中简单一个包就搞定Arthas 的安装不像我想象中要配一堆环境变量它就是一个可执行的 jar 包或者直接用包管理工具装。我习惯的方式是下载arthas-boot.jar放到服务器某个固定目录然后执行java -jar arthas-boot.jar跑起来后它会列出当前机器上所有 Java 进程让你输入序号。如果你已经知道进程 PID也可以直接用java -jar arthas-boot.jar 12345还有一种更省事的方式下载as.sh脚本后直接执行效果一样。我第一次跑的时候内心是“就这”——它连下载依赖都是自动的完全不用手工配JAVA_HOME。前提是机器上得有和 JVM 匹配的 JDK 环境一般跑 Java 应用都有。启动后你会进入一个arthas开头的交互命令行整个排查过程就在这个终端里完成。2.2 连接不上进程八成是这几个原因我见过不少同事卡在第一步明明进程在跑Arthas 却连不上去。常见原因大概有三类。第一权限不足。Arthas 需要 attach 到目标进程如果你当前用户不是启动 Java 进程的用户或者没有足够的系统权限attach 会失败。解决办法是切到同一个用户执行或者用 sudo。第二容器环境受限。不少容器基础镜像为了瘦身把 JDK 的jstack、jmap相关组件裁剪掉了Arthas 依赖的tools.jar没在镜像里。这时候要么换完整 JDK 镜像要么把缺失的工具包补进去。第三进程号选错。机器上有多个 Java 进程比如有网关、有多个应用实例你手一抖选错了进去后看到的线程栈当然对不上。建议先用jps -l确认 PID 和应用名的对应关系再让 Arthas 挂上去。如果你用的是 Java 17 及以上还要注意新版本的模块化限制可能需要额外开 JVM 参数--add-opens。这些在官方文档里都能找到但第一次遇到时真会卡半天。2.3 第一条命令dashboard 告诉我这台机器摔在哪进入交互命令行后我建议新人先敲dashboard。它会立即刷出一个实时面板展示整个 JVM 的大盘CPU 使用率、内存占用、GC 频率和耗时、加载类数量以及各个线程的状态、CPU 占比、是否等待锁等。我第一次敲完 dashboard 是有点震撼的因为原本要jstack、jstat、top -Hp好几个命令拼凑的信息它一个界面全部给你。而且它是实时刷新的退出按q就行。从这里你能一眼看到哪个线程 CPU 占比最高哪个线程卡在 WAITING 状态GC 老年代是不是疯狂在回收。dashboard 解决的问题是“这台机器现在整体什么状态”它不负责告诉你业务代码哪里有问题但能给你一个精确定位的方向。所以我的习惯是任何线上排查先进 dashboard再往下钻。3. 日常排查三板斧dashboard、thread、watch 到底怎么用3.1 thread查线程状态定位打满 CPU 的元凶如果你从 dashboard 看到某个线程 CPU 占比一直高接下来自然要去看它的堆栈这个活交给thread。thread命令最常用的是带-n参数显示 CPU 占用最高的前几个线程thread -n 3输出里会直接给出线程名、线程 ID、CPU 占比、状态以及完整的调用栈。注意那个“线程 ID”和系统里的 PID 不是一回事但这个 ID 足够你在top -Hp里对上了。还有一个高频用法是看死锁thread -b它会找出当前被阻塞并且阻塞了其他线程的那个线程。我们遇到过很多次数据库连接池耗尽、Redis 客户端线程卡死之类的问题都是靠这一条命令直接揪出“元凶线程”等待锁的位置。另外按状态过滤也很实用。大批线程不干活你想快速看它们为什么都 WAITING可以执行thread --stateWAITING很多看起来“卡住不动”的服务本质就是一堆线程在某个锁或某个空转循环里同时停下来。看状态就能快速判断是等锁、等 IO 还是纯粹死循环。3.2 watch像给方法装了摄像头入参出参一次看个透如果说 thread 是看整体状态watch 就是钻到某一个具体方法里去看细节。举个例子订单服务里有这么个方法public OrderResult createOrder(OrderParam param)你想知道线上真实调用时param 里到底传了什么返回值是什么有没有抛异常。传统做法是加日志、发版、等流量但 Arthas 只需要一行命令watch com.demo.OrderService createOrder {params, returnObj, throwExp} -x 3-x 3表示展开对象详情到第三层避免一坨 toString 糊在屏幕上。这样每次createOrder被调用终端都会打印出入参和返回对象的内容等于给这个方法装了摄像头。watch 还支持条件表达式比如只观察金额大于一万的订单watch com.demo.OrderService createOrder {params[0].amount, returnObj} params[0].amount 10000这个能力非常关键因为线上流量大全量打印容易被刷屏也会产生不必要的 IO。加了条件之后只输出可疑请求排查效率翻倍。不过要提醒一句watch 是对目标方法做动态增强在高频调用场景下会带来额外开销本身那点开销通常可接受但如果你同时 watch 十几个方法又在大流量高峰期开一整晚则要小心线程局部缓冲和输出流量的问题。用完一定要stop退出。3.3 dashboard全局状态一屏看懂先有全局再下结论很多人拿到问题就急着 watch我反而建议先 dashboard。为什么因为如果你对整个 JVM 的状态没有感知很容易被局部的假象带偏。比如接口变慢你以为某个方法有问题结果 dashboard 一看老年代 GC 像抽风一样每秒一次Full GC 的耗时占了 CPU 大头——那方向就完全变了。再比如线程数动辄上千很多线程都处于 TIMED_WAITING那可能是连接池配置的问题跟单个业务方法关系不大。dashboard 还能看到类加载数量。如果类加载数异常增长可能意味着动态生成类泄露那又是另一个排查方向。总之dashboard 负责告诉你“现在哪坏了”thread 和 watch 负责告诉你“具体坏在哪个位置”。4. 线上问题实战拆解从“感觉不对劲”到“精确定位”的全过程4.1 事故复盘一批请求变慢可日志里没有任何异常某次我在做某模拟项目 X 的线上支持突然收到告警订单查询接口 P99 从 80ms 涨到 800ms但错误率不高也没有异常日志。团队成员的第一反应是数据库慢查询可看了数据库监控慢查询不多QPS 也没明显变化。这时候所有人都很懵连方向都没有。按我以前的思路只能先保留现场再发一版带详细日志的代码等下次波动时继续观察。但那次我等不了因为 P99 还在继续恶化用户已经能感知到卡顿。我决定上 Arthas 看看。4.2 第一步dashboard 打底thread 抓现行连上 Arthas 后先跑dashboard。面板显示 CPU 总体不高但内存和 GC 都还算正常唯一扎眼的是很多线程处于WAITING状态而且业务线程占比很大。接着执行thread -n 5最忙的五个线程里除了一些后台线程外业务线程的堆栈指向同一个地方——都在某个 Redis 分布式锁的lock方法上。也就是说大量线程不是在做真正的工作而是排队等一把锁。到这一步问题范围已经缩小到“分布式锁竞争激烈”。但为什么会竞争锁的 key 是怎么拼出来的下一步要查具体参数。4.3 第二步watch 深挖方法参数发现 Key 设计失衡定位到锁获取逻辑后我需要看清楚每个线程到底在用什么样的 key 抢锁。于是在获取锁的方法入口加了一个 watch并加了个条件只打印等待时间超过 100ms 的调用。watch com.demo.LockService tryLock {params, returnObj} #cost 100结果很快出来了。抢锁的 key 千奇百怪但最要命的是有一部分 key 使用了“用户 ID 前缀 全局固定串”的拼接方式。用户 ID 前缀只有十几种固定串就一个。也就是说流量再分散最终都挤到了十几种共同的 key 上。这相当于所有用户都在抢同一批门锁大量线程自然只能排队。回头再看代码这个拼接方式的本意是“按用户分片”避免单一热点。但设计者显然没意识到前缀的基数太小分片均匀度远低于预期。这种 bug 在静态代码 review 里很难暴露因为只有在真实流量下才会看到具体 key 的竞争频率。4.4 第三步jad mc redefine 验证修复方案顺便把热更新也学了定位到根因后修复方式也明确扩大 key 的基数比如把完整的用户 ID 或订单号作为 key。代码改动很小但按流程走发版至少需要半小时到一小时线上还在持续报警我想先止血。Arthas 的jad、mc、redefine三个命令形成了完整链路先反编译线上类确认当前逻辑jad com.demo.LockService它会输出反编译后的 Java 源码。我把这段源码保存到本地修改 key 拼接逻辑然后用mc /tmp/LockService.java -d /tmp/out编译出新的 class 文件再执行redefine /tmp/out/com/demo/LockService.classArthas 会把这个新 class 热替换到运行中的 JVM。生效后我再用 watch 观察新请求确认 key 分布已经打散P99 也慢慢回落。整个过程用了不到十分钟应用没有重启。必须说一句热更新只是止血手段不是长期方案。因为redefine只能修改方法体不能增删字段和方法而且 JVM 重启后就没了。真正的修复还是要走正式发版流程。但在比赛现场这十分钟争回来的窗口足够让团队从容安排版本发布而不是手忙脚乱重启所有节点。5. Arthas 的进阶玩法ognl、trace、tt以及性能开销要注意的事5.1 ognl线上直接调用任意方法抠出隐藏状态有时候你想知道某个注册中心客户端的连接配置到底加载成了什么但日志里没有又不想看代码猜可以用ognl在线上直接执行表达式。比如看某个类的静态字段ognl -x 3 com.demo.GlobalConfigconfigMap也可以调方法ognl -x 2 java.lang.RuntimegetRuntime().availableProcessors()甚至可以直接查看某个 Spring Bean 的内部状态配合getBean之类的调用也能做到。ognl 就像线上 JavaScript 控制台你想读什么内容、调什么函数敲命令就行。但这个能力也很危险相当于给了你一个生产环境的执行端口。如果权限控制不严谁都可能上来执行任意代码。所以 Arthas 应该限制在有权限的人才而不是让每个开发都随便连生产。5.2 trace方法内部每一步耗时别再靠猜经常有人问接口慢日志显示整个方法消耗 500ms但方法里有十步操作到底是哪一步慢这种问题用trace。trace com.demo.OrderService createOrder它会记录方法内部调用的子方法并展示每一个子方法自己的耗时和总耗时占比。执行完后能看到类似这样的调用链---[10ms] com.demo.CacheService.get ---[480ms] com.demo.DatabaseService.queryOrderList ---[5ms] com.demo.MqProducer.send那 480ms 一眼暴露问题就在数据库查询。接下来你可以继续对这个queryOrderList做 trace逐层下钻像剥洋葱一样找到最耗时的叶子方法。trace 还支持按耗时过滤比如只观察超过 200ms 的调用命令是trace com.demo.OrderService createOrder #cost 200这样可以避免把正常调用的耗时都打出来。5.3 tt时间隧道把刚才的请求拽回来重放tt全称 TimeTunnel它可以把一段时间内所有方法调用现场记录到内存里然后你选择其中某一次调用进行回放用来重现问题。基本用法是先记录目标方法tt -t com.demo.OrderService createOrder等到线上某个请求触发后再查看列表tt -l表里会有调用次数、时间戳、耗时、是否异常等信息。找到可疑的那条记录通过它的 index 查看详情tt -i 1001甚至可以拿当时的入参重新发起一次调用tt -i 1001 --play这个场景特别适合那些“偶现”问题。你想复现但抓不到入参tt 就能把当时那个请求拽回来让你拿着真实参数反复操作。它比你自己构造测试数据要可靠得多。5.4 性能开销与安全边界不要在生产环境瞎玩Arthas 确实好用但不是什么场景都能放肆地开。它本身常驻开销很小attach 后不做操作时几乎可以忽略。但只要你用上了watch、trace、tt就是在对目标方法做字节码增强和调用拦截这对高频方法是有额外开销的虽然通常只有微秒级别但架不住你同时 watch 十几个热点方法流量一上来开销会被放大。我的经验是高峰大流量期间能不用 trace 就不用 trace尤其别对 QPS 上万的接口做全量 trace。watch 一定要加条件表达式只捕可疑请求。问题定位完尽快执行stop退出 Arthas不要一直挂着。所有命令的输出都可能包含敏感业务数据比如用户手机号、身份证号注意脱敏避免终端日志泄露。安全边界方面Arthas 的能力相当于拿到了 JVM 的管理权限生产环境的 Arthas 接入应该放在受控网络里执行人要有明确授权。线上排查的时候每一步操作最好都有人知晓避免误操作影响业务进程。6. 我的 Arthas 使用心得与几个不得不说的坑6.1 真香之后我后悔没早点学的几个理由Arthas 让我最香的一点不是某个单点命令有多强而是它把整个排查思路从“盲人摸象”变成了“上帝视角”。以前的思路是日志缺什么就补什么补完等着看下次什么时候炸。现在的思路是线上任何时刻我都能直接观察方法调用、线程状态、实时变量所有信息都是第一手的不需要等复现。这种变化最直接的影响是排查线上问题的时间从小时级缩短到分钟级。一次事故的 MTTR平均修复时间下降比多写多少行优雅代码都实在。还有一点让我很意外Arthas 上手门槛真的不高。基础命令就这么几条dashboard、thread、watch、trace、jad一天就能用熟。真正要锻炼的是“用什么命令回答什么业务问题”的思维能力这个靠多练几次线上故障复盘就能逐渐积累。6.2 几个坑版本兼容、异步线程、流量高峰期慎用、临时修改别忘恢复坑一JDK 版本兼容。Arthas 对 JDK 版本有要求太老或太新的版本都可能出现 attach 失败、某些命令不可用的情况。比如我在一个 JDK 8 的老应用上用过一切正常换到 JDK 17 的新应用后如果不加模块访问参数部分命令会被拒绝。最好先用一个测试环境试通再上生产。坑二异步线程里的问题不好查。watch和trace主要针对同步调用如果业务代码把耗时的部分丢到线程池里异步执行你 watch 原方法看到的只是它提交任务的那一瞬间内部执行还在另一个线程里。这时候可以先用thread -n找到那个异步线程再结合trace去观察真正的执行方法。坑三热更新容易“改完就忘”。用过redefine后如果不留个记录等应用正式发版时很可能把热更的修复内容漏了。我的建议是热更结束后立刻在群里同步一句“某类已用 Arthas 热更正式版本必须包含此修复”并贴出改动点。否则三天后没人记得生产重启一次问题又回来了。坑四Mc 编译时注意 classpath。用mc编译反编译出来的源码时如果依赖的第三方库很多有时候会因为缺 classpath 而编译失败。解决办法是在编译命令里明确加上-c指定 classloader或者直接用mc -d配合项目依赖的 jar 目录。6.3 最后再分享一个小技巧把常用命令写进初始化脚本里Arthas 支持在启动时加载配置文件比如.arthasrc相当于给每个会话预置一些常用命令别名。我一般会放两句dashboard thread -n 5也不算多高深就是省得我每次进去都要重复敲一遍。有人喜欢把生产环境的进程 PID 记成脚本直接java -jar arthas-boot.jar pid减少交互选择出错的风险。这种偏个性化的习惯用顺手之后效率能高不少。不过再顺手的工具也要有边界。Arthas 解决的是“线上能不能查”的问题它不能替代良好的日志体系、监控报警和混沌演练。日志该打的地方好好打监控该配的指标配全Arthas 是你在最后一百米冲刺时用的特种装备不是日常偷懒不写日志的借口。真香归真香但咱得把这份香用在最关键的地方。