Arthas 简介

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到 JVM 的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从 JVM 内查找某个类的实例?

常见的故障问题排查案例

启动arthas

一般线上已经部署了arthas,可以直接用该命令启动

1
2
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-bin/arthas-boot.jar

选择进入哪个process,此时控制台会出现下面几个选项,它通过不同序号标明不同的Java程序,我们看到我们的目标程序app.jar,序号为1,所以我们输入1按回车。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@jr-uhr-org-7fdd8589db-c9nj7 /]# java -jar arthas-bin/arthas-boot.jar
[INFO] arthas-boot version: 3.5.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 /app.jar
1
[INFO] arthas home: /arthas-bin
[INFO] Try to attach process 1
[INFO] Attach process 1 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'

wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.5
main_class
pid 1
time 2025-01-15 17:34:22

定位CPU 100%问题

使用thread命令,可以看到所有的线程,查看他们的CPU占比。

image-20250115174050351

找到CPU占比异常的线程,输入以下命令可以查看其堆栈信息,找到异常的代码段。

1
thread [pid]

image-20250115174159409

如果想使用arthas反编译查看源码,可以先在idea下载一个arthas idea plugin

在idea中找到出现异常的类,右键 Arthas Command -> Decompile Class Jad,可以直接生成shell命令

image-20250115175113962

反编译类的指令jad --source-only 类的包路径

定位方法代码段的命令jad --source-only 类的包路径 方法名

1
jad --source-only com.jr.uhr.org.service.impl.ContractRuleDetailServiceImpl 

定位线程死锁问题

1
thread -b

可以直接查看产生死锁的线程

图片

由上述结果我们可知两个线程的id分别是65和66,所以使用thread pid号的命令直接定位到问题代码段并解决问题即可。

图片

定位字段详情

我们希望看到某个类下所有字段的详情,我们就可以使用这条命令

1
sc -d -f 类的包路径

image-20250115175942543

想查看TestController的字段详情,就可以键入这条命令

1
sc -d -f com.example.arthasExample.TestController

可以看到这条指令不仅可以查看字段的定义和注解,还可以查看线程池的使用情况以及集合内部的value

图片

查看静态变量

按照官网的说明,对于静态变量的监控更推荐使用 ognl 表达式

image-20250115180525605

可以先使用sc命令查看hash值

image-20250115180630475

图片

得到classLoader的hash值以后,再把其粘贴到输入框中,点击copy command,这样ognl表达式就生成了

image-20250115180732331

运行耗时

使用trace命令可以直观地观察到这个接口运行最耗时的方法

image-20250115181213513

1
trace com.jr.uhr.org.controller.SysPostController getByPostTreeList  -n 7 --skipJDKMethod false 

方法执行情况监控

我们希望查询单位时间内这个方法的执行成功、失败、以及平均耗时等情况,也就是monitor监控指令,也可以直接通过插件快速生成:

image-20250115181428147

默认情况下,生成的指令为会执行10次,每10s进行一次输出:

1
monitor com.jr.uhr.org.controller.SysPostController getByPostTreeList  -n 10  --cycle 10 

监控方法出入参

生成指令的步骤还是一样的,对准方法然后右键通过插件快捷生成指令:

image-20250115181529055

指令的会打印出入参和异常,然后执行次数为7,输出的结果属性的遍历深度为4,对应的指令和执行结果如下:

1
watch com.jr.uhr.org.controller.SysPostController getByPostTreeList '{params,returnObj,throwExp}'  -n 7  -x 4 

监控方法调用路径

使用stack指令:

image-20250115181650035

会给出我们选中的类的全路径和方法名,默认执行7次,这样我们就可以快速的得到当前调用的堆栈信息

1
stack com.jr.uhr.org.service.SysPostService getByPostTreeList  -n 7 

获取方法调用过程

在开发测试过程中,如果遇到需要上游调用的情况,可能上游触发了一次后,没能观察到具体的问题,但又不好意思让上游配合多触发几次,这时候就得使用 tt 指令了。

image-20250115182507946

然后点击第一行复制这个指令,可以看到这个指令会指明我们要监控的类和方法,默认情况下执行7次:

image-20250115182539545

查看监控结果可以看到,第二次执行报错:

图片

查看索引为1003,所以我们键入tt -i 1003,此时我们就可以直观的看到入参和错误详情:

图片

同时这个插件的第二行列举了许多tt的指令,鼠标悬浮在上面可以看到相应的中文释义

image-20250115182752577

需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。

注意:tt 相关功能在使用完之后,需要手动释放内存,否则长时间可能导致OOM。退出 arthas 不会自动清除 tt 的缓存 map

1
tt --delete-all

内存溢出问题

可以使用dashboard查看随着时间推移的内存变化情况

image-20250115183138681

此时我们就可以借助插件快速生成heapdump指令:

image-20250115183209418

从指令上看,默认情况下,内存快照会被dump到tmp目录下:

1
heapdump /tmp/dump.hprof

同时也可以导出到本地:

