ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Arthas实战:Java线上问题排查利器,无需重启即可诊断JVM

Arthas实战:Java线上问题排查利器,无需重启即可诊断JVM

真香定律,放在技术圈一样适用。以前总觉得线上排查问题,靠日志、靠监控、靠人肉加日志就够用了,直到真正把 Arthas 用起来,才发现以前那些操作有多绕。Arthas 是一个开源的 Java 诊断工具,不用改代码、不用重启服务,可以直接 attach 到正在运行的 JVM 进程上,实时查看类加载情况、方法出入参、调用栈、线程状态、JVM 指标,还能反编译和在线热更新。它专门解决一类问题:线上偶发、日志不全、无法本地复现、一重启现场就没了。无论你是后端开发、维护过老项目,还是正在被“线上 bug 怎么都复现不了”折磨,这篇文章都值得花十分钟读完。

1. 为什么说它是排查线上问题的“真香”工具

1.1 传统排查方式的三座大山

先聊聊大家最熟悉的排查流程。遇到线上故障,第一反应通常是翻日志。但日志工具抄出来的往往是“结果”,不是“过程”,很多关键参数、临时变量、调用时序根本不存在。如果日志里什么都有,那问题可能早就被监控平台发现了;现实往往是日志不够、不够细、没有参数上下文。

接着你会想本地复现,把线上数据导出来,构造请求打一遍,结果发现本地怎么都复现不出来。因为并发量、缓存状态、时钟顺序、脏数据都可能影响最终结果,本地环境永远只是线上环境的一种近似。最后一招是加日志重启:改一行 log.info,编译,打包,发版,等流量进入,再看日志,这中间耗时少说半小时多则几个小时,而且频繁重启会让现场被破坏,流量重放也未必能还原问题。

这三座大山我相信每个后端都经历过。Arthas 恰好能绕开它们:不需要提前埋点,因为它能在运行时直接查看真实出入参;不需要反复发布,因为它直接操作当前进程;不需要靠猜,因为它能把调用路径和方法执行细节摊开给你看。用过一次之后,你会真心觉得“真香”。

1.2 原理:attach 与字节码增强

要理解 Arthas 为什么能做到这些,得先知道它的原理基础。Arthas 本质上利用了 Java 的 attach 机制和字节码增强技术。attach 是 JDK 提供的一种能力,允许一个 JVM 进程运行时连接另一个 JVM 进程,并通过 Agent 方式执行操作。字节码增强则是在类加载或运行时,对已经加载的类做动态修改,在方法执行前后插入拦截逻辑,相当于给方法装上一套“临时探针”。

这套探针是临时的,JVM 一重启就没了,不会影响原有逻辑。原理本身在 Java 生态里不算多新鲜,但真正难的是把这一套能力封装得好用,做成命令行交互式的产品。Arthas 做到的正是这一点,从启动到使用,几乎不需要在应用代码里增加任何依赖,这对排查场景来说是极大的优势。

1.3 什么时候该想到用它

很多人问什么场景下 Arthas 才是刚需。我的判断标准很简单:只要你遇到“日志看不出来、重启代价高、需要确认运行期状态”这三个条件中的两个,就该考虑它。典型场景包括:接口耗时突然变长、CPU 飙升、线程池被打满、缓存数据异常、灰度环境某个请求表现不一致、老项目没有源码无法重新编译。

这里有一个容易被忽略的点:Arthas 不是只给“救火”用的。在正常的压测、联调、性能分析阶段也可以主动用它。我经常在压测时开着 dashboard,一边加压一边观察线程池大小、GC 频率和热点方法,比事后看测试报告直观得多。因为它不是从统计数据里做推测,而是直接从运行中的 JVM 里拿事实。

1.4 为什么团队应该统一学习

