1 介绍
arthas是java的诊断监控工具,常用于非本地环境的debug,官网的介绍如下。
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
对于arthas的基础用法,本文不做过多的介绍,可以参考官方文档的快速入门章节,在入门章节中主要介绍了以下几个指令的基本用法:
dashboard查看jvm概览thread查看jvm线程jad将jvm已经加载的类反编译成java源码watch监控方法的出入参quitstop退出
接下来我们以一个springboot的项目为例,来展示一些arthas的高阶技巧,创建一个springboot的项目,示例代码如下:
@RestController
@RequestMapping("/")
@SpringBootApplication
public class ArthasdemoApplication {
public static void main(String[] args) {
SpringApplication.run(ArthasdemoApplication.class, args);
}
@RequestMapping("/demo1")
public String demo1(int input) {
return "/demo1: " + (100 / input);
}
}
通过curl可以有如下结果:
$ curl localhost:8080/demo1?input=10
/demo1: 10
2 高级技巧之watch
watch可以监控方法的入参、返回值以及抛出的异常,虽然watch不算是特别高级的使用技巧,但是因为用的太频繁,也给他列在这里了。
watch最基础的用法就是watch 全限定类名 方法名,当该方法被调用的时候就会打印{params, target, returnObj},params代表入参数组,target代表的是当前对象this,而returnObj代表返回值。
$ watch com.example.arthas.demo.ArthasdemoApplication demo1
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 67 ms, listenerId: 1
method=com.example.arthas.demo.ArthasdemoApplication.demo1 location=AtExit
ts=2024-02-07 13:04:26; [cost=3.60725ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@ArthasdemoApplication$$EnhancerBySpringCGLIB$$bc075746[com.example.arthas.demo.ArthasdemoApplication$$EnhancerBySpringCGLIB$$bc075746@3c1d2476],
@String[/demo1: 10],
]
如果想下钻结果内部的属性值,可以通过-x,指定下钻深度,如果只想打印入参和返回值,可以最后专门指定watch的内容,如下:
$ watch com.example.arthas.demo.ArthasdemoApplication demo1 -x 2 "{params, returnObj}"
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 58 ms, listenerId: 2
method=com.example.arthas.demo.ArthasdemoApplication.demo1 location=AtExit
ts=2024-02-07 13:08:54; [cost=0.071708ms] result=@ArrayList[
@Object[][
@Integer[10],
],
@String[/demo1: 10],
]
如果方法抛出异常,returnObj为null,想要打印异常,需要添加throwExp如下(用input=0触发异常)
$ watch com.example.arthas.demo.ArthasdemoApplication demo1 -x 2 "{params, returnObj, throwExp}"
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 38 ms, listenerId: 4
method=com.example.arthas.demo.ArthasdemoApplication.demo1 location=AtExceptionExit
ts=2024-02-07 13:12:19; [cost=0.287375ms] result=@ArrayList[
@Object[][
@Integer[0],
],
null,
java.lang.ArithmeticException: / by zero
at com.example.arthas.demo.ArthasdemoApplication.demo1(ArthasdemoApplication.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
]
最后的表达式是ognl语法,注入了特定的变量名params,returnObj,throwExp,target等,可以直接使用。他们都是java的对象,大括号是ognl中创建List的语法,也可以使用ognl的其他语法,比如#用来声明变量,逗号隔开多句,最后一句是ognl返回值。例如只想知道第一个参数和返回字符串的长度,可以这样写。
$ watch com.example.arthas.demo.ArthasdemoApplication demo1 -x 2 "#arg0=params[0],#len=returnObj.length,{#arg0,#len}"
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 63 ms, listenerId: 6
method=com.example.arthas.demo.ArthasdemoApplication.demo1 location=AtExit
ts=2024-02-07 13:18:13; [cost=0.063875ms] result=@ArrayList[
@Integer[1],
@Integer[11],
]
其他需要注意的信息:
- 第一行的
Affect(class count: 2 , method count: 1)表示匹配了两个类一个方法,可以用通配符例如demo*就表示所有以demo开头的方法都会监听。 - 第一行的
location=AtExit表示方法正常调用并退出,location=AtExceptionExit则表示异常退出。 - 打印
target的时候结果是ArthasdemoApplication$$EnhancerBySpringCGLIB$$bc075746类型,因为被spring进行了增强。
在watch的同时我们还可以对已有对象调用方法,例如我们在demo1方法下添加demo2代码如下
@Setter
String name;
@RequestMapping("/demo2")
public String demo2() {
return "/demo2: " + name;
}
因为name没有被赋值过,所以访问demo2得到结果都是null
$ curl http://localhost:8080/demo2
/demo2: null
但是我们用watch可以捕捉target对name调用setName方法进行赋值:
$ watch com.example.arthas.demo.ArthasdemoApplication demo2 'target.setName("Frank")'
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 37 ms, listenerId: 2
method=com.example.arthas.demo.ArthasdemoApplication.demo2 location=AtExit
ts=2024-02-07 13:43:27; [cost=0.087958ms] result=null
这样我们第一次访问demo2就会在访问之后,将name赋值,第二次访问的时候,就拿到Frank
$ curl http://localhost:8080/demo2
/demo2: null
$ curl http://localhost:8080/demo2
/demo2: Frank
3 高级技巧之理解增强Bean与原始Bean(target)
上面的watch中,我们学到很多骚操作,但是需要提个醒的是,如果你对spring bean的加载原理和流程不理解,很容易就会出错,所以这一节的知识,一定要补一下。
我们直接看现象,新增demo3方法,并在App类上添加@EnableCaching
@RestController
@RequestMapping("/")
@EnableCaching
@SpringBootApplication
public class ArthasdemoApplication {
....前面的demo1 demo2代码省略
@Autowired
DemoService demoService;
@RequestMapping("/demo3")
public String demo3() {
return demoService.getName();
}
}
新增DemoService类
@Service
public class DemoService {
@Getter
@Setter
private String name = "demo";
@Cacheable
public double random() {
return Math.random();
}
}
下面是问答时间请问:
- 1 curl localhost:8080/demo3 的返回值是什么?
- 2
watch com.example.arthas.demo.DemoService getName {returnObj}的结果呢? - 3
watch com.example.arthas.demo.DemoService getName {target.name}呢?
前两题的结果都是demo,这很简单。但是第三题的结果如下:
$ watch com.example.arthas.demo.DemoService getName '{target.name}'
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 46 ms, listenerId: 5
watch failed, condition is: null, express is: {target.name}, null, visit /Users/bytedance/logs/arthas/arthas.log for more details.
直接watch失败了,本质原因是target中没有name这个field,为什么没有呢?因为target是spring AOP增强的类,在显式AOP增强、@Transactional、@Cacheable等情况下,bean会被增强,而增强发生在下图bean初始化的(右侧)Post-Initailization过程,增强的方式是使用代理(默认能用jdk用jdk代理,否则用cgLib代理),注意代理类,只会将原bean(即代理中的target)的public方法进行增强,不会对代理对象的属性再进行依赖注入了。
下面的内容不仅对watch、还对后续重点介绍的ognl vmtool等指令都有用。
这里DemoService有一个原始的bean,有着name=demo,而最后进行了增强,将random和getName这些方法实现了增强,但是代理类中是没有name这个field的。
首先我们得知道,这个bean是不是增强bean,增强的bean在调用的时候,watch会有两次切入,一次是代理切入,一次是beanTarget切入。
$ watch com.example.arthas.demo.DemoService getName '{target.getClass()}'
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 64 ms, listenerId: 7
method=com.example.arthas.demo.DemoService.getName location=AtExit
ts=2024-02-07 14:26:56; [cost=0.080583ms] result=@ArrayList[
@Class[class com.example.arthas.demo.DemoService],
]
method=com.example.arthas.demo.DemoService$$EnhancerBySpringCGLIB$$ccd0c819.getName location=AtExit
ts=2024-02-07 14:26:56; [cost=2.776042ms] result=@ArrayList[
@Class[class com.example.arthas.demo.DemoService$$EnhancerBySpringCGLIB$$ccd0c819],
]
也可以通过getClass看名字是不是有$符号,或者直接用spring提供的AopUtils.isAopProxy(obj)判断。
上面两次切入只有beanTarget这次是可以看到name属性的,我们可以这样写↓,利用watch的表达式后面还可以接一个过滤表达式,过滤出beanTarget,即可看到name了。
$ watch com.example.arthas.demo.DemoService getName 'target' '!@org.springframework.aop.support.AopUtils@isAopProxy(target)'
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 76 ms, listenerId: 18
method=com.example.arthas.demo.DemoService.getName location=AtExit
ts=2024-02-07 14:39:32; [cost=0.079416ms] result=@DemoService[
name=@String[demo],
]
4 高级技巧之trace
其实trace也不能算一个高级技巧,只不过他也是实际分析问题中非常常用的指令,所以放到这里。
trace用法简单,直接接包名 方法名即可,可以查看该方法运行时,调用的每个子方法的耗时,分析性能瓶颈。
$ trace com.example.arthas.demo.ArthasdemoApplication demo3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 83 ms, listenerId: 19
`---ts=2024-02-07 14:45:44;thread_name=http-nio-8080-exec-6;id=24;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@2ef8a8c3
`---[0.445375ms] com.example.arthas.demo.ArthasdemoApplication:demo3()
`---[42.21% 0.188ms ] com.example.arthas.demo.DemoService:getName() #45
5 高级技巧之ongl
ognl在前面watch中提到过,他是一种简化的代码表达式,具体语法可以参考官网。除了在watch中执行一些回调,可以直接用arthas内置的ognl指令,主动触发一段代码,例如随机生成UUID,注意静态方法用@,普通方法用. 新建对象用new。
$ ognl '@java.util.UUID@randomUUID().toString()'
@String[fba6eaa1-d3a9-4cbf-8b81-50cc348aa7ae]
但是当我们使用一些spring项目中的类时,可能报错
# 在IDEA中启动的时候没问题,可以使用项目中的代码或依赖的代码
$ ognl '@org.springframework.aop.support.AopUtils@isAopProxy(new Object())'
@Boolean[false]
# 但用springboot打包后的jar包时,就找不到这个类
$ ognl '@org.springframework.aop.support.AopUtils@isAopProxy(new Object())'
Failed to execute ognl, exception message: ognl.MethodFailedException: Method "isAopProxy" failed for object org.springframework.aop.support.AopUtils [java.lang.ClassNotFoundException: Unable to resolve class: org.springframework.aop.support.AopUtils], please check $HOME/logs/arthas/arthas.log for more details.
这是因为springBoot打的jar包有着这样的结构(如下图),所有的源码编译的class文件放到了BOOT-INF/classes,而所有的依赖放到了BOOT-INF/lib,当然默认的类加载器不会加载这个目录,是springBoot的LaunchedURLClassLoader专门负责加载了整个项目,而IDEA中启动,使用的是AppClassLoader,上面ognl运行的上下文是在attach的线程中,也是使用的AppClassLoader,所以在IDEA下可以找到该类,在jar模式时就找不到了。(jvm中用 全限定类名+ClassLoader 来唯一标识一个类的)
好在ognl也提供了参数指定类加载器,我们先用classLoader指令看一下当前jvm有哪些类加载器:
$ classloader
name numberOfInstances loadedCountTotal
org.springframework.boot.loader.LaunchedURLClassLoader 1 4083
BootstrapClassLoader 1 3389
com.taobao.arthas.agent.ArthasClassloader 1 1457
sun.reflect.DelegatingClassLoader 69 69
sun.misc.Launcher$ExtClassLoader 1 63
sun.misc.Launcher$AppClassLoader 1 58
然后指定LaunchedURLClassLoader来运行ognl
$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@org.springframework.aop.support.AopUtils@isAopProxy(new Object())'
@Boolean[false]
6 高级技巧之vmtool
vmtool基于jvmti,可以实现多种功能,这里我们只介绍getInstances,获取某个类的实例列表。基于该功能我们可以获取到spring的bean,直接触发bean中的某个方法。
例如上面用到的DemoService,通过vmtool能拿到两个实例(增强前后),可以在表达式中主动调用他的方法。
$ vmtool --action getInstances --className com.example.arthas.demo.DemoService
@DemoService[][
@DemoService$$EnhancerBySpringCGLIB$$366dc6ae[com.example.arthas.demo.DemoService@7a3b5017],
@DemoService[com.example.arthas.demo.DemoService@7a3b5017],
]
使用--express指定运行的ognl表达式
$ vmtool --action getInstances --className com.example.arthas.demo.DemoService --express 'instances[0].setName("demo")'
null
$ vmtool --action getInstances --className com.example.arthas.demo.DemoService --express 'instances[0].name'
@String[demo]
这个用法非常适合,想要测试接口中部分函数的功能,而如果走整个接口的流程,需要的前置条件非常复杂的情况。用vmtool可以直接找到特定的serviceBean,直接触发其方法即可观测结果,不需要走整个接口流程。
7 高级技巧之热替换代码
boe联调,发现一行代码写错了,修改需要发布scm,tce部署,流程较长,而直接热替换整个类的代码就可以提高效率。可以参考这篇文章的用法,Arthas实践--jad/mc/redefine线上热更新一条龙。个人实际用下来,还是有一些问题的,总是报错redefine失败,原因是尝试添加或删除类的方法,而实际上没有做任何方法的增删,只是改动了已有方法的方法体。后来一直没有用了,都在用自己写的一个工具。
8 小结
arthas有很多使用姿势,这里列出了一部分使用技巧和使用场景,还有很多有用的指令,没有展开介绍。希望文章对大家有帮助。