Arthas 线上诊断实战:Java 不停机排障指南

Arthas 线上诊断解决的是 Java 工程师最尴尬的处境:生产环境接口突然变慢、CPU 打满、返回值诡异,而日志里什么都没打。传统做法是加日志、打包、重启、等复现,一轮下来半天过去,问题还可能不再出现。Arthas 是阿里巴巴开源的 Java 诊断工具,通过 attach 到运行中的 JVM,让你在不停机、不改代码、不重启的前提下,直接看到线程栈、方法参数、调用耗时。本文按真实排障场景讲透核心命令与生产使用纪律。

下面的命令全部基于 Arthas 4.x(要求 JDK 8 及以上;仍在 JDK 6/7 的老系统需使用 3.x 分支)。所有示例都可以直接照抄替换类名执行,不需要在应用里埋任何埋点。

一、线上问题难查的三类盲区

先明确 Arthas 到底补了什么短板。线上疑难问题基本落在三个盲区里,而这三类恰好都是日志和监控大盘看不见的:

盲区类型典型症状传统手段的局限Arthas 对应命令
运行时状态不可见CPU 100%、线程数暴涨、莫名死锁jstack 只能抓瞬时快照,难关联到具体业务线程dashboard / thread
方法级黑盒某接口偶发 3 秒,链路追踪只到方法入口日志没打入参出参,加日志需要重新发版watch / trace / tt
代码与预期不一致改了配置没生效、疑似部署的不是这个版本无法确认线上真正加载的字节码内容jad / sc / retransform

换句话说,Arthas 把「猜」变成了「看」。这一点和技术复盘实战:用 Postmortem 把故障变成团队资产里强调的证据链思路一致——排障要靠现场数据,不靠事后回忆。

二、三种接入方式:本机、容器、远程

1. 本机快速启动(最常用)

最省事的方式是下载 arthas-boot.jar 一键启动,它会列出本机所有 Java 进程,输入序号即可 attach:

# 方式一:boot jar(推荐,自动下载并管理版本)
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 交互式选择进程,或直接指定 PID
java -jar arthas-boot.jar 12345

# 方式二:install.sh 安装为常驻命令
curl -L https://arthas.aliyun.com/install.sh | sh
./as.sh 12345

权限坑:attach 要求执行用户与目标 JVM 的启动用户一致。生产上应用常以 apps 用户运行,用 root 直接 attach 会报错,需要切换用户执行:sudo -u apps -EH java -jar arthas-boot.jar

2. 容器内诊断

容器化部署后,Arthas 需要在与 JVM 相同的 PID 命名空间内运行,因此要先进容器再 attach。如果镜像是精简版没有 curl,可以先把 jar 拷进去:

# 宿主机把 jar 拷入容器(精简镜像常见做法)
docker cp arthas-boot.jar <container_id>:/tmp/

# 进入容器 attach(注意用应用同名用户)
docker exec -it <container_id> /bin/sh
cd /tmp && java -jar arthas-boot.jar

# K8s 环境
kubectl exec -it <pod-name> -- /bin/sh

镜像太精简(比如基于 distroless)时连 shell 都没有,这时更合适的方案是给 Pod 挂一个诊断 sidecar 或临时容器。容器编排与服务依赖的组织方式可以参考Docker Compose 一键编排实战

3. Web Console 与 Tunnel Server