Arthas 这类工具,个人会用和团队都会用的效果差别很大。一个人掌握,遇到问题只能是他上;团队都掌握,任何值班的人都能快速定位初级问题,不用每次都把核心开发叫起来看服务器。我见过不少团队把 Arthas 列为后端必修技能,要求每个人至少掌握 dashboard、thread、watch、trace 四个命令的基本用法,我觉得是非常务实的做法。

2. 上手必须先掌握的 6 个高频命令

命令速查表先放在前面,后面会逐个细讲:

命令用途典型场景
dashboard全局面板CPU、内存、GC、线程状态一眼看全
watch观察方法出入参确认参数是否正常、异常在哪抛出
trace方法调用链耗时定位慢请求卡在哪个子调用
thread线程分析CPU飙高、死锁、线程阻塞
jad反编译看线上实际运行的代码
mc + redefine热更新紧急修复后临时止损

2.1 启动与选择进程:java -jar arthas-boot.jar

Arthas 的使用路径非常直接。第一步,下载 arthas-boot.jar,放到服务器某个目录下,执行:

java -jar arthas-boot.jar

启动后它会自动列出当前机器上所有 Java 进程,输入序号就能 attach。如果想跳过交互直接选进程,可以在后面直接加 PID:

java -jar arthas-boot.jar 12345

12345 是目标进程的进程号。刚上手时,我建议大家执行 help 看看当前版本支持的命令列表,因为不同版本之间命令细节会有差异,老是照着老资料敲容易踩坑。

有一个小细节:Arthas 本身运行需要 JDK,但需要 attach 的目标进程不一定要用 JDK 启动,部分 JRE 环境也可以兼容。不过实际部署中,建议用和运行环境同一版本的 JDK 去启动 Arthas 客户端,减少无谓的兼容性问题。attach 成功后会进入交互终端,提示符变成$,所有命令都在这里输入。

2.2 dashboard 一眼看全局

attach 成功后的第一个建议命令是 dashboard。它会实时刷新一个面板,展示 CPU 占用率、内存使用、GC 次数、线程状态分布、每个线程的 CPU 消耗等信息。这等于给 JVM 装了一个实时监控屏幕,打开它就能快速判断问题在哪一层:是 CPU 整体打满,还是内存上涨触发频繁 GC,还是某个线程卡死。

初次使用的人容易被大量指标淹没,我一般只看四样东西:最上面的 CPU 使用率、内存区域变化、GC 频率、线程状态里 BLOCKED 和 WAITING 的数量。如果 GC 频率异常,优先排查堆内存和对象分配;如果有大量 BLOCKED 线程,优先找锁竞争。面板是持续刷新的,按 q 可以退出回到交互终端。

2.3 watch 查看入参、返回值和异常

watch 是线上排查使用频率最高的命令,没有之一。它的作用是对指定方法观察,当方法被调用时打印出入参、返回值或者异常信息。

watch com.example.order.OrderServiceImpl createOrder '{params, returnObj, throwExp}' -x 2

这条命令的意思是:观察 OrderServiceImpl 类的 createOrder 方法,当它执行时打印入参、返回值和异常,对象嵌套展开深度 2 层。这里用了 OGNL 表达式,用单引号包裹,内置变量包括 params 数组、returnObj 和 throwExp。

我第一次用 watch 排查问题时,整个人的感觉就是开了天眼。以前要加日志重新发版才能看到的参数,现在直接在当前进程里实时看到,而且可以临时加条件,只观察特定用户或特定业务类型:

watch com.example.order.OrderServiceImpl createOrder '{params[0]}' 'params[0].userId == 10086' -x 3

条件表达式写最后,返回布尔值,只有满足条件才打印。这样在流量大的接口下看,不会被无关请求刷屏。

2.4 trace 定位方法耗时分布

如果说 watch 解决的是“参数对不对”,trace 解决的就是“时间花在哪”。它会输出指定方法的调用链路上每个子方法的耗时,自动计算总耗时和每个节点耗时,执行效果类似一棵调用树:

