十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Arthas watch命令实战:从入参观察到成员变量的线上排查指南

Arthas watch命令实战:从入参观察到成员变量的线上排查指南 1. 线上问题查不到日志watch命令就是那把手术刀1.1 一个典型的线上故障排查场景干过几年Java后端的人基本都经历过这种抓狂时刻某天线上突然冒出一个偶发问题用户那边报下单失败你翻遍日志只看到一句干巴巴的系统异常没有堆栈没有上下文。业务方催得紧你心里清楚问题八成出在那个调用频率极高的Service方法里但那个方法没有打印入参也没有打印出参几十个分支条件来回嵌套你盯着代码看了半天也猜不出到底走了哪条路。更尴尬的是本地复现不了。测试环境的造数成本高压测又压不出同样的场景。你甚至想过直接把临时日志代码部署上去打一版带System.out的包上去抓到了再撤回来。但生产环境发布是有流程的改代码发布少说要半小时遇到灰度还要等流量比例等日志真的出来了问题可能已经过去下一轮又不知道什么时候再犯。这就是Arthas存在的意义。它能直接attach到运行中的JVM上不改代码、不重启进程用一条命令就能观测目标方法的入参、出参、异常信息甚至能把对象内部某个成员变量的实时值拉出来看。而这一切操作只需要几百毫秒的预热时间对线上服务几乎没有侵入感。1.2 watch命令到底能看什么很多人第一次听说Arthas是因为它在阿里内部解决过大量线上疑难杂症但真正上手之后会发现日常排查中用得最多的其实就是watch、trace、tt这三板斧。其中watch是观察能力的天花板它能在方法调用发生时把执行上下文完整地拍一张照给你。具体来说watch能拿到四类数据方法的入参包括每一个参数的完整对象结构方法的返回值方法正常执行结束后拿到returnObj方法抛出的异常catch住throwExp就能定位异常现场发起调用的对象本身也就是this通过它可以读取当前对象的任何成员变量。这意味着很多原本打死也查不出来的问题在watch面前就是明牌。比如某个状态字段在并发下被改坏了你可以直接watch修改它的那个方法在调用前后分别读一下字段值比如上游传进来的参数少了个关键属性你可以watch入口方法把入参对象整个展开看看接收到的到底是个什么东西。这篇文章我就不讲那些官方文档里已经写得明明白白的安装步骤了重点聊聊watch命令从入门到实战的几个关键阶段尤其是类成员变量的获取——这一步卡住了很多人网上教程大多一笔带过实际用起来坑却不少。2. 先把语法吃透watch到底在watch什么2.1 一条完整命令的构成watch命令的标准语法是这么写的watch class-pattern method-pattern express condition-express四个部分缺一不可但后面两个可以根据场景留空。拆开来看class-pattern类名表达式支持全限定名也支持通配符。比如com.example.OrderService或者*OrderServicemethod-pattern方法名同样支持通配符express观察表达式这是最核心的部分决定你想看什么condition-express条件表达式满足条件时才触发输出用于精准过滤。我第一次用的时候犯过一个新手错误只写了类名和方法名没写表达式结果啥也没输出。后来才反应过来watch的默认行为是有表达式才展示数据表达式就是你想看的那个东西。一个最基础的完整示例watch com.example.OrderService createOrder {params, returnObj} -x 2这条命令的意思很清楚观察OrderService.createOrder方法输出它的入参和返回值对象结构展开两层。命令执行后Arthas会进入监听状态下一次该方法被调用时控制台就会打出类似下面的信息ts2024-06-15 10:23:45; [cost12.3ms] resultArrayList[ Object[][ OrderDTO[ orderIdString[1000234], userIdLong[888888], ... ], ], Long[123456], ]ts是触发时间cost是方法耗时result里装的就是你在表达式里指定的内容。看到这里你基本就明白watch的逻辑了它不是主动去查而是挂在目标方法上等调用调用一发生就把你要的数据捞出来。2.2 ognl表达式watch的灵魂watch命令的表达能力全靠OGNL表达式支撑。OGNL这玩意儿有些年头了但懂的人不算多不过没关系watch里常用的就那几个固定的对象引用记住它们的含义就够了。params 所有入参是一个Object数组按声明顺序排列 params[0] 第一个入参 params[1] 第二个入参 returnObj 返回值方法执行成功后才有值 throwExp 异常对象方法抛出异常时才有值 target 当前调用方法的对象也就是this这些固定对象引用在任何方法上都是通用的不需要额外声明直接写在表达式里就行。组合起来可以玩出很多花样比如params[0].name是第一个参数的name字段returnObj.code是返回值的code字段target.status是当前对象的status成员变量。OGNL还支持方法调用和静态属性访问比如在表达式里直接调params[0].getUserId()、访问某个静态常量com.example.ConstantsMAX_RETRY这些都是合法操作。不过我的建议是表达式别写太复杂能用简单属性访问解决的就不要在命令行里搞一堆逻辑。为什么因为OGNL解析一旦出错报错信息很抽象你根本不知道是语法问题还是运行时问题排查起来非常痛苦。2.3 观察时机方法执行前还是执行后默认情况下watch是在方法执行完之后输出结果的这保证了你既能拿到入参也能拿到出参。但有些场景下你需要在方法执行之前看状态比如某个参数在方法内部被修改了你想知道进来的时候长什么样。这时候要用-b参数它表示在方法调用之前观察。比如watch com.example.OrderService createOrder params[0] -b加上-b之后方法和入参在方法体执行前就会被捞出来。这里有个需要特别留意的点用-b的时候returnObj和throwExp是拿不到的因为方法还没执行完。反过来如果你只关心返回值那就用默认时机不要加-b。另外还有个-f参数表示方法执行完成后观察这是默认行为但显式写出来有个好处它能跟-b之外的场景区分开。比如你同时想看在方法执行前后某个成员变量分别是什么可以分两条命令分别加-b和-f去观察两次输出拼起来就是完整的前后对比。我在实际排查并发问题时经常这么干。3. 实战看入参从暴力打印到精准截获3.1 基础用法打印所有参数先来一个最典型的场景。假设线上有个PaymentService.pay(Long userId, BigDecimal amount, String channel)方法有用户反馈支付金额不对你想看看实际进来的参数是什么。watch com.example.PaymentService pay {params[0], params[1], params[2]} -x 2执行后会得到这样一份输出ts2024-06-15 11:02:11; [cost3.2ms] resultArrayList[ Long[10001], BigDecimal[99.00], String[wechat], ]一目了然。三个参数分别是用户ID、金额、渠道。如果金额确实不对那问题大概率出在调用方而不是这个支付方法本身。如果你懒得一个个写params[0]、params[1]可以直接写params把整个参数数组打出来。不过要注意params是一个数组直接输出时Arthas会把数组本身作为一个对象展示要是你不加-x指定展开深度可能只能看到数组的长度看不到里面每个元素的具体值。所以实际使用中我更喜欢显式列出感兴趣的参数这样输出更干净也少刷屏。3.2 只看你关心的参数与字段大多数时候你并不需要看到入参对象的全部字段。比如OrderDTO有二十个字段但你现在只关心orderId和status表达式就可以写成watch com.example.OrderService submitOrder params[0].orderId | params[0].status这样输出就是一行简短的内容像这样ts2024-06-15 11:15:33; resultString[1000234|1]字符串拼接在OGNL里是支持的用加号连接即可。这种方式非常适合在流量大的接口上做快速观测输出量小不会把终端刷爆。还有一个我经常用的小技巧如果入参是Map可以直接用params[0].get(key)这种形式取某个key对应的值。很多接口的入参就是Map用这种方式比打印整个Map要清爽得多。要是Map的value本身是个对象还能继续往下点比如params[0].get(user).getName()只要对象的结构是确定的这条链就能一直点下去。3.3 按条件过滤只抓有问题的那一次生产环境的接口调用量很大你要是直接watch一个高频方法什么条件都不加终端会在几秒内被刷得面目全非而且真正有问题的那次调用很可能淹没在海量输出里。所以学会写条件表达式是watch进阶的第一课。还是拿支付方法举例。假设用户反馈的是某个特定用户支付金额不对你可以这样过滤watch com.example.PaymentService pay {params[0], params[1]} params[0] 10001第四个参数就是条件表达式只有条件为true时才会输出结果。这里的意思是只有第一个参数等于10001时才打印入参和入参金额。条件表达式里支持的写法很丰富比如参数是对象时可以直接访问属性做范围判断watch com.example.OrderService createOrder {params[0]} params[0].amount 10000这段的意思是只抓下单金额超过10000的调用。配合、||可以做更复杂的组合条件。我在排查特定渠道特定金额区间的故障时经常一条命令就精准锁定了现场输出干净利落。3.4 处理重载方法参数数量不对怎么办这是个很容易踩的坑。同一个类里有两个同名方法比如OrderService.query(String orderId)和OrderService.query(String orderId, int page, int size)你直接watchcom.example.OrderService query {params}两个方法都会被匹配到。如果它们入参结构差异很大表达式就直接报错了——因为对query(String, int, int)来说params[1]存在但对query(String)来说params[1]越界。解决办法有两个。第一个在方法名后面用参数类型做精确匹配Arthas支持这种写法watch com.example.OrderService query {params} -n 1 com.example.OrderService.query(java.lang.String)但这种写法的类名格式要求比较严格容易写错。第二个办法更实用既然方法名支持通配符那就在类名上做文章。如果两个重载方法不在同一个类中问题不大如果在同一个类中我一般会先用methods命令确认目标方法列表或者直接watch所有重载但表达式里只写大家共有的属性。实在不行就调整条件表达式比如watch com.example.OrderService query params.length 1 ? params[1] : only one param params.length 1用三元表达式兜底至少不会因为越界直接报错。不过说实话这种写法可读性很差我建议在遇到重载方法时优先使用精确方法匹配或者临时用-n 1限制只观察一次确认命中正确后再调整。4. 看出参与异常returnObj和throwExp的正确姿势4.1 拿到返回值入参看明白了接下来就要看方法到底返回了什么。这里有个概念要先理清returnObj不是Ognl里的关键字而是Arthas内置的变量引用它代表方法正常执行结束后的返回值在表达式里直接写returnObj就行。最简单的用法watch com.example.OrderService createOrder returnObj -x 2这条命令会监听createOrder方法只要正常返回就把返回值完整展开。比如返回的是ResultOrderVO这种通用包装对象你就能看到code、message、data各个字段的值。不过我更推荐把入参和出参合在一起看这样前后对照排查效率高得多watch com.example.OrderService createOrder {params[0], returnObj} -x 2输出长这样ts2024-06-15 11:30:22; [cost45.1ms] resultArrayList[ OrderDTO[...], Result[ codeInteger[200], messageString[success], dataOrderVO[...], ], ]从这份输出里你不仅能看到进来的是什么还能看到出去的是什么再结合中间的耗时这方法干了什么、效率如何心里基本有数了。4.2 捕获异常现场有一种情况比看返回值更让人头疼方法抛异常了但不是每次都抛而是偶发。日志里那句冷冰冰的异常信息根本不足以定位问题你真正想看的是抛出异常的那一刻入参到底是什么。watch的-e参数就是干这个的。它表示只观察方法抛出异常的情况watch com.example.OrderService createOrder {params[0], throwExp} -e -x 3当方法抛出异常时输出里会有两部分入参和异常对象。异常对象完整展开后你能看到它的message、cause甚至整个堆栈信息。这对于偶发异常排查特别有用因为异常一旦被上层吞掉或者只打了message底层真正的触发条件就被掩盖了而这个命令能帮你把元凶拉出来。这里有个细节要注意-e和默认模式是互斥的加了-e之后returnObj就永远为空了因为方法根本没正常返回。同样地配合-b使用的时候throwExp也是拿不到的。想做异常场景下的入参快照必须用-e让方法执行完在异常抛出点取数据。4.3 结合条件表达式做断言式排查条件表达式不仅可以过滤入参也能针对返回值做判断。比如你怀疑某个方法偶尔会返回错误码但不确定什么时候会发生那就可以把条件写在返回值上watch com.example.OrderService createOrder {params[0], returnObj} returnObj.code ! 200 -x 2这条命令的意思是只要返回值不是200就打印入参和返回值。挂上之后你就不用盯着终端了它会在条件满足的那一刻自动输出相当于给方法装了一个异常报警器。我在处理那种一天只出现几次、毫无规律的报错时这个方法帮了大忙有时候早上挂上下午回来看一眼终端问题现场清清楚楚。条件表达式里还支持访问抛出的异常类型比如throwExp instanceof java.lang.NullPointerException这样能只抓特定类型的异常。如果异常对象里有你关心的字段比如错误码也可以直接点出来。万变不离其宗记住一点条件表达式本质上是一个返回boolean的OGNL表达式你能在watch主表达式里写什么条件里基本也都能写。5. 最难的一关类成员变量的获取5.1 target对象与this的用法说实话很多人用watch看入参出参都挺顺手的但一到看类成员变量就卡住了。为什么因为官方文档里关于这方面讲得比较含糊只丢给你一个target的概念没解释清楚什么时候用、怎么用。这里我尽量一次讲透。先明确一个基本事实watch观察的是某个方法而方法一定是属于某个对象的。这个当前正在执行方法的对象在OGNL表达式里就是target。所以当你想看当前对象的成员变量时表达式写成target.字段名就行。举个例子。假设OrderService里有一个成员变量private ConfigCenter configCenter;而这个ConfigCenter有一个isSwitchOn()方法你想看每次调用createOrder时这个开关的状态watch com.example.OrderService createOrder target.configCenter.switchOn -n 3输出会是ts2024-06-15 14:20:11; [cost10.2ms] resultBoolean[true]这就有意思了你直接看到了方法执行时这个对象内部的真实状态而不是靠猜。很多问题恰恰出在成员变量的状态上比如某个计数器的值被并发线程搞乱了、某个缓存Map被意外清空了这些用日志根本查不到但用watch看成员变量一抓一个准。还有个小细节表达式里的target其实就是当前对象的this。有些OGNL表达式的写法里直接写this.字段名也是可以工作的但实际测试下来target在Arthas的watch表达式中更稳定官方示例也统一用target。所以你记住一点就行看成员变量用target。5.2 静态变量怎么取成员变量分两种实例成员变量和静态成员变量。target.字段名只能拿实例的静态变量根本不在实例里它属于类本身。这种情况下OGNL的静态访问语法就派上用场了。Arthas的OGNL是通过ognl框架执行的它支持用类全名静态字段名这种形式访问静态字段。举个例子你的OrderService里有个private static final AtomicInteger COUNTER new AtomicInteger(0);你想看当前的值watch com.example.OrderService createOrder com.example.OrderServiceCOUNTER.get()相似的如果要访问静态方法格式是类全名方法名(参数)。不过说实话静态变量这么看虽然可行但Arthas还有更直接的方式那就是getstatic命令。它专门用来查看某个类的静态属性不需要依赖方法调用这个引子getstatic com.example.OrderService COUNTER所以我的习惯是如果静态变量跟某个方法的执行上下文有关系就在watch表达式里顺手带上如果只是单纯想看某个静态字段当前值直接用getstatic更省事。5.3 深层嵌套对象的解析看成员变量时另一个高频问题对象层级太深字段点不出来。比如target.orderRepository.redisTemplate.connectionFactory这种链路写上去之后Arthas要么报错要么返回null很多人就懵了。这里有两个原因。第一链路中的某个中间对象可能为null不是表达式写错了是实际运行时的对象引用就是空的。第二OGNL有默认的访问限制一些非public的字段或者没有getter的字段它访问不了。Java的字段大多声明为privateOGNL能不能直接访问private字段取决于Arthas在这个版本里是否做了特殊处理。实测下来Arthas对private字段的访问支持得还不错但如果遇到访问不到的情况可以先尝试写出该字段对应类的getter方法比如target.getRepository().getConnection()这通常更稳定。还有一个判断技巧拿不到字段值时先用-x把当前对象整个展开看看对象树到底长什么样。比如watch com.example.OrderService createOrder target -x 4把这行输出琢磨一遍你就能看清成员变量的名字、类型、层级结构然后再决定下一步精确取哪个字段。有时候不是字段取不到而是你在命令行里凭记忆写的字段名跟实际代码里的拼写差了一个字母这种低级错误展开一眼就能发现。我这里再补一个实用的点如果你想看的成员变量是集合类型比如target.userCache是一个Map你可以直接用target.userCache.get(userId)这样的OGNL表达式去取某个key对应的值。OGNL对集合的索引、取值操作支持得非常好List可以用[0]、get(0)Map可以用[key]或者get(key)。这些能力让你在看类内部状态时不用先把整个Map打出来再人肉翻找。5.4 结合入参来读成员变量看成员变量很少是单独的行为更多时候是拿着入参的关键字段去对照成员变量的状态。比如某个方法会根据缓存里的配置来决定走哪条分支你就可以把入参和缓存字段一起拿出来watch com.example.OrderService createOrder {params[0].userId, target.cacheMap.get(params[0].userId)} -x 2这样输出里直接就是传进来的用户ID以及这个用户在缓存里的配置对象二者一对照方法走了什么逻辑、为什么结果出乎意料基本一眼就能看出来。这种内外结合的观察方式是我用watch用得最多的姿势比单独看入参或者单独看成员变量有效得多。6. 生产环境用watch的保命指南6.1 性能开销到底有多少Arthas能在生产环境用吗这个问题几乎每次分享都会有人问。答案是可以但不能无脑用。watch的实现机制是字节码增强。Arthas在attach到目标JVM后会对匹配到的目标方法进行字节码注入在方法入口和出口植入监听逻辑。这个注入过程本身是一次性开销对方法后续执行的性能影响理论上很小但绝不是零。如果你watch的是一个QPS上万的接口而且表达式里做了复杂的OGNL计算、还展开了多层对象结构那额外的开销就会被放大。我个人的经验值是对低频方法、问题排查期的方法只要加上-n限制观察次数完全不用担心性能对高频方法建议配合条件表达式做过滤让绝大多数调用直接跳过输出逻辑只有命中条件的才做完整处理。另外watch结束后记得用stop命令退出Arthas或者用watch命令的-n参数自动结束否则增强一直挂着虽然平时没事但总归多一分风险。6.2 必配参数-n、-x 与观察次数管理我见过太多新手在生产环境跑watch命令不带-n结果终端被刷了几万行最后只能CtrlC强行终止。虽然Arthas的监听是挂在JVM进程上的CtrlC退出客户端并不会移除字节码增强但至少停止了输出。规范做法是每次执行watch都明确指定观察次数watch com.example.OrderService createOrder {params[0], returnObj} -n 1 -x 2-n 1表示命中一次之后自动停止观察。问题一旦抓到命令自动退出干净利落。就算没抓到你也可以重新调整条件再执行一次总比让命令一直挂在那儿好。-x参数控制的是对象展开深度。这个值的设置是门学问设大了输出会非常冗长一个对象层层展开能打出几百行设小了又可能看不到你真正关心的嵌套字段。我的建议是先-x 1或-x 2快速看一眼结构确认对象层级后再决定要不要调大。别一上来就-x 5那种输出量你会后悔的。6.3 权限与安全别在生产环境裸奔Arthas的功能太强大了强大到它可以查看JVM里任何类的任何字段、甚至可以修改某些状态。这就意味着它同时也是一把双刃剑。在生产环境用Arthas至少要遵守几个原则只允许有权限的运维或开发人员使用尽量通过堡垒机操作操作过程留痕观察类名和方法名时尽量缩小匹配范围不要用过于宽泛的通配符避免误伤多个类不修改任何线上数据。watch只是看但Arthas本身具备动态修改能力不应该在生产环境滥用用完及时退出别让Arthas的agent长时间挂在生产JVM上。另外有些大厂的内部规范里Arthas只允许在预发或者灰度环境使用生产环境需要走审批。如果你所在的团队还没有相关规范我建议你主动提出一条生产环境使用Arthas必须登记并且在操作前确认目标机器和进程无误避免把别人的应用给attach了。6.4 与trace、tt命令的配合使用最后聊一个组合拳的打法。watch单独用已经很强了但如果你遇到的是一个方法调用链路很长、问题藏在深水区的场景你会发现光watch一个方法不够你根本不知道应该watch链路上的哪一环。这时候先用trace命令看一下整个调用链的耗时分布trace com.example.OrderService createOrdertrace会把方法内部各子调用的耗时打出来这样你能快速定位最耗时或者最可疑的那一段。锁定目标方法之后再用watch去细看那个方法的入参、出参和类成员变量。一粗一细配合起来效率极高。还有个更狠的招是tt命令。它能把方法调用的现场记录到内存里调用结束后你可以随时查看当时的入参、出参、对象状态甚至可以时空隧道式地回放。遇到过那种刚才明明发生了一次异常调用但你没来得及watch的情况吗tt就是为这个场景设计的。不过tt的存储是有限制的默认最多存100条而且占用一定的堆内存生产环境不要长期开着大容量tt。我的日常排查套路基本是这样的线上告警来了先trace定位耗时异常或失败的入口方法再用watch细看这个方法的入参、出参和关键成员变量确认是不是数据问题如果这还不够就用tt记录下若干次现场事后慢慢分析。这三板斧用熟了大部分线上疑难杂症都能迎刃而解而且全程不需要发版、不需要重启、不需要加日志。工具本身没有花哨的地方但用好它确实能让你的线上排查效率提升一个量级。
返回列表