
Arthas的watch命令线上问题排查利器这个标题我相信很多Java后端同学都眼熟。但老实说我见过不少同事把watch当“死命令”用起一个watch看一下参数打出来的结果密密麻麻看不懂然后放弃转头去加日志重新发版。其实watch真正用好了能解决线上排查里非常大的一块痛点——方法入参、出参、类成员变量这些平时“看不见”的东西几乎都能在不停机、不改代码的前提下实时捞出来。这篇博客我打算换个讲法不抄官方文档而是从“我实际排查时怎么用它”的角度把watch的语法、表达式、常见坑一次性说透。不管你是刚接触Arthas的新手还是已经用过但总觉得差点意思的老手这篇文章应该都能给你点启发。1. 线上排查的痛点为什么偏偏是watch1.1 传统排查路子哪里让人头疼先说一个很典型的场景生产环境的接口突然超时或者某个用户反馈数据不对但本地复现不了。你去看日志发现日志里只有一行异常堆栈方法入参是什么、出参是什么、对象内部当时的状态是什么一概没有。就算有日志也往往是“经手人”字段没打全或者打了但被日志框架截断了。这时候你能怎么办加日志、发版、复现、看日志、猜问题一个循环下来少说两三个小时运气不好要折腾半天。更麻烦的是有些问题只在特定用户、特定数据下出现你加了日志也不一定能复现。甚至有些核心方法调用频率极高你不敢随便加日志怕把磁盘打爆。我之前负责过一个交易系统用户报“支付成功但订单状态还是待支付”查了半天日志支付回调的入参确实收到了但订单服务里那个更新状态的方法到底被没被调用、传入的订单号是不是同一个完全没有记录。最后就是靠watch把这个方法盯住发现上层调用时订单号被截断了一位——这种问题靠肉眼看代码真的很难看出来。1.2 watch在Arthas里的定位Arthas是阿里开源的一款Java诊断工具watch只是它众多命令中的一个但绝对是最常用、最能“救命”的那几个之一。它的作用简单说就是观察一个方法在执行过程中的数据。你指定一个类、一个方法Arthas会在JVM里动态织入字节码增强逻辑当这个方法被调用时把入参、出参、异常、当前对象这些信息实时打印出来。打个比方watch就像是给某个方法装了一个“临时的监控摄像头”摄像头跟着方法走方法执行时自动录像方法结束把关键帧给你看。关键是你不需要改动任何业务代码也不需要重启应用装上就能用用完就能拆。1.3 适合用它解决的场景根据我自己的使用经验watch特别适合下面几类问题入参丢失型代码里没打日志或者打了但他方传进来的参数就是不对的你想看真实调用时到底传了什么。出参不符合预期型方法内部逻辑复杂返回值算出来不对你想确认最终返回给调用方的是什么。状态突变型某个对象是整个类的成员变量在处理过程中被修改了你想看它在方法执行期间变成了什么值。异常被吞型方法内部catch了异常但没有往外抛你想在异常发生的那一刻抓到原始异常对象。当然watch也不是万能的。它本身有性能开销不适合对超高QPS的方法长时间挂它只能看方法内部可见的数据一些极端底层的东西可能要看Arthas之外的工具。但大多数业务方法的问题watch都能覆盖。2. watch命令的语法和几个核心概念2.1 命令格式拆解watch命令的基本格式是这样的watch class-pattern method-pattern express condition-express [options]看着有点抽象我拆开讲一下。class-pattern是类名匹配method-pattern是方法名匹配express是你要输出的表达式condition-express是触发条件[options]是一些额外的开关比如是否在方法执行前观察、是否观察异常、结果展开多少层等。一个最经典的例子是Arthas官方文档里的watch demo.MathGame primeFactors {params, returnObj, throwExp} -x 3这条命令的意思是观察demo.MathGame类里面primeFactors方法方法执行结束后把入参params、返回值returnObj、异常throwExp都打出来结果最多展开3层。-x 3很重要如果不加默认只展开1层复杂对象打出来就是一堆toString()结果根本没法用。2.2 四个核心参数怎么理解class-pattern和method-pattern比较好理解就是定位你要观察哪个方法。要注意的是类名默认要写全限定名比如com.example.service.OrderService但Arthas也支持模糊匹配比如*OrderService、*Service.order*这种写法在实际中非常实用。比如你只记得方法名里有“create”就可以写*Service create*来匹配。express是watch的灵魂。它支持OGNL表达式内置了几个变量供你使用params方法的参数数组params[0]是第一个入参params.length可以拿到参数个数。returnObj方法的返回值。throwExp方法抛出的异常对象。target当前调用这个方法的实例对象也就是this。clazz当前类的Class对象。我用得最多的是{params, returnObj}、{params[0], returnObj}、{target, params, returnObj}这种组合。你也可以只写一个表达式比如params[0]、target、returnObj.toString()自由组合。condition-express就是过滤条件可以理解为“只有满足这个条件才打印”。比如你只想看某个userId参数等于1001的调用就可以写params.length 0 params[0].id 1001作为条件。这个机制在生产环境特别有用不然高频方法会刷屏输出根本看不过来。2.3 几个常用的开关选项watch默认是在方法执行结束后finish阶段观察包括正常返回和抛异常两种情况。但有些场景你需要在方法执行前看或者只想看异常发生的现场这时候就要用开关-b在方法调用前观察也就是before阶段。入参这时候已经传进来了但方法还没执行适合看“方法进来时是什么样”。-f在方法返回后观察也就是finish阶段默认是打开的。-s只在方法正常返回后观察属于finish的子集。-e只在方法抛出异常后观察适合抓异常现场。-n限制观察次数比如-n 1表示只观察一次就自动结束。-x结果展开层级数默认是1建议调试复杂对象时开到3或5。-E开启正则表达式匹配默认是通配符匹配。举个例子你想抓异常可以这样写watch com.example.OrderService updateState {params, throwExp} -e -n 3 -x 2意思是只看updateState方法抛出异常的情况把入参和异常对象打出来最多观察3次结果展开2层。3. 实战watch查看方法入参的几种正确姿势3.1 最简单直接的全量入参查看先看一个最常见的用法。假设我们有一个订单服务用户反馈某笔订单状态不对我们想看看更新订单状态的方法到底被传了什么参数进来watch com.example.OrderService updateState {params} -n 5 -x 2执行这条命令之后Arthas会进入监听状态。这时你去触发一次业务操作等这个方法被调用的时候控制台就会输出类似这样的信息Press Q or CtrlC to abort. Affect(class-cnt:1 , method-cnt:1) cost in 68 ms. ts2024-06-15 14:23:45; [cost0.123ms] com.example.OrderService.updateState [params]java.lang.Object[ Integer[1001], Order[ idLong[900001], statusInteger[1], ... ], ]这里能清楚看到第一个入参是1001第二个入参是一个Order对象里面有哪些字段一目了然。-x 2是因为Order对象里面还嵌套了其他对象展开2层才能看到关键字段。有一点要提醒-n一定要加。不加的话只要这个方法是热点方法输出会一直刷。你可能会被淹没在海量输出里。当然如果你就是想持续观察一段时间那就不要加-n等看完按Q或CtrlC退出但建议配合condition-express过滤。3.2 带条件过滤只抓你想看的那次调用很多时候方法被调用的频率很高但问题只发生在特定参数上。比如updateState被调用了几十万次只有orderId为900001的那一次出了问题。如果你不加条件输出的日志会刷到你怀疑人生。这时候condition-express就派上用场了。还是上面的场景我们只想看params[0]等于900001的调用watch com.example.OrderService updateState {params} params[0] 900001 -x 2看到没就是多了一个参数params[0] 900001。这个表达式支持OGNL所以什么params.length 0、params[0].id 1001、target.getUserId() 88都可以写。在排查具体用户问题时用条件过滤几乎是一步到位。我踩过的坑是条件表达式里如果要用对象的字段比如params[0].getUserId()注意一定要调用getter方法直接写params[0].userId是取不到的因为OGNL在Arthas的安全策略下默认不直接开放字段访问。后面讲成员变量的时候还会再提这一点。3.3 入参是复杂对象、List、Map时怎么办生产环境里的方法入参往往不是简单的Long、Integer而是一个个DTO、实体对象甚至是ListOrder、MapString, Object这种集合结构。这个时候关键就是-x参数的设置。比如入参是一个ListOrderwatch com.example.OrderBatchService batchUpdate {params[0]} params[0].size() 0 -x 3-x 3会展开到第三层也就是能看到List里的每个Order对象的字段。如果Order里再嵌套了Address对象建议再加到-x 4或者-x 5。但这里有个权衡展开层数越多输出内容越冗余我个人一般从-x 2开始试不够再看情况往上加。还有一个小技巧如果对象里有某些字段没有setter方法、不是public的toString()打印不出来但你真的很想看那就别指望直接输出字段了。你可以用表达式调用getter方法比如watch com.example.OrderService updateState {params[0].getOrderId(), params[0].getUserInfo().getUserName()} params[0] ! null -x 2这样打印出来的结果就是这两个getter的返回值干净、聚焦。Arthas干这种事情很顺手本质上是在JVM层执行OGNL表达式不受Java访问权限的约束所以private方法理论上也能通过反射调用到。不过稳妥起见还是优先用getter。4. 实战watch查看方法出参与异常现场4.1 正常情况下的出参查看查看方法出参是watch的另一个高频场景。比如接口返回的数据总是有问题你想确认服务端实际返回了什么内容。命令很直观watch com.example.OrderService getOrderInfo {params[0], returnObj} -x 3当方法被调用时输出里会有returnObj这个字段里面就是方法的返回值。我在实际排查中经常干的一件事是先确认入参没问题再确认返回值有问题然后把排查范围锁定在方法内部逻辑。这比从头看代码省事太多。有一点要注意如果方法是void类型returnObj会是null。这时候你不能说“没返回就是执行失败了”得配合方法内部的状态变化来看。比如方法执行完后某个成员变量的值是否变化了这就要用到下一节讲的target变量。4.2 只抓异常现场-e 参数的用法线上最恶心的问题之一就是“异常被吞了”。上游调用方只看到超时或者失败但实际异常信息被方法内部的try-catch吃掉没有打日志。这种情况用watch的-e参数特别有效。比如这段代码public void syncOrder(Order order) { try { // 业务逻辑 } catch (Exception e) { // 只记录了一个error级别日志但没打出完整堆栈 log.error(sync order failed); } }你就可以这样抓watch com.example.OrderService syncOrder {params[0], throwExp} -e -n 10 -x 3-e的意思是只在方法抛出异常时触发输出。这样你就能看到异常发生时的入参、异常对象本身。throwExp可以继续用OGNL取里面的字段比如throwExp.getMessage()、throwExp.getCause()这对判断异常根因帮助巨大。这个场景我强烈建议配合-n使用因为异常不一定每次都出现你挂个-n 10抓够10次就自动停了不会一直挂在生产环境上。4.3 查看方法“执行后状态的改变”有时候你不仅关心方法的返回还想知道方法执行完后某个对象变成了什么样。举个例子有个OrderContext对象方法内部会往这个对象里塞各种字段最后再返回。你希望能直观对比方法执行前后这个对象的差异。这时候可以把target和params结合起来看watch com.example.OrderService fillContext {params[0], target.getContext(), returnObj} -x 4target.getContext()拿到的是当前对象this里面的成员变量。如果你发现target.getContext()在执行后比入参多了一些字段那就能确定方法内部确实修改了这个对象。这个组合非常多用途既能看入参又能看出参还能看对象的中间状态变化可以说是watch里最花哨也最实用的一种玩法。5. 实战watch查看类成员变量不止target那么简单5.1 target、params、clazz三者的区别先说清楚几个容易混淆的内置变量params是方法参数是每次调用都不同的。target是当前调用方法的实例对象也就是this。同一个对象如果多次调用同一个方法target是同一个实例。clazz是当前类的Class对象适合看静态字段或者类级别的信息。要查看类成员变量主要用target。比如watch com.example.OrderService process {target} -n 1 -x 2这样会把这个OrderService实例打印出来里面的所有成员变量自然都能看到。但直接打印整个target对象的问题在于如果对象里引用了别的对象而那个对象没有友好的toString()你看到的可能就是一堆com.example.xxx123abc这样的内存地址没什么卵用。所以更推荐的做法是直接指定字段的getter方法watch com.example.OrderService process {target.getOrderCache(), target.getConfig()} -n 1 -x 35.2 用OGNL表达式精确读取成员变量前面说了直接打印整个对象不一定看得清。你想精确看某个成员变量最好用getter。但有些老代码没有写getter或者成员变量是private final的只能反射访问这时候怎么办Arthas的OGNL表达式其实可以调用反射相关的方法但那样写起来比较猥琐。我更推荐的做法是如果真的特别想看一个私有字段的值可以用Arthas其他命令配合比如vmtool或者ognl命令直接执行表达式。如果坚持用watch也有一种写法watch com.example.OrderService process target target ! null -x 1然后看输出的target对象里有没有你需要的字段。这里要说一下-x级别决定了对象内部展开的深度。万一你只看到target的内存地址但字段没有展开那就把-x调大比如-x 3、-x 5。如果对象里有循环引用输出可能会很吓人这时反而要调小-x用getter精确取值。5.3 查看静态字段和类级别的信息有时候问题出在静态变量上。比如一个ConfigHolder类里有个private static MapString, String CONFIG_MAP怀疑这个Map在生产环境被某些操作污染了。这时候target就不管用了因为静态变量不属于任何实例。一种办法是用clazzwatch com.example.ConfigHolder getConfig clazz -n 1不过说实话这种情况下用watch不是最优解Arthas的ognl命令专门干这种事情ognl -c com.example.ConfigHolder #root.CONFIG_MAP但这种做法需要你提前知道类加载器和类名而且访问私有静态字段也需要绕一下。回到watch这个话题我的经验是如果只是临时看一眼静态变量优先用ognl如果是要在某个方法被调用时反复查看才用watch配合clazz变量。6. 生产环境使用watch的常见坑和避坑手册6.1 表达式写错了怎么排查watch的OGNL表达式写起来灵活但也很容易踩坑。最常见的几个报错或异常行为表达式返回null写的字段路径不对比如params[0].userId如果字段不是public的OGNL默认拿不到会返回null。解决办法是改写成params[0].getUserId()。下标越界params[0]访问了不存在的参数。比如方法只有两个参数你写了params[2]。这种直接看方法签名就能确认。条件表达式抛异常比如params[0].getOrders().size() 10如果getOrders()返回nullOGNL会有NPE。这时候要在条件里加空判断params[0].getOrders() ! null params[0].getOrders().size() 10。我调试的时候有个习惯先用最简单的表达式确认方法能被监听到比如直接watch xxx yyy params -n 1能看到输出后再逐步增加复杂度。不要上来就写一个超长的OGNL报错了都分不清是语法问题还是字段路径问题。6.2 生产环境性能影响别让watch成为新的故障源这里必须认真提醒一下Arthas的watch本质是字节码增强方法每次被调用都会经过增强逻辑的计算和判断必然带来额外开销。对绝大多数业务方法来说这个开销可能只有零点几毫秒可以接受但你要是对一个被调用几十万次、单次执行本身还不到0.1ms的方法挂watch那影响就会被放大。我在生产环境用watch有几个铁律一定要加-n限制观察次数尤其是排查热点方法时抓几个样本就够了。尽可能加condition-express过滤只观察特定参数下的调用。不用的时候马上退出按Q或者CtrlC结束watch监听。不要同时在多个方法上挂watch会放大总体开销。团队协作时用完要告知同事避免一个人挂完忘了关、另一个人又挂一个新的最后叠了一堆增强。生产环境完全可以用watch这是一把高效的手术刀但你不能把手术刀当电锯用。6.3 输出结果太长、对象嵌套太深怎么办Arthas默认的-x 1输出很浅复杂对象基本没法看但你把-x调到10又会刷出几十屏内容连日志都看不清。我的经验是分级排查第一轮-x 1或-x 2先确认方法有没有被调用、入参大概是什么类型。第二轮针对可疑字段用getter表达式精确取比如params[0].getUser().getName()。第三轮如果确定要看整个对象的完整结构再把-x加到3或4偶尔用到5。还有一个思路是用watch配合-n 1把单次调用的输出存到文件里慢慢看。Arthas本身支持命令结果重定向或者你直接复制终端滚动缓冲区都比盯着实时刷新舒服。6.4 和其他Arthas命令配合用更香watch不是万能的有些场景换一个命令可能更合适你想看一个方法的完整调用路径、每层耗时用trace而不是watch。你想把一个请求从入口到出口整条链路都记录下来用tt也就是TimeTunnel它能录下方法调用的现场之后还能tt -p重放。你想动态改某个字段的值来验证假设用ognl或者vmtool。你想看方法被谁调用的用stack。我个人的组合拳是先用trace看方法调用链路和耗时分布锁定可疑方法再用watch盯住这个方法的入参、出参和成员变量如果需要复盘具体某一次调用的完整上下文用tt录制。这套组合下来大部分线上疑难杂症都能在一顿饭的功夫里定位到根因。最后再分享一个小技巧。watch的condition-express不仅可以过滤入参还可以过滤出参。比如你想找“所有返回值为null”的调用可以这样写watch com.example.OrderService getOrderInfo {params[0], returnObj} returnObj null -x 2看条件表达式里直接用了returnObj这个变量。这种条件在排查“为什么某些请求返回空”的时候特别有用。我当年排查过一个诡异问题只有少量请求返回空数据客户端表现是偶发白屏后来就是靠这个条件过滤抓到几个returnObj null的现场发现是缓存里存在脏数据导致的。没有watch这个能力的话这种问题真不知道要熬几个通宵才能查出来。用watch这件事说起来简单但真正用得顺手还是要在实际项目里多磨几次。希望这篇文章能让你少走一些我走过的弯路。