暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

简明的Arthas配置及基础运维教程

56

写在文章开头

Arthas
是一款强大的开源Java
诊断程序,它可以非常方便的启动并以界面式的方式和Java
程序进行交互,支持监控程序的内存使用情况、线程信息、gc情况、甚至可以反编译并修改现上代码等。上一篇文章有读者反馈比较进阶,所以笔者对此专门整理了一篇Arthas入门实用教程,希望对你有帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。

简述Arthas的运行原理

arthas
的运行原理大致是以下几个步骤:

  1. 启动arthas选择目标Java程序后,artahs会向目标程序注入一个代理。
  2. 代理会创建一个集HTTP和Telnet的服务器与客户端建立连接。
  3. 客户端与服务端建立连接。
  4. 后续客户端需要监控或者调整程序都可以通过服务端Java Instrumentation机制和应用程序产生交互。

详解Arthas基础使用

下载安装

在介绍几个典型的案例之前,我们需要先下载安装一下Arthas
Arthas
的官方地址如下:

https://arthas.aliyun.com/

考虑到方便笔者一般是使用命令行的方式下载:

curl -O https://arthas.aliyun.com/arthas-boot.jar


完成后我们通过下面这个命令就可以将Arthas
启动了。

java -jar arthas-boot.jar

此时我们就可以看到对应的进程序号和进程的pid,以笔者为例,开启arthas之后就会看到一个序号为1的9121的Java进程,我们可以直接点击1并输入回车对此进程进行监控管理:

[INFO] JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9121 arthasExample.jar

随后arthas
初次会进行相关依赖下载,然后我们就可以正式的使用arthas
管理当前进程了:

离线用户的使用姿势(可选阅读)

考虑到内网用户无法联网进行arthas
初始化,所以arthas
也人性化的提供了全量包的下载方式,有需要的读者可移步到下载页面选择全量包下载即可获取全量的arthas
包:

完成后下载并解压之后,我们可以直接通过下面这个脚本指令快速启动arthas

./as.sh

常见指令介绍

步入arthas
我们就可以进行一些比较基础的操作,以下是笔者日常用的比较多的指令,和Linux
差不多,读者可自行参阅了解

  1. cat
    :打印文件内容。
  2. cls
    :清空当前屏幕区域内容。
  3. grep
    :匹配查找。
  4. history
    :打印历史命令。
  5. pwd
    :输入当前Java
    进程所在的位置。
  6. quit
    :退出当前arthas
    客户端。
  7. stop
    :关闭arthas
    服务端,所有arthas
    客户端都会退出。

这里笔者就简单的演示一下,可以看到pwd
输出的就是笔者所监控的Java进程所处的文件目录:

[arthas@9121]$ pwd
# 当前监控的进程在服务器上的目录
/home/sharkchili/arthasExample
[arthas@9121]$

点击quit
会直接退出当前进程的客户端,stop
同理只不过多是连着服务端和其他客户端一并杀掉,这里就不多做演示了:


[arthas@9121]$ quit
# 直接返回到服务器的目录
sharkchili@DESKTOP-7IPKPVJ:~/arthas$


快捷启动配置

为了快捷启动arthas,笔者也给出个人的配置方式,首先vim一个名为as.sh
的脚本,其内容为arthas-boot.jar
的启动指令:

java -jar /home/sharkchili/arthas/arthas-boot.jar

完成脚本编写确认启动无误之后,我们将这个脚本通过alias
重命名的方式追加到/etc/profile
下,内容如下即sh
指令加上上述脚本的全路径:

alias as="sh /home/sharkchili/arthas/as.sh"

然后通过source
指令使其生效:

可以看到完成这样一段配直接后,我们可以直接通过as
指令完成arthas
的快捷启动:

# 键入as
sharkchili@DESKTOP-xxx:~$ as
# 直接快速启动arthas
[INFO] JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9121 arthasExample.jar

Arthas中比较常用的运维指令

查看实时数据面板

日常开发维护过程中对于项目的巡检还是蛮重要的,通过Arthas
dashboard
可以非常直观的查看当前系统中进程的运行情况。

arthas
的控制面板输入dashboard
,默认情况下5s进行一次刷新:

这里我们来简单介绍一下第一板块线程中的字段的含义:

  1. ID:javaj级别的线程id号,注意与jstack中的native id的区别。
  2. NAME:线程名称。
  3. GROUP:线程所在线程组名。
  4. PRIORITY:线程优先级,值越大优先级越高。
  5. STATE:线程运行状态。
  6. CPU%:线程CPU使用率。
  7. DELTA_TIME:上次采样之后,线程运行的CPU时间,单位为秒。
  8. TIME:线程运行的总CPU时长,数据格式为分:秒。
  9. INTERRUPTED:线程当前的中断位状态。
  10. DAEMON :是否为守护线程。

