目录
- arthas是什么
- 为什么要使用arthas
- arthas能做什么
- 安装arthas
- 前提准备
- arthas主要命令
- trace命令
- watch命令
- monitor命令
- jad命令
- dashboard命令
- Thread命令
- sc命令
- mc命令
- redefine命令
- 实战演练
- 1.定位到需要修改的类
- 2.将定位到的.class文件反编译成.java文件
- 3.修改.java文件
- 4.将修改后的.java文件重新编译成.class文件
- 5.将新的.class文件重新加载到JVM中
- 6.验证
arthas是什么
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
为什么要使用arthas
通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。
开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。
如果您正在考虑在代码中添加一些日志以帮助解决问题,您将必须经历以下阶段:测试、预发,然后生产。这种方法效率低下,更糟糕的是,该问题可能无法解决,因为一旦 JVM 重新启动,它可能无法复现,如上文所述。
Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。
arthas能做什么
当你遇到以下类似问题而束手无策时,Arthas
可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到 JVM 的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从 JVM 内查找某个类的实例?
注意:Arthas
支持 JDK 6+
,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab
自动补全功能,进一步方便进行问题的定位和诊断。
本文主要讲解arthas
的trace
,jad
,sc
,mc
,monitor
,watch
几个命令的使用。更多详细请查看arthas官方文档
接下来开始讲解各个命令的使用方法。
安装arthas
wget https://arthas.aliyun.com/arthas-boot.jar
使用以上命令直接下载arthas-boot.jar文件。
注意: 如果当前服务器没有信息jar文件,那么此时启动arthas会提示没有找到可执行的程序,此时只需要先启动一个可以调试的jar文件就行了。
前提准备
在讲解各个命令之前,我们需要准备一个jar包并且启动。本案例也是写了一个简单的demo并附带几个接口用来测试。
controller:
service:
然后打包为jar,上传到服务器并启动:
然后到arthas根目录,启动jarthas-boot.jar
一切准备就绪,接下来开始测试。
arthas主要命令
trace命令
作用:
方法内部调用路径,并输出方法路径上的每个节点上耗时
trace
命令能主动搜索 class-pattern/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[n:] | 命令执行次数 |
#cost | 方法执行耗时 |
[m ] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch ]。 |
这里重点要说明的是条件表达式
,条件表达式
的构成主要由ognl
表达式组成,所以你可以这样写"params[0]<0"
,只要是一个合法的ognl
表达式,都能被正常支持。
案例一:
trace com.laz.test.controller.TestController getByName
执行以上命令后,输出内容如下:
用此命令,我们可以来检查复杂业务中,方法内各个接口的调用耗时情况,根据耗时来进行优化。
使用ctrl+C可以退出trace命令。
案例二:
trace com.laz.test.controller.TestController getByName -n 1
此命令与案例一的区别
是:此命令可通过-n
后面跟的数字来确定执行几次后退出,而案例一需要手动通过ctrl+C退出。
案例三:
trace com.laz.test.controller.TestController getByName '#cost > 1'
此命令可以据调用耗时过滤出1S的调用路径,这个时间可以根据需要自行设置。
trace命令就介绍到这,如果想了解条件表达式
的写法,可以自行去官方文档查看!
watch命令
作用
函数执行数据观测
参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 函数名表达式匹配 |
express | 观察表达式,默认值:{params, target, returnObj} |
condition-express | 条件表达式 |
[b] | 在函数调用之前观察 |
[e] | 在函数异常之后观察 |
[s] | 在函数返回之后观察 |
[f] | 在函数结束之后(正常返回和异常返回)观察 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[x:] | 指定输出结果的属性遍历深度,默认为 1,最大值是 4 |
[m ] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch ]。 |
这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl
表达式组成,所以你可以这样写"{params,returnObj}"
,只要是一个合法的 ognl
表达式,都能被正常支持。
特别说明
watch
命令定义了4个观察事件点,即-b
方法调用前,-e
方法异常后,-s
方法返回后,-f
方法结束后- 4个观察事件点
-b
、-e
、-s
默认关闭,-f
默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出 - 这里要注意方法入参和方法出参的区别,有可能在中间被修改导致前后不一致,除了
-b
事件点 params 代表方法入参外,其余事件都代表方法出参 - 当使用
-b
时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
通过watch
命令可以查看函数的参数/返回值/异常信息。
案例一:
watch com.laz.test.controller.TestController getByName returnObj
此命令可以查看方法执行的返回值
注意: 观察表达式,默认值是params, target, returnObj
,这里只是列举了returnObj
案例二:
watch com.laz.test.controller.TestController getByName "{params,returnObj}" -x 2
此命令可以观察demo.TestController 类中getByName方法出参和返回值,结果属性遍历深度为2。
案例三:
watch com.laz.test.controller.TestController getByName "{params,returnObj}" -x 2 -b
此命令可以查看执行前参数
注意: 对比前一个例子,返回值为空(事件点为函数执行前(-b
),因此获取不到返回值)
案例四:
watch com.laz.test.controller.TestController getByName "{target}" -x 2 -b
此命令可以查看方法中的属性
当然,也可以通过查看target.userService查看某一属性的值,如:watch com.laz.test.controller.TestController getByName "{target.userService}" -x 2 -b
,想要了解详情,请自行查看官方文档。
monitor命令
作用
方法执行监控。 用来监视一个时间段中指定方法的执行次数,成功次数,失败次数,耗时等这些信息
对匹配 class-pattern
/method-pattern
/condition-express
的类、方法的调用进行监控。
monitor
命令是一个非实时返回命令.
实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标 Java 进程返回信息,直到用户输入 Ctrl+C 为止。
服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而不会被执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何Arthas
命令不会引起原有业务逻辑的改变。
监控的维度说明
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java 类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均 RT |
fail-rate | 失败率 |
参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[c:] | 统计周期,默认值为 120 秒 |
[b] | 在方法调用之前计算 condition-express |
[m ] | 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch ]。 |
案例:
monitor com.laz.test.controller.TestController getByName -c 5
此命令可以监控getByName方法执行的次数,每5s刷新一次
jad命令
作用
反编译指定已加载类的源码
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;
- 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
- 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解
参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] | 类所属 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
案例一:
jad --source-only com.laz.test.controller.TestController
此命令可以反编译TestController类文件或者java类里面的某个方法。
dashboard命令
作用
查看当前系统的实时数据面板
输入q
或者Ctrl+C
可以退出dashboard
命令
数据说明:
ID
:Java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应NAME
:线程名GROUP
:线程组名PRIORITY
:线程优先级, 1~10之间的数字,越大表示优先级越高STATE
:线程的状态CPU%
:线程消耗的cpu占比,采样100ms,将所有线程在这100ms内的cpu使用量求和,再算出每个线程的cpu使用占比。TIME
:线程运行总时间,数据格式为分:秒INTERRUPTED
:线程当前的中断位状态DAEMON
:是否是daemon线程
Thread命令
作用
线程相关堆栈信息。
参数说明
参数名称 | 参数说明 |
---|---|
数字 | 线程id |
[n:] | 指定最忙的前N个线程并打印堆栈 |
[b] | 找出当前阻塞其他线程的线程 |
[i ] | 指定cpu占比统计的采样间隔,单位为毫秒 |
Arthas支持管道,可以用 thread 1 | grep 'main('
查找到main class。
thread # 显示所有线程的信息
thread 1 # 显示1号线程的运行堆栈
thread -b # 查看阻塞的线程信息
thread -n 3 # 查看最忙的3个线程,并打印堆栈
thread -i 1000 -n 3 # 指定采样时间间隔,每过1000毫秒采样,显示最占时间的3个线程
thread --state WAITING # 查看处于等待状态的线程(WAITING、BLOCKED
死锁线程查看
thread # 查看线程状态
thread -b # 查看阻塞的线程信息
sc命令
作用
查看 JVM 已加载的类信息
参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的 ClassLoader 等详细信息。 |
如果一个类被多个 ClassLoader 所加载,则会出现多次 | |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[f] | 输出当前类的成员变量信息(需要配合参数-d 一起使用) |
[x:] | 指定输出静态变量时属性的遍历深度,默认为 0,即直接使用 toString 输出 |
[c:] | 指定 class 的 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[n:] | 具有详细信息的匹配类的最大数量(默认为 100) |
[cs ] | 指定 class 的 ClassLoader#toString() 返回值。长格式[classLoaderStr ] |
提示
class-pattern
支持全限定名,如 com.taobao.test.AAA,也支持 com/taobao/test/AAA 这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把/
替换为.
啦。sc
默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true开关
案例一:
sc com.laz.test.*
此命令可以输出当前路径下所有的类。
案例二:
sc -d com.laz.test.controller.TestController
此命令可以打印类的详细信息
mc命令
作用
内存编译
Memory Compiler/内存编译器,编译.java
文件生成.class
案例:
# 在内存中编译Hello.java为Hello.class
mc /root/Hello.java
# 可以通过-d命令指定输出目录
mc -d /root/bbb /root/Hello.java
redefine命令
作用
加载外部的
.class
文件,redefine
到JVM
里
注意,redefine
后的原来的类不能恢复,redefine
有可能失败(比如增加了新的field
)。
reset
命令对redefine
的类无效。如果想重置,需要redefine
原始的字节码。
redefine
命令和jad/watch/trace/monitor/tt
等命令会冲突。执行完redefine
之后,如果再执行上面提到的命令,则会把redefine
的字节码重置。
redefine的限制
- 不允许新增加
field/method
- 正在跑的函数,没有退出不能生效,比如下面新增加的
System.out.println
,只有run()
函数里的会生效。
使用步骤:
# 1. 使用jad反编译demo.MathGame输出到/root/MathGame.java
jad --source-only demo.MathGame > /root/MathGame.java
# 2.按上面的代码编辑完毕以后,使用mc内存中对新的代码编译
mc /root/MathGame.java -d /root
# 3.使用redefine命令加载新的字节码
redefine /root/demo/MathGame.class
实战演练
目的: 有时候我们再生产遇到一个紧急的问题,但是不方便重新发布服务或者发布的版本没有生效,此时就可以使用Arthas来临时修改线上代码,做到不发布版本就可以实现代码动态修改。
操作步骤:
1.定位到需要修改的类
2.将定位到的.class文件反编译成.java文件
3.修改.java文件
4.将修改后的.java文件重新编译成.class文件
5.将新的.class文件重新加载到JVM中
这里使用com.laz.test.controller.TestController
这个类为例
1.定位到需要修改的类
sc -d com.laz.test.controller.TestController
注意: 复制这个classLoaderHash,接下来要用
2.将定位到的.class文件反编译成.java文件
jad -c 6267c3bb --source-only com.laz.test.controller.TestController > /opt/java/TestController.java
- -c: 类所属 ClassLoader 的 hashcode
- –source-only: 默认情况下,反编译结果里会带有
ClassLoader
信息,通过--source-only
选项,可以只打印源代码 /opt/java/TestController.java
指的是反编译后的源文件放在哪里,并制定文件名
可以看到,java文件已经反编译成功。
3.修改.java文件
修改前:
修改后:
4.将修改后的.java文件重新编译成.class文件
mc -c 6267c3bb /opt/java/TestController.java
注意:/opt/java/com/laz/test/controller/TestController.class
是编译后的class文件的路径.
5.将新的.class文件重新加载到JVM中
redefine -c 6267c3bb /opt/java/com/laz/test/controller/TestController.class
到这一步,就成功将新的.class文件加载到JVM中了。
6.验证
访问我们刚才修改的接口。
可以看到,我们通过反编译修改的代码已生效。
本篇文章就介绍到这里,更多详情,请参考官方文档。