1. 为什么选Arthas:一次线上事故的解法
先讲一段真实经历。去年年中,我们有个核心接口在夜间低峰期偶尔会报超时,监控面板上的P99从300毫秒突然跳到5秒,持续几分钟后又自己恢复。告警群里值班同事轮流上去看日志、看GC、看CPU,折腾了两天也没定位到根因。最让人头疼的是,这个问题只在生产环境偶发,测试环境怎么压都压不出来,而生产环境又不允许随便停服、加日志、重启验证。后来是靠着Arthas在线上直接trace慢调用链,才锁定到一段在低并发时表现正常、在高并发下会触发锁竞争的资源初始化代码。那次之后,Arthas就成了我们团队线上问题排查的标配工具。
Arthas是阿里巴巴开源的一款Java诊断工具,定位非常明确:在不需要重启应用、不需要额外部署Agent组件的前提下,对运行中的JVM进行实时诊断。它解决的是Java后端开发者最头疼的一类问题——线上环境出了问题,但你没有足够的现场信息去复现和定位。对于正在跑着业务的Java进程,Arthas可以帮你做这些事情:查看类加载信息、反编译线上代码、监控方法调用参数和返回值、追踪方法级别的调用链耗时、查看线程栈、采样分析CPU热点、在线修改运行中的逻辑等等。它的适用人群很清晰:所有做Java服务端开发、需要跟生产环境打交道的人。不管你是初入行的开发,还是带团队的技术负责人,只要有过线上问题定位困难的经历,Arthas都值得你花半天时间系统学一遍。
网上关于Arthas的教程其实不少,但大多是命令手册的翻译版,告诉你怎么用watch、怎么用trace,却很少解释这些命令背后的原理、适用场景以及真正的进阶玩法。所以这篇内容,我根据自己的实际使用经验,把Arthas从启动到常用命令、再到OLGN表达式和批处理脚本的进阶玩法串一遍,重点讲清楚那些手册里不写、但实际排查中非常有用的细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 启动与连接:从下载到attach,那些拦路虎
2.1 一个反直觉的事实:listener模式与attach机制
很多人第一次接触Arthas,以为它跟SkyWalking那样的Agent一样,需要在应用启动时通过-javaagent参数挂进去。实际上Arthas是启动时动态attach到目标JVM上的,不需要预先在应用启动参数里做任何配置。它启动后会以独立进程的方式运行,跟你正在诊断的Java进程通过socket通信。这个设计的好处显而易见:不需要提前规划、不需要重发应用,任何时刻你发现线上有问题,都可以用Arthas临时接入进去。
但这也带来一个容易踩坑的点——Arthas运行的目标是“附着”到另一个JVM上,而JVM的attach机制依赖于能否拿到目标进程的PID。Arthas拿到PID的方式,默认是通过jps工具枚举本机所有Java进程。如果这一步就卡住了,后面所有功能都无从谈起。
2.2 "arthas启动无法获取jps进程"的排查链路
这个热搜词下面的问题,我见过太多次了,而且绝大多数不是Arthas本身的问题。根据我的排查经验,按下面这个顺序逐个检查,基本能解决99%的情况。
第一,检查Arthas跟目标JVM是否运行在同一用户下。JDK8及以下版本中,jps获取进程信息依赖的是/tmp/hsperfdata_<username>目录,每个用户只能看到自己启动的JVM。如果你用root用户启动Arthas,去看一个普通用户启动的Java进程,jps里就是看不到的。解决方案很简单:切换到进程所属用户再启动Arthas,或者赋予/tmp/hsperfdata_*目录的访问权限。
第二,检查目标JVM是否开启了共享内存。有个JVM参数叫-XX:+PerfDisableSharedMem,生产环境如果为了性能优化加了这参数,jps就完全无法感知这个进程。这种情况不用纠结,直接用ps -ef | grep java找到进程PID,然后执行java -jar arthas-boot.jar <PID>绕过进程枚举直接attach。
第三,容器环境的PID命名空间隔离问题。在Docker/K8s里,你通过docker exec进到容器内执行jps,看到的是容器内的PID,而用宿主机上的Arthas去attach,看到的却是宿主机PID。这两者可能对不上。最稳妥的做法是在容器内执行Arthas,或者在宿主机上用docker inspect --format {{.State.Pid}} <container>查出进程的真实PID,再手动指定给Arthas。
2.3 下载与attach失败的兜底手段
Arthas的下载地址是官方GitHub Releases,或者直接从阿里云的镜像仓库拉取。最简单的安装方式如下:
bash复制curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后它会出现一个交互式列表,让你选择要attach的Java进程。如果这一步发现列表是空的,上面那套排查链路走一遍基本能解决。但如果列表里有进程、选择后却提示attach失败,那就要检查别的方向了。
最常见的原因之一是JDK版本差异。JDK9之前,attach依赖$JAVA_HOME/lib/tools.jar,如果JAVA_HOME指向的是JRE而文件缺失,attach就会失败。JDK9之后,attach能力被收敛到jdk.attach模块里,部分精简版JDK或自定义裁剪过的运行时会把这个模块去掉。判断方法很简单:在目标JVM同样环境下执行jps -l,如果jps本身也能跑但Arthas attach报错,那就很可能是JDK模块裁剪问题。
另一种情况是权限限制。有些团队的Java进程由专门的运维账号启动,开发账号虽然有应用的操作权限,但没有attach权限。Arthas官方有个配置项可以指定attach用户,或者用setcap给Java二进制授权。不过从经验来说,这种操作最好走团队内的工单流程,让运维协助执行,不要自己在生产环境动权限,安全边界很重要。
还有一个小技巧值得分享:Arthas启动后每次都要交互式选择进程,而实际排查中你通常只关心固定的一两个服务。建议写一个简单的启动脚本,把PID发现逻辑提前注入进去:
bash复制PID=$(ps -ef | grep 'spring-boot.*your-service' | grep -v grep | awk '{print $2}')
java -jar arthas-boot.jar $PID
这样一键就能进入目标进程,省去每次找PID的时间。实测下来,特别是频繁切换多个服务排查时,这个脚本能节省大量时间。
3. 核心命令的进阶用法:watch、trace、stack不只是“试试”
3.1 watch的条件表达式与耗时过滤
watch是Arthas里使用频率最高的命令,你可以在不修改代码的情况下,观察某个方法的入参、返回值和抛出的异常。但很多人只会用最简单的形式:
bash复制watch com.example.UserService getUser '{params, returnObj}'
这能看,但信息太杂乱,尤其是在高并发接口上,一秒钟可能触发几十次调用,输出刷得根本看不完。进阶用法是用条件表达式做过滤,让它只输出你关心的那几次调用。
watch的条件表达式基于OGNL标准,你可以对参数值、返回值甚至方法耗时做判断。比如我只想看getUser方法中入参id大于1000、且耗时超过200毫秒的调用:
bash复制watch com.example.UserService getUser '{params, returnObj}' '#params[0] > 1000 && #cost > 200' -x 3
这里的#cost是Arthas内置的变量,表示方法调用耗时。-x 3表示展开结果的深度为3层,避免深层嵌套的大对象直接把终端刷爆。理解这个组合的意义在于:你不再是被动地“看现象”,而是主动地在海量调用中筛出异常样本。
还有几个值得记住的参数:-n指定最多输出几次结果,适合只想知道“这个方法到底有没有被调用、长什么样”的场景;-b可以观察方法执行前的入参快照;-e则是观察方法抛出异常时的现场。把这些参数组合起来,watch真正变成了一个线上断点级别的观测工具。
3.2 trace:定位慢调用链路的正确打开方式
trace应该是Arthas在“接口慢”这类问题上杀伤力最大的命令。它做的事情是:追踪一个方法内部调用的所有子方法,并把每个子方法的耗时列出来,帮你一眼看出耗时瓶颈到底在哪个子调用上。
比如我追踪一个Service方法:
bash复制trace com.example.OrderService createOrder
执行后任意触发一次createOrder调用,它就会打印出类似这样的结构:createOrder调用了validateStock花了20ms,调用了deductBalance花了800ms,调用了sendMQ花了5ms。瓶颈在deductBalance上一目了然。
但在真正的生产环境,一个方法内部可能调用了几十个子方法,分支嵌套好几层。很多人的第一反应是把trace结果全量打印出来,然后瞪着屏幕找耗时大的那行。这很低效。我的建议是按层追踪:先追踪最外层入口方法,找到耗时最大的子调用,再对这个子调用单独trace,逐层下沉。这个思路跟二分查找一样,每次把问题范围缩小一半,几轮下来就能定位到最底层的那一行代码。
trace还有一个很实用的参数是-n:
bash复制trace com.example.OrderService createOrder -n 1
它表示只追踪一次调用就自动停止,对于偶发故障的现场抓取特别有用。你预先挂好trace,然后等下次故障复现的那一次请求命中,输出一次现场即可,避免持续增强导致性能损耗被放大。
3.3 stack:反向回溯,用调用栈找“谁在调我”
跟trace的方向相反,stack命令是查看某个方法被哪些调用路径触发的。它解决的问题很典型:某个底层方法突然变慢或报错,你想知道是上游哪个接口的哪条链路调进来的。
比如我发现RedisTemplate.get这个方法在某个时间点频繁超时,但不确定是哪个业务方法在调用它:
bash复制stack com.example.redis.RedisTemplate get -n 5
挂上之后,下一次get被调用时,Arthas会把完整的调用栈打印出来,从filter到controller再到service,一整套链路清清楚楚。实测在排查多服务互相调用、或者一个工具方法被多个上层业务复用的场景时,stack比trace更直接——你不用去猜是谁在调,让命令告诉你。
另外,stack同样支持条件表达式。比如只看某个特定用户ID触发的调用:
bash复制stack com.example.redis.RedisTemplate get '#cost > 100' -n 3
这能把限流、超时这类偶发问题的现场采集精确到具体参数维度,排查效率完全不是翻日志能比的。
4. 追踪URL调用路径:从一次HTTP请求到完整调用链
“Arthas可以跟踪url的调用路径吗”是最近被问得很多的问题。答案是肯定的,而且有两种不同粒度的做法。先说结论:如果你想看一个URL请求从进入Spring MVC开始到返回的完整调用路径,用trace命令直接追踪DispatcherServlet.doDispatch是最粗暴有效的方案;如果你想更精细化地观察业务代码内部的调用链路,则应该先在Controller的方法上trace,再逐层往下追踪。
4.1 从入口开始:追踪DispatcherServlet
在Spring Boot应用中,所有HTTP请求都会先进DispatcherServlet的doDispatch方法。基于这个事实,你可以直接对它挂trace:
bash复制trace org.springframework.web.servlet.DispatcherServlet doDispatch -n 3
然后发起一次HTTP请求,你就会看到doDispatch内部整个请求处理流程的耗时分布:找Handler、解析参数、调用Controller方法、渲染视图、写响应。虽然这一步能看到Controller层的耗时,但默认trace只展开一层子调用,Controller内部又调了哪些方法,还需要进一步往下钻。
这里有个非常推荐的操作:在trace输出里关注那些耗时特别长的子节点,拿到该子节点对应的类名和方法名,再对这个方法单独trace。一层层剥下去,最终一定能把瓶颈定位到具体的SQL、远程调用或计算逻辑上。这个方法在Spring Mvc架构下几乎是通用的,不管你的项目是简单的单体应用还是复杂的微服务,思路都成立。
4.2 用watch精确捕获URL与参数的对应关系
如果你不仅想看到URL的调用路径,还想知道某个URL进来时携带了什么参数、通过了哪些filter,可以用watch命令对doDispatch做参数快照。doDispatch的入参是HttpServletRequest和HttpServletResponse,你可以直接从request里拿出URL信息:
bash复制watch org.springframework.web.servlet.DispatcherServlet doDispatch '{params[0].getRequestURI(), params[0].getParameterMap()}' -x 2
这样每次HTTP请求进来时,控制台会打印出访问的URI和对应的query参数,相当于给生产环境临时装了一个精准的请求日志。跟日志系统相比,它的优势在于可以按条件过滤、用完即走,不会给文件系统制造大量无效日志。
4.3 过滤无关帧,聚焦自己的业务代码
在实际追踪过程中,trace输出会包含大量框架自身的方法调用,比如Spring的HandlerExecutionChain、视图解析器内部的逻辑。如果接口调用链很长,这些框架调用会严重干扰你对业务的判断。我一般会用--exclude-class-pattern参数把框架类排除掉:
bash复制trace org.springframework.web.servlet.DispatcherServlet doDispatch --exclude-class-pattern 'org.springframework.*|org.apache.*' -n 1
这样输出最干净,剩下的几乎都是你自己项目的业务代码调用路径。这个方法在排查自定义业务逻辑的耗时问题时,比看全量trace要清爽得多。
4.4 实战案例:一个慢接口的完整追踪过程
用一个具体例子串一遍。假设线上有个GET /api/order/detail接口,监控系统显示近一小时P99从500ms涨到了3s。我拿到这个信息后的操作流程是这样的:
第一步,先确认请求有没有到Spring MVC。直接对DispatcherServlet.doDispatch做trace,同时用压测工具或真实流量触发几次请求。如果doDispatch本身的耗时就是3秒,问题一定在请求进入后的链路里;如果doDispatch很快,那瓶颈大概率在过滤器链或者更前面的网络层/网关层。
第二步,假设确认了瓶颈在Controller调用内部。从trace输出的子节点里找到那个耗时最长的、包名带你自己项目的子调用,通常是OrderController.detail方法。然后对这个方法继续trace。过程中注意看它的子调用:如果getOrderById耗时2.8秒,但getUserInfo才消耗20ms,那问题很清楚,就在数据库查询或缓存操作上。
第三步,锁定到OrderMapper.getOrderById后,如果还不放心,可以直接用watch观察这个SQL执行时的参数值和返回值,再结合数据库慢查询日志确认是不是索引失效或锁等待。整个定位过程可能只需要几分钟,但如果在没有Arthas之前,光是加日志、发版本、复现、看日志这一轮下来,没有一两个小时根本打不住。
5. 变量、OGNL与批处理:把Arthas变成脚本化工具
5.1 用OGNL表达式做运行时“透视”
Arthas对OGNL表达式的支持,是我个人认为它最被低估的能力。watch、stack、trace里的条件表达式本质上都是OGNL,但很多人只把它当作“写条件”的语法,没有意识到你还可以直接执行任意OGNL表达式来获取JVM运行时的内部数据。
比如你想看线上某个Bean的属性当前是什么值,不用加日志不用上debug,直接一行命令:
bash复制ognl '#config = @com.example.system.Config@INSTANCE, {#config.getTimeout(), #config.getRetryCount()}'
这里的@[类名]@[字段或方法]语法是Arthas OGNL访问静态方法/静态字段的核心。你可以通过它查看Spring容器中任意Bean的当前状态,甚至调用它的任意方法。在排查配置中心下发的配置是否实时生效、或者某些状态字段是否被意外修改时,这个能力比看日志准确得多。
还有一点很实用:你可以在OGNL表达式里调用System.getProperty、System.getenv,或者直接用@java.lang.System@getProperty('user.home')这种形式。如果你临时需要确认运行环境里某个系统变量的真实值,Arthas里就能拿到,比登录主机敲命令还直接。
5.2 反编译查看线上实际运行的代码
曾经有人问过我,“为什么我在本地代码里加了日志,线上还是看不到输出?”原因可能是版本没发上去、灰度批次不对、或者本地代码跟线上运行的不是同一份。这种时候与其猜,不如直接用jad命令看线上真实运行的字节码反编译结果。
bash复制jad com.example.OrderServiceImpl
执行后你会看到线上这个类的完整Java源码(由字节码反编译而来)。如果发现某个方法跟你的预期不一致,说明运行的版本和你本地看到的不同,问题立刻得到解释。我一般结合sc -d命令先查类的加载信息,包括类加载器、代码来源jar包路径、类加载时间等,确认版本信息后才开始看反编译内容。这在多环境多分支并行开发的项目里特别有用。
5.3 批量排查:把Arthas命令写进脚本
Arthas支持非交互式的批处理模式,这是很多团队没有利用起来的进阶能力。使用方法很简单,把命令按顺序写入一个脚本文件,然后用-f参数指定执行:
bash复制java -jar arthas-boot.jar <PID> -f /path/to/diagnose.script
脚本内容示例:
code复制dashboard
thread -n 3
sc -d com.example.OrderService
stack com.example.OrderService createOrder -n 3
trace com.example.OrderService createOrder -n 1
quit
这样一个脚本就能完成一次标准化的进程健康检查。对于经常要做故障复盘、或者新同学刚接手某套系统时的常规体检来说,这种脚本化方式可以保证每次检查的维度一致,不容易漏项。我一般会针对不同服务维护一套基础诊断脚本,配合值班手册使用,新同学拿到后照着跑一遍就能给出基础的诊断结论。
5.4 常用排查命令的组合套路
最后分享几个我在实际排查中验证过的命令组合思路,供你参考:
- 接口突然大量报错的排查:
dashboard看全局指标,thread -n 3看最忙线程,watch在入口异常分支上打快照,stack看是谁打进来的。 - 内存异常的排查:
dashboard观察堆内存曲线,sc -d列出嫌疑类的加载源,heapdump导出堆文件,再结合MAT分析。 - 线程池耗尽的排查:
thread -b直接找出阻塞的线程,再对对应的任务类做trace,定位到阻塞点。
这三个组合是我平时用得最多的“黄金三件套”,几乎能覆盖服务端90%以上的在线异常场景。
6. 性能开销与安全边界:线上工具的使用底线
Arthas虽好,但它本质上是AOP式字节码增强,被增强的方法每次调用都会被织入一段额外的拦截逻辑,必然带来一定的性能开销。很多团队不敢用Arthas,也是担心对在线业务产生影响。我的判断是:合理使用的情况下,开销可以忽略不计,但绝不能无节制地把观测命令挂太久。
先看开销到底有多大。watch、trace这类命令在方法被调用时,会在原字节码里插入收集逻辑,同步输出或判断条件是否命中。单次调用的额外开销大约在微秒级别,对于大部分业务方法来说,这跟业务逻辑本身动辄几毫秒的耗时相比,几乎不可感知。但如果这个方法本身的调用频率是每秒几百万次,那么即使单个开销只有1-2微秒,累计起来的CPU消耗也不容忽视。所以我的使用原则是:所有观测命令都用-n参数限制输出次数,定位到问题后第一时间stop退出Arthas,绝不让它常驻。
另外,Arthas的能力是双向的。它能让你绕开日志系统去“看到”运行时的类和相关状态,同样也意味着如果你误操作了某些危险命令,可能对线上产生不可逆的影响。比如把某个类反编译后修改逻辑再重新加载,这类热更新操作虽然Arthas支持,但在多数公司里应该被严格限制。我个人的建议是:热更新功能能不用就不用,线上环境的原则永远是“最小变更、最快回滚”,通过Arthas做诊断获取信息是一回事,去篡改线上运行逻辑是另一回事,风险等级完全不同。
还有一个容易被忽略的边界是权限管理。Arthas启动后监听在本地的端口,只有同一台机器上的用户可以访问。但如果你的云主机多租户共用、或者有跳板机权限管理不当,Arthas的会话就存在被其他高权限用户看到的可能性。只要Arthas能看到你的进程,它就能做信息收集。这块最好在团队内部达成一致:生产环境的Arthas操作需要权限申请和操作留痕,排查过程全程记录,用完立即退出。
最后再分享一个我在实际使用中的体会:Arthas对新手最友好的地方不是命令多,而是诊断链路清晰、反馈及时。你挂一个watch,立刻就知道线上这个方法到底被没被调用、参数是什么、结果是什么。这种“即时反馈”比打开一堆日志文件去搜索要直观得多。但恰恰因为上手简单,很多人会抱着“查查看”的心态在上面挂一大堆命令,最后生产变慢也不知道是自己造成的。我的经验是:每次动手连接Arthas之前,先在纸上写下“我要回答什么问题、用哪个命令、看哪个方法、看多久”,带着问题去操作,而不是漫无目的地边猜边试。这样既能精准解决线上故障,也能最大限度减少对业务的影响。
