最近一直在研究Java的动态追踪技术,碰到了Arthas,正好以前也想学,趁此机会就了解了一下。
什么是Arthas?首先我们看看Arthas官方文档是怎么描述的:
什么是Arthas
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
Arthas能为你做什么
Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到 JVM 的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从 JVM 内查找某个类的实例?
下面我们直接进入正题。
前期准备工作
我们首先准备一个简单的http访问链接。
package wq.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class DockerController {
@RequestMapping("/one")
public String testOne(String name) {
return name;
}
}
然后maven打包放到我们的linux服务器上面
[root@VM-12-12-centos ~]# cd /data/arthas-test/
我们的jar包叫demo-0.0.1-SNAPSHOT.jar
[root@VM-12-12-centos arthas-test]# ls
demo-0.0.1-SNAPSHOT.jar
后台启动我们的项目
[root@VM-12-12-centos arthas-test]# nohup java -jar demo-0.0.1-SNAPSHOT.jar &
下载安装Arthas
Arthas提供了两种下载方式,第一种是下载jar包,第二种是下载全量包,本文用的是第一种。我们找一个常用的路径,用curl下载
[root@VM-12-12-centos ~]# curl -O https://arthas.aliyun.com/arthas-boot.jar
[root@VM-12-12-centos ~]# ls
arthas-boot.jar
可以看到我们的已经下载好了,名字叫arthas-boot.jar,接下来用java -jar启动
[root@VM-12-12-centos ~]# java -jar arthas-boot.jar
[INFO] JAVA_HOME: /data/java/jdk1.8.0_181/jre
[INFO] arthas-boot version: 3.6.7
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 19401 demo-0.0.1-SNAPSHOT.jar
我们可以看到,在输出的最下面一行,有个[1]号进程,这就是我们刚刚启动的demo,Arthas启动的时候会检测所有的java进程,并且用数字的形式排列出来,我么需要监听什么,就输入数字就行了,一次只能监听一个进程。
[root@VM-12-12-centos ~]# java -jar arthas-boot.jar
[INFO] JAVA_HOME: /data/java/jdk1.8.0_181/jre
[INFO] arthas-boot version: 3.6.7
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 19401 demo-0.0.1-SNAPSHOT.jar
1
[INFO] arthas home: /root/.arthas/lib/3.6.7/arthas
[INFO] Try to attach process 19401
Picked up JAVA_TOOL_OPTIONS:
[INFO] Attach process 19401 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.6.7
main_class
pid 19401
time 2023-04-14 14:38:23
[arthas@19401]$
当出现Arthas彩色字体的时候,说明监听成功,此时我们的用户切换成了arthas。在这个界面我们能做很多操作,具体的可以看上面的官方文档,这里我就列举一下常用的指令。
1.dashboard:当前系统的实时数据面板,按 ctrl+c 退出。
参数-i:刷新时间间隔,-n:刷新次数
数字字段说明:
ID: Java 级别的线程 ID,注意这个 ID 不能跟 jstack 中的 nativeID 一一对应。
NAME: 线程名
GROUP: 线程组名
PRIORITY: 线程优先级, 1~10 之间的数字,越大表示优先级越高
STATE: 线程的状态
CPU%: 线程的 cpu 使用率。比如采样间隔 1000ms,某个线程的增量 cpu 时间为 100ms,则 cpu 使用率=100/1000=10%
DELTA_TIME: 上次采样之后线程运行增量 CPU 时间,数据格式为秒
TIME: 线程运行总 CPU 时间,数据格式为分:秒
INTERRUPTED: 线程当前的中断位状态
DAEMON: 是否是 daemon 线程
2.jvm:查看当前 JVM 信息
Thread相关参数:
COUNT: JVM 当前活跃的线程数
DAEMON-COUNT: JVM 当前活跃的守护线程数
PEAK-COUNT: 从 JVM 启动开始曾经活着的最大线程数
STARTED-COUNT: 从 JVM 启动开始总共启动过的线程次数
DEADLOCK-COUNT: JVM 当前死锁的线程数
3.mc :编译.java文件为.class文件
mc 命令有可能失败。如果编译失败可以在本地编译好.class文件,再上传到服务器。失败的原因,其中之一是本地的jdk版本和linux服务器的jdk版本不一致。
4.redefine和retransform
这两个指令是比较常用的,都是加在外部的.class文件,去重载已经jvm已经加在过的类,通常用于不重启项目的情况下热更文件。因为我jdk版本的原因,所以我直接修改java文件,然后编译成class文件。
修改我们的测试controller
@RestController
@RequestMapping("/test")
public class DockerController {
@RequestMapping("/one")
public String testOne(String name) {
return "hello word" + name;
}
}
上传class文件:DockerController.class
[root@VM-12-12-centos arthas-test]# ls
arthas-output demo-0.0.1-SNAPSHOT.jar DockerController.class nohup.out
然后重载
[arthas@26621]$ retransform /data/arthas-test/DockerController.class
retransform success, size: 1, classes:
wq.demo.controller.DockerController
[arthas@26621]$
当出现retransform success, size: 1,说明热更成功,我们浏览器请求一下
可以看到代码已经热更成功,注意,这里的热更类似于idea右键的Compile And Reload File,不能修改方法体,常量等。
5.sm:查看已加载类的方法信息
[arthas@26621]$ sm -d wq.demo.controller.DockerController
declaring-class wq.demo.controller.DockerController
constructor-name <init>
modifier public
annotation
parameters
exceptions
classLoaderHash 31221be2
declaring-class wq.demo.controller.DockerController
method-name testOne
modifier public
annotation org.springframework.web.bind.annotation.RequestMapping
parameters java.lang.String
return java.lang.String
exceptions
classLoaderHash 31221be2
Affect(row-cnt:2) cost in 12 ms.
也可以用通配符,sm -d *Controller,查看所有Controller结尾的类,-d:展示每个方法的详细信息
6.watch:很常用的一个指令,监听请求的参数和返回
[arthas@26621]$ watch wq.demo.controller.DockerController testOne params -x 3
主要分三个结构,watch 类 方法名 监听范围 ,-x监听参数深度,比如我这个就是监听wq.demo.controller.DockerController类的testOne 方法的params(参数值),我在浏览器请求:
http://101.34.174.*:8080/test/one?name=watch,传参watch,可以看到,已经有了打印
method=wq.demo.controller.DockerController.testOne location=AtExit
ts=2023-04-14 15:10:44; [cost=0.036007ms] result=@Object[][
@String[watch],
]
当然监听类容可以用ognl表达式:“{params,returnObj}”,就表示监听参数和返回值。
7.stack:输出方法的调用过程
我们还是以testOne方法为例
[arthas@26621]$ stack wq.demo.controller.DockerController testOne
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 21 ms, listenerId: 4
ts=2023-04-14 15:15:04;thread_name=http-nio-8080-exec-9;id=18;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@7c417213
@wq.demo.controller.DockerController.testOne()
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
因为我们这里方法比较简单,所以看不出什么来,实际环境就正常了。
8.trace:方法内部调用路径,并输出方法路径上的每个节点上耗时
[arthas@26621]$ trace wq.demo.controller.DockerController testOne
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 40 ms, listenerId: 5
`---ts=2023-04-14 15:17:12;thread_name=http-nio-8080-exec-3;id=12;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@7c417213
`---[0.054142ms] wq.demo.controller.DockerController:testOne()
可以看到我们请求只用了0.054142ms。
9.jad:反编译指定已加载类的源码
$ jad java.lang.String
ClassLoader:
Location:
/*
* Decompiled with CFR.
*/
package java.lang;
import java.io.ObjectStreamField;
import java.io.Serializable;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
...
public String(byte[] byArray, int n, int n2, Charset charset) {
/*460*/ if (charset == null) {
throw new NullPointerException("charset");
}
/*462*/ String.checkBounds(byArray, n, n2);
/*463*/ this.value = StringCoding.decode(charset, byArray, n, n2);
}
...
暂时先写这么多了,更多请参考Arthas官方文档