Arthas 内置 HTTP 服务,默认 3658 端口可直接开浏览器操作(http://127.0.0.1:3658)。多机集群则建议部署 tunnel server 统一纳管,应用侧启动时指定注册地址即可,避免逐台登录跳板机。

三、核心命令速查表

Arthas 命令有几十个,但实战里高频用的就下面这些。建议先把这张表存下来,遇到问题按症状反查命令:

命令作用什么时候用
dashboard实时看线程、内存、GC 全景刚上机器,先看一眼整体健康度
thread -n 3列出最忙的 3 个线程栈CPU 飙高第一刀
thread -b找出阻塞其他线程的罪魁怀疑死锁 / 线程池卡死
watch观察方法入参、返回值、异常返回结果不对,但日志没打
trace输出方法内部各调用耗时接口慢,要定位慢在哪一行
tt记录调用现场,事后回放偶发问题,需要抓到再慢慢看
monitor按周期统计调用量/成功率/耗时压测或灰度期间盯一个方法
jad / sc反编译线上字节码、查类来源怀疑部署版本不对、jar 包冲突
logger动态调整日志级别临时开 DEBUG,不用重启
profiler生成火焰图(集成 async-profiler)需要完整 CPU 采样报告

四、四个真实排障场景

场景一:CPU 打满到 100%

这是最经典的场景。thread -n 会按 CPU 占用排序输出线程栈,几秒钟就能看出是哪段代码在空转。常见元凶是死循环、正则回溯、或者疯狂的 GC 线程:

# 最忙的 3 个线程及其完整栈
thread -n 3

# 指定采样间隔 2 秒,结果更可信
thread -n 3 -i 2000

# 看单个线程细节
thread 47

# 找阻塞源(输出 "Found 1 blocked thread" 时直接锁定)
thread -b

# 全量火焰图,30 秒采样后导出 HTML
profiler start --event cpu
profiler stop --format html --file /tmp/cpu.html

如果 thread -n 显示占用最高的是 GC task thread,说明问题不在业务代码而在内存,应转向 GC 分析。具体调优参数与判断标准可以对照JVM GC 调优实战那篇。

场景二:接口偶发 3 秒,链路追踪只到入口

trace 是定位方法内部耗时的核心武器,它会打印被观测方法内所有子调用的耗时占比。关键技巧是用条件表达式只抓慢请求,避免刷屏:

# 只打印耗时超过 200ms 的调用,最多抓 3 次
trace com.example.order.OrderService createOrder '#cost > 200' -n 3

# 下钻两层(默认只跟一层,看不到嵌套调用)
trace -E com.example.order.OrderService|com.example.pay.PayClient '.*' --skipJDKMethod false

# 按周期统计成功率与平均耗时:每 5 秒一行
monitor -c 5 com.example.order.OrderService createOrder

实战中 trace 常常一层层往下扒:先 trace 入口方法,看到 80% 时间落在某个 DAO,再 trace 那个 DAO,最后发现是一条没走索引的 SQL。这类问题的根治手段见MyBatis 与 JPA 性能优化PostgreSQL 慢查询优化

场景三:返回值不对,但日志什么都没打

watch 能把方法的入参、返回值、异常对象全部打印出来,等价于「临时加一行 log 且立即生效」。-x 控制对象展开深度,是最容易被忽略但最关键的参数:

# 观察入参 + 返回值 + 异常,展开 2 层,抓 5 次后自动退出
watch com.example.user.UserService getProfile '{params, returnObj, throwExp}' -x 2 -n 5

# 只在抛异常时记录(-e 表示异常退出点)
watch com.example.user.UserService getProfile '{params, throwExp}' -e -x 3

# 条件过滤:只看某个用户 ID 的调用
watch com.example.user.UserService getProfile '{params, returnObj}' 'params[0]==10086' -x 2

# 记录调用现场,事后回放(tt = TimeTunnel)
tt -t com.example.user.UserService getProfile -n 5
tt -i 1002 -p                # 用第 1002 号现场重新发起一次调用
tt -i 1002 -w 'target.dao'   # 查看当时的实例字段

tt 的价值在偶发问题上无可替代:先挂着录,等问题出现后再慢慢分析那次调用的完整现场,甚至能用 -p 原地重放验证修复思路。

场景四:怀疑部署的不是这个版本

「我明明改了啊」是排障中最耗时的误会。jad 直接反编译 JVM 里真正加载的字节码,sc -d 告诉你这个类是从哪个 jar、哪个 ClassLoader 加载的,一分钟终结争论:

# 反编译线上真实字节码(只要源码部分)
jad --source-only com.example.config.AppConfig

# 查类的加载来源与 ClassLoader,排查 jar 包冲突
sc -d com.example.config.AppConfig

# 同名类被加载多份 = 典型依赖冲突
sc -d *AppConfig | grep code-source

# 查看方法签名是否与预期一致
sm -d com.example.config.AppConfig getTimeout

# 临时开 DEBUG 日志,不重启
logger --name ROOT --level debug

确认代码确实旧了,就回去查 CI 流水线为什么没把新包推上去;构建产物与部署版本的一致性校验,属于GitHub Actions CI/CD 实战要解决的问题。

五、内存对象与热更新:高阶但要慎用

Arthas 还能直接从堆里捞对象、改一行代码热生效,这类能力威力大、风险也大:

# 看各内存区用量(比 jstat 直观)
memory

# 从堆里取出实例,看真实字段值(务必加 --limit)
vmtool --action getInstances --className com.example.cache.LocalCache --limit 3

# 强制触发一次 GC 观察回收效果
vmtool --action forceGc

# 导出堆快照,拿回本地用 MAT 分析
heapdump --live /tmp/dump.hprof

# 热更新三步:反编译 -> 改文件 -> 编译并 retransform
jad --source-only com.example.util.RateLimiter > /tmp/RateLimiter.java
mc -c 1be6f5c3 /tmp/RateLimiter.java -d /tmp
retransform /tmp/com/example/util/RateLimiter.class

热更新(mc + retransform)只适合紧急止血:它改的是 JVM 内存中的字节码,重启即失效,且无法新增字段或方法。用完必须立刻走正常流程发版补上,同时执行 retransform --delete <id> 清理,否则下一个人排查时会看到「代码和行为不一致」的鬼故事。

六、生产环境使用纪律

Arthas 本质是通过字节码增强实现观测,会带来额外开销。在生产上用它必须守住几条纪律,否则诊断工具本身就成了故障源:

危险动作后果安全做法
watch/trace 不加 -n高 QPS 方法瞬间刷屏,终端卡死甚至拖慢应用一律加 -n 5 限制次数,配合条件表达式过滤
通配符匹配过宽(如 trace com.example.* *增强上千个类,CPU 明显抬升精确到类名与方法名,用 -E 正则也要收窄
quit 以为退干净了只断开会话,增强仍挂在 JVM 上确认不再诊断时用 stop 彻底卸载
直接在全部实例上操作影响全量流量先摘掉一台的流量,在单实例上诊断
把热更新当发布手段重启后行为回退,无人知晓只做临时止血,当天补正式发版

另外两个易踩的点:一是 reset 可以撤销指定类的增强,排查完顺手清理;二是 Arthas 对被 JIT 高度优化或被其他 Agent(如 APM 探针)改写过的方法,偶发观测不到,此时换观测点或临时关闭冲突 Agent 即可。

七、把排障能力沉淀成团队资产

工具再好,也只解决「查得到」的问题。真正让线上稳定下来的是三件事:可观测性前置(关键方法的耗时与异常在监控里就有,不必每次上机器)、诊断动作固化(把 CPU 飙高、接口变慢这类高频场景写成排障手册,命令直接复制)、问题回流到测试(每次线上暴露的坑补一条用例,见Java 单元测试实战:JUnit 5 与 Mockito 指南)。

建议的落地节奏:先在测试环境把 dashboardthread -ntracewatch 这四个命令练熟,再整理成本团队的排障 checklist,最后配一台 tunnel server 让所有实例可远程接入。等到真出事的那个凌晨,你要的是肌肉记忆,而不是现场翻文档。

上一篇 Java 单元测试实战:JUnit 5 与 Mockito 指南
下一篇 技术人个人知识管理实战:告别收藏夹吃灰