arthas诊断工具简单使用
Arthas(阿尔萨斯)是什么
Arthas是Alibaba开源的Java诊断工具
Arthas能做什么
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从JVM内查找某个类的实例?
开发中常见的问题
前端调用一个后端的接口,该接口逻辑非常复杂,可能会调用其他service接口或其他模块的FeignClient,但是在其中某个接口报错了,报错信息又不是很明显,这种情况下,一般要么分析代码逻辑,要么增加日志输出来排查发生异常的原因
使用Arthas后,可以通过
watch
命令观察指定方法的调用情况,可以观察入参
、返回值
、异常信息
、本次调用的方法信息
、本次调用的类信息
等,配合ognl表达式可以实现复杂的操作后端改动或者增加一个复杂的接口后,联调时可能遇到一个问题,改一遍代码,然后pull代码,编译,重启服务,然后又遇到问题,再修改代码,再重复同样的操作,这些步骤十分的耗时,如果开发时间比较紧迫,更加让人难以忍受
使用Arthas的
jad
、mc
、retransform
命令修改java代码,编译字节码文件,加载新的字节码,不需要重新pull代码,编译,重启等一系列耗时的操作,就能直接实现修改后的效果
如何使用
安装启动
安装方式很多,有直接启动jar包、使用shell脚本、rpm安装等,这里使用直接启动jar包的方式
1
2
3
4
5下载arthas-boot.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar
启动
java -jar arthas-boot.jar选择要attach的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28[INFO] arthas-boot version: 3.5.4
[INFO] Process 10294 already using port 3658
[INFO] Process 10294 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 10294 java-study-0.0.1-SNAPSHOT.jar
[2]: 13008 org.apache.zookeeper.server.quorum.QuorumPeerMain
[3]: 19681 /usr/nacos/target/nacos-server.jar
[4]: 4819 org.elasticsearch.bootstrap.Elasticsearch
[5]: 21381 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
[6]: 22535 kafka.Kafka
[7]: 21772 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
1
[INFO] arthas home: /root/.arthas/lib/3.5.4/arthas
[INFO] The target process already listen port 3658, skip attach.
[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.4
main_class
pid 10294
time 2021-09-15 10:18:15输入
help
查看命令帮助信息
使用
watch
命令观察方法的调用情况,如入参,返回值,异常信息等
watch
命令参数参数名称 参数说明 class-pattern 类名表达式匹配 method-pattern 方法名表达式匹配 express 观察表达式,默认值: {params, target, returnObj}
condition-express 条件表达式 [b] 在方法调用之前观察 [e] 在方法异常之后观察 [s] 在方法返回之后观察 [f] 在方法结束之后(正常返回和异常返回)观察 [E] 开启正则表达式匹配,默认为通配符匹配 [x:] 指定输出结果的属性遍历深度,默认为 1 观察表达式
观察表达式的构成主要由ognl表达式组成,使用它可以获取对象的属性,调用对象的方法等
ognl表达式可以使用的对象有params(入参)、target(当前调用的类信息)、returnObj(返回值)、throwExp(异常信息)等,详细内容参考表达式核心变量
测试代码
根据id查询用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public User getUserById(Long id) throws Exception {
if (StringUtils.isEmpty(id)) {
throw new Exception("用户id为空!");
}
List<User> collect = userList.stream().filter(u -> id.equals(u.getId()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(collect)) {
throw new Exception("根据id未找到用户信息");
}
return collect.get(0);
}观察方法调用情况
观察方法入参和返回值
执行watch命令:
1
2# {params, returnObj} 表示需要观察方法入参和返回值,-n 5 表示执行5次后结束 -x 3 表示遍历对象的深度
watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3调用接口:
1
curl http://localhost:8088/test/user/1
查看控制台输出:这里result中包含了入参和返回的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17[arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 25
method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
ts=2021-09-17 14:28:29; [cost=1.151249ms] result=@ArrayList[
@Object[][
@Long[1],
],
@User[
id=@Long[1],
username=@String[1001],
avatar=@String[https://xxxx.com/1001.png],
phoneNumber=@String[10010010001],
age=@Integer[20],
gender=@String[男],
],
]观察方法抛出的异常信息
执行watch命令:
1
2# {params, throwExp} 表示需要观察方法入参和异常对象
watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3调用接口:
1
curl http://localhost:8088/test/user/1001
查看控制台输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15[arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 70 ms, listenerId: 26
method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
ts=2021-09-17 14:29:56; [cost=2.055759ms] result=@ArrayList[
@Object[][
@Long[1001],
],
java.lang.Exception: 根据id未找到用户信息
at com.codecho.test.service.UserServiceImpl.getUserById(UserServiceImpl.java:48)
at com.codecho.test.controller.TestController.getUserById(TestController.java:28)
at sun.reflect.GeneratedMethodAccessor25.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
...可以通过调整观察表达式只展示异常具体信息
1
watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3
重新调用接口,再次查看控制台输出
1
2
3
4
5
6
7
8
9
10[arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 27
method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
ts=2021-09-17 14:31:08; [cost=1.685761ms] result=@ArrayList[
@Object[][
@Long[1001],
],
@String[根据id未找到用户信息],
]只观察满足条件的方法调用
执行watch命令:
1
2# params[0]%2==0 表示当用户id为偶数时才会记录调用信息
watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3调用接口:
1
2
3
4curl http://localhost:8088/test/user/1
curl http://localhost:8088/test/user/2
curl http://localhost:8088/test/user/3
curl http://localhost:8088/test/user/4查看控制台输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31[arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 28
method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
ts=2021-09-17 14:32:06; [cost=0.067629ms] result=@ArrayList[
@Object[][
@Long[2],
],
@User[
id=@Long[2],
username=@String[1002],
avatar=@String[https://xxxx.com/1002.png],
phoneNumber=@String[10010010002],
age=@Integer[23],
gender=@String[男],
],
]
method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
ts=2021-09-17 14:32:11; [cost=0.081801ms] result=@ArrayList[
@Object[][
@Long[4],
],
@User[
id=@Long[4],
username=@String[1004],
avatar=@String[https://xxxx.com/1004.png],
phoneNumber=@String[10010010004],
age=@Integer[21],
gender=@String[女],
],
]执行watch命令:
1
2# #cost > 5 表示耗时超过5ms才会记录调用信息 -v 表示打印条件表达式的值和结果
watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3调用接口:
1
curl http://localhost:8088/test/user/1
查看控制台输出:
1
2
3
4
5
6
7
8
9[arthas@10294]$ watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 8) cost in 76 ms, listenerId: 29
Condition express: #cost > 5 , result: false
Condition express: #cost > 5 , result: false
Condition express: #cost > 5 , result: false
Condition express: #cost > 5 , result: false
Condition express: #cost > 5 , result: false
Condition express: #cost > 5 , result: false
使用
trace
命令查看方法的调用路径和每个节点的耗时
trace
命令参数参数名称 参数说明 class-pattern 类名表达式匹配 method-pattern 方法名表达式匹配 condition-express 条件表达式 [E] 开启正则表达式匹配,默认为通配符匹配 [n:] 命令执行次数 #cost 方法执行耗时 观察接口调用路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32[arthas@40528]$ trace *ActionInfoServiceImpl interactionAssist -n 2
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 857 ms, listenerId: 4
`---ts=2021-09-17 15:37:53;thread_name=http-nio-8325-exec-1;id=85;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@4258106
`---[26.143404ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl$$EnhancerBySpringCGLIB$$34d24eff:interactionAssist() [throws Exception]
+---[25.926649ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() [throws Exception]
| `---[21.546574ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:interactionAssist() [throws Exception]
| +---[0.131916ms] com.alibaba.fastjson.JSONObject:toJSONString() #6957
| +---[0.190136ms] org.apache.logging.log4j.Logger:info() #6957
| +---[0.00505ms] com.hand.core.util.UserUtil:getUser() #6960
| +---[0.00535ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getInteractionId() #6966
| +---[1.262189ms] com.hand.interaction.base.interaction.service.InteractionService:queryByIdCache() #6966
| +---[0.00564ms] com.hand.interaction.base.interaction.model.Interaction:getEndTime() #6970
| +---[0.00526ms] com.hand.interaction.base.interaction.model.Interaction:getLicenseMode() #6977
| +---[0.010319ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:checkUserInfo() #6977
| +---[0.004508ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getInteractionId() #6980
| +---[0.00497ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getOpenId() #6980
| +---[0.006282ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getMobilePhone() #6980
| +---[0.127087ms] com.hand.interaction.base.util.CommonUtils:tianYu() #6980
| +---[0.005831ms] com.hand.base.user.model.User:getOpenId() #6982
| +---[0.005991ms] com.hand.core.util.StringUtils:isBlank() #6983
| +---[0.009688ms] com.hand.interaction.base.interaction.model.Interaction:getBoostType() #6988
| +---[0.008306ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getHpOpenid() #6988
| +---[0.004388ms] com.hand.core.util.StringUtils:isBlank() #6989
| +---[0.007474ms] com.hand.interaction.base.actioninfo.model.ActionInfo:setHpOpenid() #6992
| +---[min=0.003366ms,max=0.007244ms,total=0.01061ms,count=2] com.hand.interaction.base.interaction.model.Interaction:getInteractionFormat() #6997
| +---[0.005109ms] com.hand.interaction.base.interaction.model.Interaction:getShareMode() #7026
| +---[0.004529ms] com.hand.interaction.base.interaction.model.Interaction:getId() #7028
| +---[0.005249ms] com.hand.interaction.base.interaction.model.Interaction:getBoostType() #7035
| +---[19.019048ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:newMemBoostValidate() #7050 [throws Exception]
| `---throw:com.hand.core.basic.service.BasicServiceException #7585 [您已经是会员,无法助力!]
`---throw:com.hand.core.basic.service.BasicServiceException #49 [您已经是会员,无法助力!]
使用
jad
、mc
、retransform
命令热更新代码
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void payMoney(User user) throws Exception {
// 查找用户
User queryUser = null;
if (!StringUtils.isEmpty(user.getId())) {
queryUser = getUserById(user.getId());
} else {
queryUser = getUserByUsername(user.getUsername());
}
// 测试调用普通方法
callPublicMethod(queryUser);
callPrivateMethod(queryUser);
// 支付收款
payService.pay(queryUser, new BigDecimal("88.8"));
}使用
jad
命令反编译要修改的类的字节码,并保存到指定目录1
jad --source-only *UserServiceImpl > /usr/arthas/UserServiceImpl.java
使用vim编辑反编译后的java代码,将金额从88.8改成99.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public void payMoney(User user) throws Exception {
// 查找用户
User queryUser = null;
if (!StringUtils.isEmpty(user.getId())) {
queryUser = getUserById(user.getId());
} else {
queryUser = getUserByUsername(user.getUsername());
}
// 测试调用普通方法
callPublicMethod(queryUser);
callPrivateMethod(queryUser);
// 支付收款
payService.pay(queryUser, new BigDecimal("88.8"));
}使用
mc
命令内存编译修改后的java代码,-d 指定输出class的目录1
mc /usr/arthas/UserServiceImpl.java -d /usr/arthas
注意:
mc
命令是可能编译失败的,如果编译失败,可以在本地修改java代码后将编译后的class文件上传到服务器上再使用mc
命令使用
retransform
命令加载修改后的字节码文件1
retransform /usr/arthas/UserServiceImpl.class
使用watch命令观察接口入参,重新调用接口
1
watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
1
curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 99.9 元的收款1
2
3
4
5
6
7
8
9
10
11
12
13
14
15[arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
@User[
id=@Long[2],
username=@String[1002],
avatar=@String[https://xxxx.com/1002.png],
phoneNumber=@String[10010010002],
age=@Integer[23],
gender=@String[男],
],
@String[99.9],
]如果要取消
retransform
命令的效果,需要删除修改类的retransform entry
并重新触发retransform
,如果不进行此操作,即使stop arthas后,retransform
的效果仍会生效1
2
3
4
5
6
7
8# 查看retransform entry
retransform -l
# 删除指定的retransform entry
retransform -d 1
# 重新触发retransform
retransform --classPattern *UserServiceImpl使用watch命令观察接口入参,重新调用接口
1
watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
1
curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 99.9 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 99.9 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 99.9 元的收款
测试调用普通public方法
测试调用普通private方法
用户 1002支付了一笔 88.8 元的收款1
2
3
4
5
6
7
8
9
10
11
12
13
14
15[arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
@User[
id=@Long[2],
username=@String[1002],
avatar=@String[https://xxxx.com/1002.png],
phoneNumber=@String[10010010002],
age=@Integer[23],
gender=@String[男],
],
@String[88.8],
]
quit
、stop
quit
退出当前arthas客户端,arthas服务端不会关闭,所做的修改不会重置
stop
关闭arthas服务端,所有arthas客户端都会退出,关闭前增强类会被重置,使用
redefine
、redefine
重加载的类不会被重置