第二板块就是内存使用版块,记录各个堆区、元空间的内存使用情况以及GC情况。而第三板块则是服务器运行参数版块,这一版块记录着程序当前运行服务器的内核版本信息、jdk版本等。

需要了解的是arthas
中的操作指令可以通过--help
了解查阅,我们以dashboard
为例,其使用说明如下,可以看到我们可以通过-i决定面板刷新间隔(单位是毫秒),用-n决定面板刷新次数:

所以如果我们希望每1s刷新1次,刷新5次,那么对应的命令就是:

dashboard -i 1000 -n 5

查看JVM信息

arthas
也可能非常直观的查看jvm
信息,对应的指令也就是jvm

同样的这个指令也会输出多个板块的内容,我们先来看看第一个板块,可以可以看到该指令可以非常直观的看到机器名称、jvm启动时间、jdk版本以及我们配置jvm参数信息:

由于板块比较多,这里笔者就说几个笔者比较常用的板块,分别是线程板块和文件描述符板块,通过这两个板块笔者可以日常巡检了解是否发生线程死锁或者程序中是否出现资源未能及时关闭的情况:

  1. THREAD
    :它记录当前活跃线程数、活跃的守护线程数、从JVM启动开启曾经活着的最大线程数、总共启动线程数以及发生死锁的线程数。
  2. FILE-DESCRIPTOR
    :这个板块记录JVM可以打开的最大文件描述符和当前已经打开的文件描述符数。

查看和修改日志

logger指令也算是笔者比较喜欢的指令,它可以非常直观的查看我们对于日志的配置,如下图,以笔者当前运行的程序为例,可以看到如下几个信息:

  1. 日志级别为INFO
  2. 存储错误日志的ERROR_FILE
    的相对路径。
  3. 存储普通日志INFO_FILE
    的相对路径。

当然我们也可以查看指定名字的日志信息,例如我们想查看com.example.arthasExample.TestController
的日志信息,就可以直接键入logger -n com.example.arthasExample.TestController
指令进行查看:

logger
指令还有一个比较实用的功能,即直接修改日志级别,例如我们希望修改ROOT
这个名称的日志级别,就可以基于如下步骤完成修改:

  1. 获取classLoader
    的哈希码。
  2. 基于哈希码通过logger
    指令修改日志级别。

我们程序中有这样一段代码,此时我们请求下面这个接口只会输出info
级别的日志:

 @GetMapping(value = {"/user/logger"})
    public String loggerPrint() {
        log.info("info logger");
        log.debug("debug logger");
        return "success";
    }

对应的输出结果为:

2024-08-19 23:53:29.454  INFO  c.e.a.TestController          :138  http-nio-8080-exec-1                    info logger


接下来我们就直接通过arthas
修改日志级别,首先我们需要获取当前classloader
的哈希码:

sc -d com.example.arthasExample.TestController

然后我们直接通过这个哈希码,执行如下指令将日志设置为debug

logger -c 306a30c7 --name ROOT --level debug

于是debug日志就出现了:

2024-08-20 00:13:31.438  INFO  c.e.a.TestController          :138  http-nio-8080-exec-6                    info logger
2024-08-20 00:13:31.439  DEBUG c.e.a.TestController          :139  http-nio-8080-exec-6                    debug logger


查看JVM内存信息

接下来就是memory
指令,这也是笔者比较常用的指令之一,通过memory
我们可以监控到当前内存的使用情况: 如下图所示,键入memory
指令后我们就可以看到这些区域的内存已用、总大小、最大值以及使用率等信息:

对应的我们也给出上文中memory
各行代表的含义:

  1. heap
    :堆区内存。
  2. ps_eden_space
    :堆内存中新生代Eden
    区。
  3. ps_survivor_space
    :堆内存中新生代survivor
    区。
  4. ps_old_gen
    :堆内存老年代区。
  5. nonheap
    :非堆内存,即堆内存之外的内存。
  6. code_cache
    :因为Java执行是将字节码编译为机器码,而这个区域就是用于缓存这部分代码。
  7. metaspace
    :元空间,以jdk1.8为例该空间是用于存储Java类和方法的元数据信息、常量池等。
  8. compressed_class_space
    :存放类文件信息的区域。

对应的我们在这里也简单的复习一下JVM
内存区域的分布,建议读者可参考下图了解memory
指令中各个字段的含义:

查看JVM的环境变量

arthas
sysenv
指令常用于获取系统环境变量信息,键入这条指令我们可以看到当前java程序所使用的系统大部分环境变量信息,如下图,可以看到大部分的当前系统用户名称、编码格式、当前程序路径以及客户端ip和端口号等信息:

