Java线上监控诊断产品Arthas

最近一直在研究Java的动态追踪技术,碰到了Arthas,正好以前也想学,趁此机会就了解了一下。
什么是Arthas?首先我们看看Arthas官方文档是怎么描述的:

什么是Arthas

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

Arthas能为你做什么

Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

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

Java线上监控诊断产品Arthas_第2张图片

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,说明热更成功,我们浏览器请求一下
Java线上监控诊断产品Arthas_第3张图片
可以看到代码已经热更成功,注意,这里的热更类似于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官方文档

你可能感兴趣的:(linux,后端,SpringBoot,java,jvm)