1
heapdump D://heap.hprof

然后我们就可以通过MAT定位到出现问题的线程,然后根据名称取搜索问题线程和有相关threadLocal操作的变量即可。

热更新代码

我们希望将线上环境这段代码条件由小于2改为小于等于0,对此我们可以通过arthas的jad反编译、mc编译以及redefine完成热点代码更新:

1
2
3
4
5
6
7
8
9
   /**
* 通过postCode查询岗位信息列表
* @param postCode
* @return
*/
@GetMapping("/getByPostCode")
public R getByPostCode(String postCode, String startDate) {
return sysPostService.getByPostCode(postCode, startDate);
}

首先我们通过插件获取反编译指令

image-20250116094026761

1
jad --source-only com.jr.uhr.org.controller.SysPostController getByPostCode

生成指令后,我们手动设置导出路径/tmp/SysPostController.java

1
jad --source-only com.jr.uhr.org.controller.SysPostController >> /tmp/SysPostController.java

然后通过vim /tmp/SysPostController.java修改代码:

1
2
3
4
5
6
7
8
9
10
 /**
* 通过postCode查询岗位信息列表
* @param postCode
* @return
*/
@Log(title = "通过postCode查询岗位信息列表", businessType = BusinessType.SELECT)
@GetMapping("/getByPostCode")
public R getByPostCode(String postCode, String startDate) {
return sysPostService.getByPostCode(postCode, startDate);
}

我们需要通过arthas得到当前类的hashCode

image-20250116094511378

1
sc -d com.jr.uhr.org.controller.SysPostController

基于上述的hashCode,我们通过mc指令将tmp下的Java文件编译为字节码文件到/tmp目录下:

1
mc -c 68de145  /tmp/SysPostController.java -d /tmp

最后我们通过redifine将这个class文件到JVM里:

1
redefine /tmp/com/example/arthasExample/SysPostController.class

如果看到success就说明我们的代码替换成功了,此时请求接口就有存储日志了。

获取spring上下文的操作

spring中有个上下文叫applicationContext,它记录着所有的bean的信息,通过该容器我们可以得到所有的bean进行各种操作。这意味着如果我们在spring项目中如果有什么特殊的执行操作,完全可以通过tt定位到包含上下文的类,然后获取其applicationContext完成特殊操作。

读过Spring MVC源码的读者可能都知道,每当又HTTP请求发送到web容器时请求进行映射转发处理时都会经过RequestMappingHandlerAdapter,所以我们通过插件生成RequestMappingHandlerAdapter的tt指令:

image-20250116094814906

1
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter * -n 7 

这个arthas插件甚至可以在设置里配置static spring context

image-20250116095805803

然后等待用户请求后,定位到这个类,如下图对应1000索引就是笔者的请求:

图片

基于这个索引对应的target,其实就是我们的RequestMappingHandlerAdapter,我们通过-w 像写java代码一样获取到applicationContext从而得到testController最终完成调用:

1
tt -i 1000 -w 'target.getApplicationContext().getBean("testController").findUserById(3)'

使用火焰图实现性能优化

Async-profiler 可以观测运行程序,每一段代码所占用的cpu的时间和比例,从而可以分析并找到项目中占用cpu时间最长的代码片段,优化热点代码,达到优化内存的效果。

火焰图里,X轴越长,代表使用的越多,Y轴是调用堆栈信息。当前收集的是什么类型的数据,比如cpu 那么x轴长度越大,占用的cpu资源就越多~。

image-20250116103405101

第一行可以选择监测的对象,这里选的是监测CPU资源,还能选择用户态/内核态,点击不同线程分开统计

image-20250116103339400

开始监测命令

1
profiler start --event cpu --interval 10000000

结束监测命令

1
profiler start --event cpu --interval 10000000 --format html --duration 30 

结果如下图,此图俗称火焰图,主要看每个方法的横轴长度,占用横坐标越长的操作,其占用的 cpu 即最长,很直观的。

image.png

分析x轴越长的方法,观察是否有优化空间,优化完以后再生成一次火焰图查看优化的结果。

如果生成的火焰图不太理想,当async-profiler全量采样导出的svg文件太大时,想要找到关键的调用点,就非常困难。

1

也可以换一种生成方式,生成jfr文件,使用jprofiler打开jfr文件,选择Open a snapshot, 打开之后选择CPU views

1
profiler start --event cpu --interval 10000000 --threads --format jfr --duration 30 --threads

View -> Find 查找要分析的类和方法,然后选择 Analyze -> Calculate Backtraces to Selected Method

修改Summation mode 为Total times,即可看到这个方法被哪些上游调用到,调用量和占比

参考链接

arthas快速入门:https://arthas.aliyun.com/doc/

arthas插件的使用文档链接:https://www.yuque.com/arthas-idea-plugin/help/pe6i45

用户实操案例分享:https://github.com/alibaba/arthas/issues?q=label%3Auser-case&page=1

image-20250116100449644

image-20250116100506901