Java线上排查利器Arthas实战:从CPU飙高到接口超时 在Java后端摸爬滚打久了你大概率经历过这样的场景线上应用突然CPU飙高、接口超时、用户反馈某个功能时好时坏可日志里干干净净一点异常都没打出来。重启大法虽然能暂时缓解但根本问题还在而且重启以后什么现场都没了。这种时候我第一反应就是开Arthas这个工具我第一次用就觉得“真香”甚至可以说它已经成了我排查线上问题的默认入口。所有要连着好几台机器、翻各种日志、反复猜测代码逻辑的日子在Arthas面前基本可以画上句号了。Arthas是Java生态里非常出名的在线诊断工具它最大的特点是不用改代码、不用重启应用直接附着在运行中的JVM上就能实时查看类加载信息、方法调用参数、返回值、异常甚至还能动态修改内存中已加载的类重新定义类逻辑。对于需要快速定位生产环境问题的开发、运维和SRE同学来说这是一把不折不扣的瑞士军刀。这篇内容我会把日常用得最多、最顺手的功能串起来讲一遍从怎么快速附着到具体命令怎么用、遇到问题的排查思路是什么尽量按照实战场景来写方便你直接照葫芦画瓢。1. 为什么Arthas值得“真香”先搞清楚它解决的痛点1.1 传统排查方式的困境我最早排查线上问题基本靠三板斧看日志、加临时日志重新发版、重启试试。日志如果没有打印出关键堆栈就得靠猜加日志发版光是走流程就要很久而且改了代码的行为可能就和问题现场不一致了有些偶发问题根本复现不出来重启就更不用提了JVM里的现场被清得一干二净线程状态、堆内存数据、正在执行的调用链全都没了。更难受的是定位性能问题。一个接口耗时忽高忽低你怀疑某个第三方调用超时但没法确认到底卡在哪一行。最原始的方案是到处打时间戳然后重新发版算下来一个下午就过去了。而且发版本身会引入变量问题可能就这次发完消失了下次出问题又不知道什么时候。Arthas这类工具的思路完全不同应用不需要重启它会在目标JVM进程上启动一个代理然后把诊断命令通过socket传给这个代理代理用字节码注入技术去拦截目标类的方法把你想看的参数、返回值、异常、执行耗时实时抓回来。相当于给运行中的应用做了一次“无痛内窥镜”检查随时都能做做完拔掉恢复原样。1.2 和BTrace、Greys等同类工具的取舍其实在Arthas之前已经有BTrace这类早期工具我也试用过。BTrace有一个很大的问题是使用门槛比较高需要编写专门的脚本而且对版本、对JVM参数比较敏感用得不好容易把自己搞挂。Arthas虽然也用了类似字节码增强的原理但它在交互体验和命令设计上做了很多工程化的打磨像看板一样实时刷新的dashboard、一键查看线程CPU占用的thread命令、跟踪方法调用链的trace命令都是直接敲命令就能用非常符合后端开发的直觉。另外Arthas对Java版本和中间件环境的兼容性做得不错像Spring Boot这类框架的项目几乎开箱即用不需要额外改启动参数。所以很多团队会把Arthas作为标准线上问题排查工具来推广甚至要求每个服务镜像里都预留Arthas的安装路径方便随时附着排查。提示Arthas本身是一个诊断工具它不改变业务代码的最终逻辑。但要注意在敏感环境使用时需要做好权限管理避免有人通过它执行危险操作比如动态修改类逻辑。生产环境建议配合审计和权限体系来用。2. 五分钟上手Arthas安装、启动与附着2.1 用官方脚本快速安装Arthas的安装方式很灵活最省事的方法是直接下载官方提供的as.sh脚本然后执行curl -L -O https://github.com/alibaba/arthas/releases/latest/download/arthas-boot.jar java -jar arthas-boot.jar这里我个人更推荐直接下载arthas-boot.jar来用因为后续版本升级时用它来引导会更统一也方便自定义下载目录。脚本方式适合快速体验它的本质也是下载arthas-boot.jar再启动。启动之后命令行会列出当前机器上所有的Java进程就像这样[INFO] arthas-boot version: 3.x.x [INFO] Found existing java process, please choose one and input the serial number: * [1] 12345 xxx-order-service [2] 23456 xxx-gateway输入序号回车就能附着到对应的进程上。需要注意当前登录用户必须和目标进程的启动用户一致否则会附着失败或者出现权限不足的问题。这个坑我踩过很多次后面会在排查章节单独说。2.2 无交互式启动适合脚本和自动化场景如果你需要在CI/CD流水线或者自动化脚本里启动Arthas就不能用交互式选择进程的方式。这时候可以直接指定进程ID启动java -jar arthas-boot.jar --attach-only --target-ip 127.0.0.1 -p 12345或者直接用as.sh带参数./as.sh -p 12345还有一种场景是应用本身就集成了Arthas的agent方式启动即在JVM参数里加上-javaagent:arthas-agent.jar这样应用一跑起来Arthas的attach通道就已经准备好了需要诊断的时候直接用客户端连接即可。这种方式特别适合容器环境因为容器的PID 1进程往往不是Java进程动态attach会受PID namespace影响提前挂agent反而更省心。2.3 理解Arthas的attach与Agent机制Arthas底层用的是Java的Attach API。它向目标JVM发送attach请求并加载一个agent这个agent会和Arthas的服务端建立通信服务端再开一个telnet端口默认8563以及一个HTTP端口默认8562。你敲的每一条命令都会先经过本地的Arthas客户端再通过这个通道发送给目标JVM里的agent去执行。这条链路听起来有点绕但只要理解了排错就快多了。比如你遇到attach成功但命令半天没反应那大概率是目标进程中agent初始化出了问题比如类加载器冲突、Arthas版本过旧等。再比如你发现本地能连上自己的测试环境但线上环境始终连接失败就要检查网络策略是否开放了8563和8562这两个端口。2.4 检查Arthas是否工作正常附着成功后可以先执行一个最简单的命令试试[arthas12345]$ version如果返回版本号说明链路通畅。然后可以执行[arthas12345]$ sysprop查看系统属性。这些都正常了再进入正式的诊断流程。3. 高频命令实战解析从看板到方法级追踪3.1 dashboard实时全局看板dashboard是我每次进入Arthas后几乎必敲的第一条命令。它会以一个动态面板的形式展示当前JVM的概况包括线程、内存、GC、类加载数量以及运行时环境信息。看板会自动刷新默认每3秒刷新一次当然你可以用-i参数指定刷新间隔比如[arthas12345]$ dashboard -i 1000这样每秒刷新一次。下面给你看一眼典型输出长什么样ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPTED DAEMON 67 xx-rpc-thread-14 main 5 RUNNABLE 33 0:23 false true ... Memory used total max usage GC heap 1.2G 2.0G 4.0G 30.00% gc.ps_scavenge.count: 128 ...看板里最值得关注的是CPU占用排前几的线程。如果某条业务线程长期处于RUNNABLE状态而且CPU占比明显偏高基本可以锁定它就是CPU飙高问题的元凶。再用thread -n命令抓它的线程栈就能进一步确认卡点位置。3.2 thread线程排查利器thread命令有好几个重要的用法按实用频率排大概是这么个顺序第一查看当前最耗CPU的线程栈[arthas12345]$ thread -n 5这个命令会把CPU占用最高的5个线程的堆栈信息全部打印出来并且自动带上线程的ID、名称、CPU使用率、状态。有一次我遇到线上CPU突然打满用这个命令一下就看到了某个线程卡在了一个JSON序列化工具的循环里栈顶清清楚楚写着某行代码的toString调用问题原因秒定位。第二查看指定线程的完整栈[arthas12345]$ thread 67其中67就是线程ID。这会打印该线程的完整堆栈适合排查线程阻塞在哪里、锁等待在哪里。第三查找线程间是否有锁等待和死锁[arthas12345]$ thread --stateBLOCKED如果想要检查死锁Arthas没有专门的一条命令但是可以结合thread --stateBLOCKED和线程栈中的“waiting for monitor”字样来判断。某次我处理过一个偶发死锁问题两个线程都在等对方的锁通过这个命令一查堆栈里两个线程互相等待的锁对象非常直观最后定位到是加锁顺序不一致导致的经典死锁。3.3 jad反编译线上类确认跑的是不是你以为的代码线上最尴尬的情况之一就是代码看着是新的实际发出去的包是旧的。与其问别人不如直接反编译看字节码。jad命令能反编译任意已加载类打印出它的Java源码结构实际是反编译结果可能和原始源码有细微差别但逻辑基本可读。用法非常简单[arthas12345]$ jad com.example.OrderService输出会带类加载器信息、源码位置、反编译的Java代码。这个命令在排查“为什么线上行为和本地不一致”时特别有用。有一次客户端反馈某功能表现异常我用jad一查发现类里根本没有最新加的判断逻辑立刻确认是发布流程出了问题省掉了无休止的讨论。如果你的类被JIT优化了反编译出来可能有部分内容显示不出来但不影响判断大致逻辑。可以用jad --source-only只输出源码方便复制到本地做diff比较。3.4 watch观察方法调用入参、返回值和异常watch是Arthas里我最喜欢的命令没有之一。它用来观察指定方法的每一次调用抓取入参、返回对象、抛出的异常、耗时等。写法如下[arthas12345]$ watch com.example.OrderService createOrder {params, returnObj, exception, cost} -x 3拆解一下这个命令第一个参数是类全限定名。第二个参数是方法名。第三个参数是OGNL表达式决定要输出什么。{params, returnObj, exception, cost}分别代表入参列表、返回对象、异常、耗时。这个大括号不是列表字面量而是输出多个字段的组合。-x表示展开对象的层级数字越大展开细节越多。比如-x 3能看到入参对象的三层嵌套结构。这个命令在排查“参数对了却得到奇怪结果”、“某个方法偶尔抛异常但日志没记录”的场景下几乎是精准制导。比如有次同行遇到一个接口偶尔返回错误码但代码里没打日志我建议用watch带异常参数去抓结果没等多久就抓到了异常栈是一个特殊的非法参数组合导致的本地测试完全没覆盖到。注意点watch默认只监控方法进入时的信息如果你想额外看方法退出时的结果要加上-b或-e参数来控制位置或者更常用的是保持默认然后在OGNL表达式里同时取返回值和异常即可。Arthas的做法是默认在方法结束以后统一输出所以直接查returnObj和exception是能拿到的。3.5 trace定位方法内部耗时分布有时候接口慢但你不知道热点是在内部哪个子调用上。trace命令可以追踪一个方法内部的所有子调用输出每一层的耗时以及调用次数。[arthas12345]$ trace com.example.OrderService createOrder #cost 200这个命令的含义是当createOrder整体耗时超过200毫秒时打印方法内部的调用链耗时详情。每一行会显示类名、方法名、耗时、调用来源。trace是我处理性能问题时最依赖的工具。比如你怀疑数据库查询慢但数据库监控又没查到慢SQL或者怀疑HTTP外部服务超时却不知道具体在哪个阶段。trace一跑整个方法内部的每一步耗时一目了然是内存计算慢还是RPC调用慢还是某个循环里做了太多无用计算全都能看到。和watch不同的是trace的成本相对高一些尤其是在高频调用方法上开启trace会有一定的性能开销。所以在生产环境使用时要控制好追踪的目标方法不要挂在整个入口上大规模trace更推荐在定位到大致方法后细粒度去开trace。3.6 stack方法被谁调用、调用链长什么样stack命令会输出当前方法被调用的调用路径简单理解就是“谁调用了我”。用法[arthas12345]$ stack com.example.OrderService createOrder这个命令适合回答“这个方法为什么被反复触发”、“这个入口是被人调用还是内部定时任务触发的”。有一次排查一个数据错乱问题就是想弄清楚某条数据为什么会走到一个异常分支最后用stack发现原来是消息消费逻辑里间接调用了同一个方法导致数据状态被重复修改。stack还支持按条件输出比如只输出耗时超过阈值的调用[arthas12345]$ stack com.example.OrderService createOrder #cost1000这样既能看到调用链也能筛选出慢调用用来排查偶发性慢请求特别好使。3.7 tt时间隧道记录方法调用的历史现场如果说watch是“实时看每一次调用”那tt就是“把调用的现场记下来回头慢慢看”。tt命令能记录一段时间内某个方法的调用快照包含入参、返回、异常、调用时间等并且你能用tt的Play功能去重放某一次调用。基本用法[arthas12345]$ tt -t com.example.OrderService createOrder -n 20意思是记录最近20次createOrder调用的快照。输出里每行有一个ID如果你想看第8次调用的具体参数和返回值[arthas12345]$ tt -i 8有一次线上偶发问题最让人头疼的就是无法复现当时我用tt挂了半小时记录到了那一次异常调用的完整入参然后用这个参数在测试环境一跑就复现了整个排查从“玄学”变成了“确定性”。tt的代价比watch更大因为它要在方法出入口都插入拦截逻辑并且保存快照。所以开启时间不要过长记录次数也要限制比如-n 20抓完立即关闭。3.8 monitor统计方法调用频率、成功率和耗时趋势如果你想看一个方法在一段时间内的整体表现比如每分钟被调多少次、成功率如何、平均耗时多少monitor就是干这个的[arthas12345]$ monitor com.example.OrderService createOrder -c 30表示每30秒统计一次createOrder的调用次数、失败次数、失败率、平均耗时等。这个命令适合不需要看单个调用细节而是想看宏观趋势的场景。比如压测过程中观察某个方法的TPS、RT变化或者线上某个接口失败率突然升高时先确认是不是这个原因。3.9 sc、getstatic与ognl查看类信息、读取静态变量、执行任意表达式这三个命令放到一起讲因为它们是Arthas里“探查类结构”的组合拳。sc用来搜索所有已加载的类[arthas12345]$ sc *OrderService*输出会显示类全限定名、类加载器、类状态。如果想看类的详细结构加-d参数[arthas12345]$ sc -d com.example.OrderService这样会输出类的修饰符、注解、父类、接口、成员变量等。getstatic用来读取类的静态变量[arthas12345]$ getstatic com.example.OrderService CACHE_SIZE比如某个配置类的静态字段是在启动时加载的又没办法动态刷新用getstatic能直接看到当前内存里的值判断配置是否真的生效了。ognl是最灵活的命令它能在目标JVM里执行任意表达式。比如调用某个静态方法、获取某个对象的字段、甚至修改某个字段的值[arthas12345]$ ognl com.example.GlobalConfiggetConfig()也可以带上下文执行比如获取当前线程上下文的某个Bean[arthas12345]$ ognl -x 2 #context org.springframework.web.context.ContextLoadergetCurrentWebApplicationContext(), #bean context.getBean(orderService), #bean.queryStatus()ognl的“任意执行”能力非常强但也要格外小心它相当于给了你在生产环境执行代码的权限。尤其是在写修改类型表达式时一定要仔细确认不要改错字段。3.10 mc与redefine动态更新类逻辑redefine是我用得比较谨慎但关键时刻能救命的命令。它允许你修改已加载类的字节码相当于在运行中“热更新”类的逻辑。基本流程是先用jad把目标类反编译出来导出源码到本地改完逻辑以后用mc命令编译成.class文件再用redefine命令把它加载进JVM。步骤如下[arthas12345]$ jad --source-only com.example.OrderService /tmp/OrderService.java # 然后在本机编辑 /tmp/OrderService.java改好逻辑 [arthas12345]$ mc /tmp/OrderService.java -d /tmp/output [arthas12345]$ redefine /tmp/output/com/example/OrderService.class注意事项redefine只改变后续新执行的逻辑对已经执行到一半的调用是无效的而且redefine不会改变类的结构你不能新增字段、不能新增方法签名、不能增加销毁原结构。一般我拿它来做临时补丁加一个日志输出、调一个参数值等等等新的版本发上去以后就不要再依赖redefine的临时逻辑了。警告redefine本质上是修改运行中的JVM内存里的类定义风险等级很高只适合临时应急。不到万不得已我建议你先尝试用ognl修改配置值、用dashboard观察趋势最后才考虑redefine。而且redefine前一定要把原始类文件备份好保证能回滚。4. 实战案例拆解从CPU飙高到接口超时的排查全流程4.1 案例一CPU突然飙到500%的线程定位事情大约是这样的某跨平台订单服务的CPU在下午两点左右突然飙高监控报警直接打到群里。按照以前的处理方式第一件事是登录机器看top找到CPU高进程的PID然后jstack导出线程栈再手动去找nid对应的线程。这一套流程本身没问题但jstack的线程栈里全是十六进制线程ID要对应到业务线程还得人工比对效率很低。有了Arthas之后流程变成了这样先附着到目标进程执行dashboard马上就能看到哪个线程的CPU占用异常高通常那个线程的ID还会反复出现在刷新面板的最前面。然后执行[arthas12345]$ thread -n 3Arthas会直接打印出CPU占用最高的三个线程的完整堆栈自动把线程名称、CPU时间、堆栈都组装好了。那次我一眼就看到某个线程卡在一个XML解析方法内部最底层是在JVM里做正则匹配数据量一大CPU直接被吃满。顺着栈里的业务类名很快就对应到了代码行发现是一个对超大字符串做了多次replaceAll的循环属于典型的正则回溯问题。这个案例关键点在于thread -n非常省事省掉了jstack 人工匹配nid的过程。如果只靠jstack十几分钟能定位就算不错了用Arthas一两分钟就锁定了根因。4.2 案例二接口偶发超时怎么抓到慢调用的具体位置另一个印象深刻的案例是某个查询接口偶发超时时好时坏。我先打开trace守护在入口方法上[arthas12345]$ trace com.example.QueryService queryDetail #cost 1000跑了大约十分钟抓到了一次耗时1200毫秒的调用快照。结果显示大部分耗时花费在调用下游的某个HTTP接口上时间约900毫秒而我对这个下游HTTP接口的容错判断阈值是500毫秒所以导致上游超时。顺着这个线索检查了下游服务的响应时间分布发现是对方服务在整点时刻做批量任务导致该时段RT拉长。后续调整了调用方的超时时间并且给这个下游调用增加了本地缓存问题彻底解决。如果不用trace靠加日志排查这种偶发问题可能要发好几个版本才能抓到一个慢样本而且很有可能会因为随机性导致错过现场。trace的价值在于它不需要任何提前埋点事后诊断能力很强。4.3 案例三方法返回值为什么和预期不一致还有过一次经历是同事在排查一个对账系统的金额计算差异。日志里打了入参也打了最终返回值但中间经过了好几个子方法的嵌套调用看不出是哪一步把金额改错了。这种情况watchstack组合拳非常管用。先watch最外层方法看看入参和最终返回值如果出入参没问题就深入内部方法分别watch找到第一个出现差异的那一层。当时我们用watch单独盯了一个金额调整的方法抓到的入参是一个对象对象的某个金额字段是100.00但returnObj返回时变成了99.99。再往下一层看发现里面调用了一个舍入工具舍入模式默认是向下取整而业务方预期是四舍五入。顺着这个链路往下追又用stack查了舍入工具方法的调用来源确认只有这一条业务路径走错了舍入模式。整个过程没有重启、没有改动一行代码一个下午就把根因挖干净了。5. 常见问题与排查思路Arthas使用中踩过的坑5.1 attach失败提示Unable to open socket file这个报错最常见的场景是你用的系统账号和目标Java进程的启动账号不一致。比如应用是用service用户启动的你sudo到root用户执行Arthas那么Arthas无法读取目标JVM的临时目录文件就会出现socket文件路径相关报错。解决办法很直接换成和启动进程相同的用户执行。如果一定要root操作可以考虑安装Arthas到目标用户目录下并用su切换到该用户后再启动。5.2 所有命令都能执行但watch/trace没有任何输出这个有两种可能。一种是方法已经启动了很久没有被调用你可以先用monitor确认调用是否真的存在。另一种是方法是在其他类加载器里加载的而Arthas默认匹配的类加载器集合不对。特别是用到自定义类加载器、OSGi环境时需要用sc -d确认类的类加载器信息然后用classloader命令配合指定类加载器再观察[arthas12345]$ classloader -l然后指定类加载器的索引再对目标方法执行watch。你也可以用watch时带上-c参数指定类加载器的hash值。当然现实中多数Spring Boot应用是单一类加载器基本不会遇到这类问题。5.3 对象层级太深输出被截断默认情况下Arthas为了避免把大量数据一股脑打印出来会限制OGNL表达式输出对象的展开深度。如果你想看更多字段用-x参数调大即可。比如[arthas12345]$ watch com.example.OrderService createOrder {params, returnObj} -x 5如果-x调得太大导致输出非常长影响阅读可以同时用-n限制观察次数比如-n 3只输出三次结果。5.4 ognl表达式写复杂了容易出错怎么调试ognl表达式在Arthas里虽然功能强大但一旦写复杂了语法错误可能比业务代码本身还难排查。我的习惯是先在本地用一个简单的Spring Boot应用试表达式确认无误了再拿到生产环境执行。还有一个技巧是拆分成小的表达式逐步验证先获取对象引用再访问字段不要一口气写超长的OGNL链条。比如你想拿到Spring容器里的某个Bean并调用方法可以先执行[arthas12345]$ ognl #ctxorg.springframework.web.context.ContextLoadergetCurrentWebApplicationContext(), #ctx确认这个对象能拿得到再去扩展后面的字段访问。5.5 Arthas本身对性能的影响很多人会担心Arthas附着之后影响业务性能。实话实说开启watch/trace/monitor这类命令会对目标方法做字节码增强有一定额外开销。如果目标是低频方法影响几乎可以忽略如果目标是高QPS方法性能损耗会明显一些。所以生产使用有几个原则只在需要排查的时间窗口打开命令用-n限制记录次数用完马上关闭。不要同时开启大量方法的trace/watch。优先用dashboard和thread这类低开销命令做初步排查锁定目标后再用watch/trace做精细分析。5.6 Arthas版本太旧导致命令行为不一致Arthas迭代速度比较快一些命令的参数和输出格式在旧版本里可能不兼容。如果你发现某个命令行为和文档不一致优先检查版本[arthas12345]$ version建议下载最新release版本并保持目标环境和运维脚本里使用的版本一致免得今天在这台机器上可用、明天换台机器就报错。5.7 容器环境里attach的特殊情况容器化部署越来越普遍在容器里用Arthas有时会遇到PID namespace限制导致attach失败或者容器里没有安装完整JDK只有JRE。解决办法是基础镜像里提前安装JDK而不是只有JRE。使用Arthas的agent模式启动应用避免动态attach的限制。容器内执行Arthas时确认/tmp目录可写并且PID对得上宿主机还是容器内的Namespace。5.8 无法访问8563端口如果Arthas在远程机器上运行而你想从本地连上去操作需要确保8563端口默认telnet端口和8562端口默认HTTP端口被你当前的网络允许连接。一些公司内部网络策略较严格需要提网络放行申请或者你可以用ssh隧道把远程端口映射到本地再访问这样就不用额外开防火墙端口了。整体来说Arthas确实是一个让我“用过就回不去”的工具。它解决的不是某一个具体的问题而是把人从“盲人摸象”的排查模式里解放出来让你能直接看到运行中JVM的内部。刚开始接触时可能会被它的命令数量吓到但只要把dashboard、thread、jad、watch、trace、tt这几个核心命令用熟就已经能覆盖绝大部分日常排查场景了。我自己一次一次踩坑之后才慢慢意识到满屏的命令并不可怕关键是把每一条的适用时机记清楚——先看全局再锁线程然后用watch或trace深入方法内部再用ognl处理疑难杂症这套思路比硬记命令参数重要得多。希望这篇文章能帮你少走一些弯路也欢迎你把这些命令在测试环境多试几遍熟能生巧之后线上问题再也不用靠“重启大法”来解决了。