trace com.example.order.OrderServiceImpl createOrder

命令执行后,目标方法一旦被调用,你会看到类似下面的输出:

`---createOrder() # 总耗时 823ms +---checkStock() # 56ms +---deductBalance() # 720ms +---sendMessage() # 47ms

顺着耗时最大的节点继续往下 trace,逐层钻取,就能锁定真正慢的底层方法。这个过程就像剥洋葱,把慢请求一层层拆到最小执行单元。

有一点要提醒:trace 是通过字节码增强实现的,它只增强执行命令之后发生的调用,历史调用不会回溯。所以想抓到想要的 trace 结果,必须让目标请求再进来一次。如果接口触发频率低,可以配合定时任务或者手动打一个测试请求。

2.5 thread:比 jstack 更实用的线程分析

很多同学排查线程问题只会 jstack,但 jstack 一下输出大量线程栈,分不清哪个线程才是问题源头。Arthas 的 thread 命令可以直接按 CPU 消耗排序,这个能力非常实用:

thread -n 3

它会打印出 CPU 占用最高的 3 个线程,同时带上对应线程栈。对比 jstack 还得自己去数线程 ID、对应 16 进制换算,效率高了一个量级。它还能直接查死锁:

thread -b
Found one Java-level deadlock: =============================================== "thread-1": waiting to lock monitor ...

thread -b会直接输出检测结果和相关线程栈,不用再从 jstack 输出里手工解析 Locked ownable synchronizers。如果需要看指定线程,可以带线程 ID 精确查看,这个在分析阻塞场景时很管用。

2.6 jad + mc + redefine 实现在线热更新

这一组命令要谨慎使用,但在紧急情况下非常救命。jad 用于反编译已加载的类,可以看线上运行的真实代码,而不是本地仓库里可能已经过期的版本;mc 是内存编译,基于 jad 查看后的源码在内存里重新编译;redefine 是把编译好的字节码替换到运行中的 JVM 里。

jad --source-only com.example.order.OrderServiceImpl > /tmp/OrderServiceImpl.java mc /tmp/OrderServiceImpl.java -d /tmp redefine /tmp/com/example/order/OrderServiceImpl.class

热更新最典型的使用场景是:线上有 bug 需要临时改一行逻辑,发布流程要走审批,这时先用 Arthas 做临时修复让服务先恢复,然后马上走正规发布。要注意,这个操作不会改磁盘上的 class 文件,也不会写回 jar 包,服务一重启就失效。而且不是所有代码都能热更新成功,增加类、改变方法签名的场景通常会失败。

3. 实战:从几个真实故障看 Arthas 的实际用法

3.1 场景一:CPU 飙高,快速定位到正则灾难性回溯

我先讲一个最常见的 CPU 100% 问题。某服务在活动流量上来后,CPU 使用率持续接近满核,业务方反馈接口响应很慢。按老办法,先看 top 找到进程 PID,然后 jstack 导出线程栈,再做 16 进制转换,找到对应线程,整个过程相当繁琐。

用 Arthas 的流程会快很多。attach 之后先执行 dashboard,整体看一眼 CPU 和线程状态,当时很明显看到几个线程占用 CPU 非常夸张。然后执行:

thread -n 5

它直接给出了 CPU 最高的 5 个线程及其调用栈。我看到一个线程栈反复停在同一个业务方法里,跟进 trace 这个方法,发现内部有一段非常复杂的字符串正则匹配,在特定输入下触发灾难性回溯,CPU 全部耗在了正则引擎内部。定位过程前后不到五分钟,全程没有重启服务。

定位到原因之后,我用 jad 反编译出当前代码,把正则逻辑临时替换成朴素的字符串前缀匹配,通过 mc 和 redefine 热更新到线上进程,服务立刻恢复。这个案例给我的最大感触是:CPU 飙高和接口慢,本质上都是“定位问题”的效率问题。只要能看到线程在干什么,排查时间就能从小时级缩短到分钟级。

3.2 场景二:接口偶发超时,trace 逐层揪出隐藏阻塞

另一个案例是某接口偶发超时,频率不高,大概每天几次。这种偶发问题最折磨人,日志里偶尔看到超时,但不知道卡在哪个环节。我先用 watch 看接口入参,发现超时请求和正常请求的参数没有明显区别,于是改用 trace 观察调用链路。

第一次 trace 还没有抓到超时,等了差不多十几分钟才有一次超时出现。这次 trace 输出里,接口方法总耗时接近 3 秒,其中一个下游 RPC 调用的子节点耗时占 2.5 秒,其他子节点都只有几十毫秒。顺着这个异步下游继续往下定位,最后发现是某个配置中心客户端的刷新逻辑在特定时刻阻塞,导致请求串行等待。

这个例子想说明一个关键点:很多偶发问题根本不体现在业务日志里,因为业务代码里没有打印每一次下游调用的耗时明细。Arthas 的运行时观察能力,等于给线上系统装了一台显微镜,它能把正常日志看不到的细节全部拉到眼前。

3.3 场景三:验证修复方案,不用发布就能试

还有一种很常见的用法,不是紧急救火,而是验证方案。曾经有一个灰度问题,我们怀疑是某个枚举映射导致数据错乱,但不敢完全确定。改代码发布会话成本高,而且如果改完还是错的,等于白折腾。

我先用 jad 反编译出线上实际运行的类,确认枚举字段顺序和本地代码不一致,这是一个非常关键的信息。因为本地代码经过多次迭代,线上的 class 可能已经和 git 分支对不上。确认问题后,我在本地改好,用 mc 和 redefine 热更新到灰度进程,再跑一次请求,问题复现时已经不出现了,这才放心走正式发布流程。

很多人担心热更新有副作用。我的经验是:先在测试环境或灰度环境做验证,确认无误后再对生产操作;操作完成之后,立刻用 watch 或重新发一次请求确认行为符合预期。热更新在 JVM 层是支持的,但生产环境的变更仍要符合团队变更流程,最好有同事在场一起确认,至少留一条操作记录。

4. 常见问题与排查技巧实录

4.1 attach 不上目标进程怎么办

我遇到最多的问题是 attach 失败,报错或者列不出进程。这里有一个速查表,基本覆盖我遇到过的原因:

常见原因现象处理建议
用户权限不一致attach 时报错或无权限切换成目标进程同一用户启动
/tmp 空间满了attach 后立刻异常退出清理 /tmp 下临时文件
目标进程已退出进程列表里找不到用 jps 确认进程状态
端口被占用telnet 连接失败指定新端口启动

Linux 下 attach 有权限限制,最好用和目标进程相同的用户操作。Arthas attach 时会在 /tmp 写临时文件,空间不足会导致各种奇怪问题。如果还是不行,可以在启动 arthas-boot.jar 时带上--telnet-port和--http-port参数,换两个没被占用的端口,很多端口冲突的问题就解决了。

4.2 对线上服务的影响到底有多大

这是每个初次接触的人都会问的问题。字节码增强确实会带来一些性能开销,但实际影响通常很小。我的实测经验是,在常规业务方法上开启 watch 或 trace,单次调用的额外开销在几毫秒以内,对整个请求来说几乎感知不到。

但如果是对超级热点方法开 trace,比如某个每秒调用上万次的基础工具方法,开销就会放大。这种基础方法被所有上层业务调用,拦截和记录的成本会被放大到不可忽略。我的建议是:精准选择目标方法,不要对整个包路径做通配观察;观察期间留意 dashboard 的 CPU 变化,如果发现明显升高,立刻缩小范围或者停掉观察命令。

4.3 权限控制不能忽视

Arthas 很强,强到可以随时查看进程内数据、修改运行中的代码,所以它也是一把双刃剑。在团队里使用,至少要做到三件事:

  • 控制在指定的运维和开发人员范围内使用,不要全员开放服务器权限;
  • 生产环境使用前提前申请临时权限,并记录操作原因;
  • 操作完成后及时退出交互终端,不要长期挂着会话。

有些团队会把 Arthas 封装成内部平台,通过 Web 界面暴露受限接口,避免开发人员直接登录服务器乱敲命令。我比较认可这种思路,能力强大和可控并不矛盾。安全这块做到位,工具才能长期用下去。

4.4 部分命令在生产环境不可用

跟具体 JDK 版本相关的问题也值得关注。某些 JDK 版本下 jad 反编译可能不完美,尤其是涉及 Lambda 表达式或者动态代理生成的类时,反编译结果会让人看得很困惑。redefine 的限制更多,对新增成员变量、改变方法签名这类结构性变更通常不支持,强行操作会报错。

遇到这种情况,不要死磕热更新。先用 watch 拿到足够信息,确认根因后,该走发布流程就走发布流程。工具的作用是帮你看清现场,而不是替代正常的变更管理。

5. 使用经验和技巧分享

5.1 条件表达式让 watch 不再刷屏

watch 默认打印所有请求,在流量大的接口下输出会刷得飞快,关键是里面有价值的请求可能只占很小比例。用条件表达式可以做到精准观察,比如只看某类订单创建失败的请求:

watch com.example.order.OrderServiceImpl createOrder '{params[0], returnObj}' 'params[0].orderType == 2 && returnObj.success == false'

条件表达式写在命令最后,返回布尔值,满足条件才触发打印。这个技巧能让你在繁忙环境下直接锁定目标请求,不用从大量日志输出里去捞。

5.2 ognl 命令:主动触发静态方法调用

有些时候你不仅想看,还想主动触发某个方法,比如清理某个静态缓存、查看某个配置项。Arthas 提供 ognl 命令,可以不写代码直接执行静态方法调用:

ognl '@com.example.config.CacheManager@clear()'

这种操作在应急时很有效,但风险也高,它相当于直接往 JVM 里执行代码。我建的建议是先把命令在测试环境验证一次,再上生产,并且必须明确这个调用会影响什么、会不会有并发问题。

5.3 脚本批量采集线程现场

遇到周期性出现的问题,比如每小时整点偶发一次卡顿,单次执行 thread 不一定能抓到现场。可以用一条简单的 shell 循环,把 Arthas 的批处理模式用起来:

while true; do echo "--- $(date) ---" >> thread_dump.log java -jar arthas-boot.jar PID -c "thread -n 5 -i 1000" >> thread_dump.log 2>&1 sleep 5 done

这里用到了-c参数,可以让 Arthas 执行一条命令后自动退出,非常适合在脚本里批量使用。-i 1000是采样间隔,单位是毫秒。把多次采集到的线程快照放在一起对比,周期性问题的触发特征就很容易暴露出来。

5.4 工具是帮手,不是万能药

最后说一点个人观察。Arthas 确实香,但解决的是“运行态信息不可见”的问题,很多线上故障的根因其实是代码逻辑、容量规划或者架构设计。工具能帮你更快定位问题,但定位完之后该重构要重构、该扩容要扩容,不能把热更新当成常态化修复方式。

我现在的习惯是:复杂问题先用 Arthas 快速观察现场,拿到关键证据后把结论和操作记录整理到运维工单,然后走稳定的修复流程。工具负责把定位时间从小时降到分钟,剩下的工程质量问题,还是要靠扎实的代码和可靠的变更流程解决。

前后踩过几次坑之后,我对 Arthas 的态度已经从“要不要学”变成了“怎么用得更稳”。如果你也被线上问题复现难、定位慢困扰,别犹豫,找个低峰期在测试环境把 watch、trace、thread 这几个基础命令练熟,真到救火那一刻,你会感谢这个“真香”工具。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进