使用arthas及插件故障排查实践
Arthas 简介
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
当你遇到以下类似问题而束手无策时,Arthas
可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到 JVM 的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从 JVM 内查找某个类的实例?
常见的故障问题排查案例
启动arthas
一般线上已经部署了arthas,可以直接用该命令启动
1 | curl -O https://arthas.aliyun.com/arthas-boot.jar |
选择进入哪个process,此时控制台会出现下面几个选项,它通过不同序号标明不同的Java程序,我们看到我们的目标程序app.jar
,序号为1,所以我们输入1按回车。
1 | [root@jr-uhr-org-7fdd8589db-c9nj7 /]# java -jar arthas-bin/arthas-boot.jar |
定位CPU 100%问题
使用thread命令,可以看到所有的线程,查看他们的CPU占比。
找到CPU占比异常的线程,输入以下命令可以查看其堆栈信息,找到异常的代码段。
1 | thread [pid] |
如果想使用arthas反编译查看源码,可以先在idea下载一个arthas idea plugin
在idea中找到出现异常的类,右键 Arthas Command -> Decompile Class Jad,可以直接生成shell命令
反编译类的指令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 类的包路径 |
想查看TestController
的字段详情,就可以键入这条命令
1 | sc -d -f com.example.arthasExample.TestController |
可以看到这条指令不仅可以查看字段的定义和注解,还可以查看线程池的使用情况以及集合内部的value
:
查看静态变量
按照官网的说明,对于静态变量的监控更推荐使用 ognl 表达式
可以先使用sc命令查看hash值
得到classLoader的hash值以后,再把其粘贴到输入框中,点击copy command,这样ognl表达式就生成了
运行耗时
使用trace命令可以直观地观察到这个接口运行最耗时的方法
1 | trace com.jr.uhr.org.controller.SysPostController getByPostTreeList -n 7 --skipJDKMethod false |
方法执行情况监控
我们希望查询单位时间内这个方法的执行成功、失败、以及平均耗时等情况,也就是monitor
监控指令,也可以直接通过插件快速生成:
默认情况下,生成的指令为会执行10次,每10s进行一次输出:
1 | monitor com.jr.uhr.org.controller.SysPostController getByPostTreeList -n 10 --cycle 10 |
监控方法出入参
生成指令的步骤还是一样的,对准方法然后右键通过插件快捷生成指令:
指令的会打印出入参和异常,然后执行次数为7,输出的结果属性的遍历深度为4,对应的指令和执行结果如下:
1 | watch com.jr.uhr.org.controller.SysPostController getByPostTreeList '{params,returnObj,throwExp}' -n 7 -x 4 |
监控方法调用路径
使用stack指令:
会给出我们选中的类的全路径和方法名,默认执行7次,这样我们就可以快速的得到当前调用的堆栈信息
1 | stack com.jr.uhr.org.service.SysPostService getByPostTreeList -n 7 |
获取方法调用过程
在开发测试过程中,如果遇到需要上游调用的情况,可能上游触发了一次后,没能观察到具体的问题,但又不好意思让上游配合多触发几次,这时候就得使用 tt 指令了。
然后点击第一行复制这个指令,可以看到这个指令会指明我们要监控的类和方法,默认情况下执行7次:
查看监控结果可以看到,第二次执行报错:
查看索引为1003,所以我们键入tt -i 1003
,此时我们就可以直观的看到入参和错误详情:
同时这个插件的第二行列举了许多tt的指令,鼠标悬浮在上面可以看到相应的中文释义
需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。
注意:tt 相关功能在使用完之后,需要手动释放内存,否则长时间可能导致OOM
。退出 arthas
不会自动清除 tt 的缓存 map
。
1 | tt --delete-all |
内存溢出问题
可以使用dashboard查看随着时间推移的内存变化情况
此时我们就可以借助插件快速生成heapdump
指令:
从指令上看,默认情况下,内存快照会被dump到tmp目录下:
1 | heapdump /tmp/dump.hprof |
同时也可以导出到本地:
1 | heapdump D://heap.hprof |
然后我们就可以通过MAT定位到出现问题的线程,然后根据名称取搜索问题线程和有相关threadLocal操作的变量即可。
热更新代码
我们希望将线上环境这段代码条件由小于2改为小于等于0,对此我们可以通过arthas
的jad反编译、mc编译以及redefine完成热点代码更新:
1 | /** |
首先我们通过插件获取反编译指令
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 | /** |
我们需要通过arthas
得到当前类的hashCode
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指令:
1 | tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter * -n 7 |
这个arthas插件甚至可以在设置里配置static spring context
然后等待用户请求后,定位到这个类,如下图对应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资源就越多~。
第一行可以选择监测的对象,这里选的是监测CPU资源,还能选择用户态/内核态,点击不同线程分开统计
开始监测命令
1 | profiler start --event cpu --interval 10000000 |
结束监测命令
1 | profiler start --event cpu --interval 10000000 --format html --duration 30 |
结果如下图,此图俗称火焰图,主要看每个方法的横轴长度,占用横坐标越长的操作,其占用的 cpu 即最长,很直观的。
分析x轴越长的方法,观察是否有优化空间,优化完以后再生成一次火焰图查看优化的结果。
如果生成的火焰图不太理想,当async-profiler全量采样导出的svg文件太大时,想要找到关键的调用点,就非常困难。
也可以换一种生成方式,生成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