根据help
的提示,这条指令同样也支持查询单个环境变量,不过意义不大,毕竟不是每个都知道环境变量叫什么,只有查看了才知道(笑):

[arthas@23543]$ sysenv PWD
 KEY                                              VALUE                                                                                                                                                                                               
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 PWD                                              /home/sharkchili/arthasExample

查看和修改JVM系统属性

sysprop
查看的jvm的系统属性,基本上通过这个指令我们可以看到大部分的JVM
参数配置信息,如下输出结果,我们大体可以看到JDK版本、程序名称以及日志编码格式和当前系统用户名称等:

查看当前JVM线程堆栈信息

arthas
提供thread
指令用于查看线程情况,如下所示,它基本打印了线程计数信息和几个活跃的线程的实时情况,默认情况下,它是按照CPU
增量时间降序进行排序:

按照help
的提示,我们也可以通过-n打印前几个忙碌的线程调用堆栈信息,如下所示,笔者希望打印处前2条忙碌的线程,键入的指令为thread -n 2
:

同时arthas
也支持按照时间间隔进行输出打印,比如我们希望列出5s内最忙的3个线程,那么对应的指令就是:

thread -n 3 -i 5000

当我们的Java程序有大量的线程时候,我们希望筛选中某种状态的线程,我们可以通过--state
指定,例如我们希望打印处于RUNNABLE
状态的线程,那么我们就可以键入thread --state RUNNABLE
来获得输出结果:

对于死锁问题,我们也可以通过-b指令来定位查看当前程序是否存在阻塞其他线程的线程,如下图所示,以笔者为例当前程序就不存在死锁的情况:

此时我们给出一个触发死锁的接口并调用:

@RequestMapping("dead-lock")
    public void deadLock() {
        //线程1先取得锁1,休眠后取锁2
        new Thread(() -> {
            synchronized (lock1) {
                try {
                    log.info("t1 successfully acquired the lock1......");
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                synchronized (lock2) {
                    log.info("t1 successfully acquired the lock1......");
                }
            }
        }, "t1").start();

        //线程2先取得锁2,休眠后取锁1
        new Thread(() -> {
            synchronized (lock2) {
                try {
                    log.info("t2 successfully acquired the lock2......");
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                synchronized (lock1) {
                    log.info("t2 successfully acquired the lock1......");
                }
            }
        }, "t2").start();
    }

此时,键入-b指令就可以定位阻塞其他线程的线程以及所在代码段:

vmtool对于JVM的调控

vmtool算是笔者用的比较多的一个工具指令,可用于查询对象或者强制GC等功能,这些功能读者可自行参考官网查阅:

vmtool:https://arthas.aliyun.com/doc/vmtool.html

而笔者这里想要介绍的是一个强制打断线程的功能,这个指令对于特定场景下应急处理还是蛮实用的。

我们的程序调用下面的接口被系统监控到CPU100%,此时我们就可以通过arthas进行特定场景下的应急处理:

@RequestMapping("cpu-100")
    public static void cpu() {
        //无限循环输出打印
        new Thread(() -> {
            while (true) {
                log.info("cpu working");
            }
        }, "thread-1").start();
    }

我们通过thread指令看到thread-1基本将单个CPU跑满了,并且我们通过控制台定位到对应的id为48:

此时我们可以通过vmtool
action
指令将线程打断:

vmtool --action interruptThread -t 48

完成操作之后即可看到这个线程被我们成功打断了:

同时vmTool也支持观测变量的详情,以下面这个实例变量dateTimeStr
为例,每次接口请求都会实时刷新:

private String dateTimeStr = DateUtil.formatDateTime(new Date());

    @RequestMapping(value = "/getVal")
    public String getVal() {
        dateTimeStr = DateUtil.formatDateTime(new Date());
        log.info("dateTimeStr: {}", dateTimeStr);
        return "success";
    }

如果我们希望查看此刻dateTimeStr 的值,我们就可以通过vmtool
action
指定为getInstances
,然后指定类的全路径(以笔者这段代码为例则是com.example.arthasExample.TestController)
,最后键入表达式instances[0].dateTimeStr
意为获取当前实例的dateTimeStr

vmtool  --action getInstances --className com.example.arthasExample.TestController  --express 'instances[0].dateTimeStr' 

此时我们就可以非常直观的监控到这个变量的信息了:

小结

因为前段时间对于arthas的安利写的相对进阶一些,对于某些基础的步骤没有做太多介绍,所以补充了这篇文章,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。

参考

深入探究 JVM | 探秘 Metaspace:https://www.sczyh30.com/posts/Java/jvm-metaspace/

JVM运行时内存 :https://blog.csdn.net/m0_53611007/article/details/120685628


文章转载自写代码的SharkChili,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论