举个小栗子🌰带你入门Java诊断工具“Arthas”

1,894 阅读5分钟

前言

Arthas 开源已经有一段时间了,你可能听说过,但是还没有进行过使用。它是由阿里巴巴开源的一款Java 诊断工具,阿里开源的东西,使用起来的感受大都是...

McoSNF.jpg

Arthas 和传统的 jmap 啊 jstack 等等工具,有什么不同呢? 用螺丝刀作比喻的话,传统的工具有十字形的螺丝刀,一子形的螺丝刀等,Arthas 就是多功能瑞士军刀,属于综合型的诊断工具之一。闲话少说,今天就拿Arthas进行小试牛刀。

场景

既然是使用诊断工具,没错,首先我得有病啊🤗,那就先写一个简单的死循环 demo 让 CPU 飙升,程序无法正常退出,看看使用 arthas 有什么好办法呢? 试验环境是 WSL 的 Ubuntu,笔者为什么要用 WSL 环境呢?

MckYi4.png

WSL 这个东西的确鸡肋,而且跑不了 Docker,但是有些需要在 Linux 环境下操作的东西,用来应应急还是不错的(其实本次尝试在 Windows 进行试验一样是可行的),言归正传,开始动手:

进行测试

root@DESKTOP:~/project# mkdir test;cd test
root@DESKTOP:~/project/test# vim EndlessLoop.java
public class EndlessLoop {

    public static void main(String[] args) {
        System.out.println("enter main method");
        while(true) {
            task();
        }
    }

    public static void task() {
        System.out.println("do something...");
    }
}
root@DESKTOP:~/project/test# javac EndlessLoop.java

非常非常简单,直接 VIM 一个测试类,既然是测试死循环,就直接叫 EndlessLoop 吧,main 方法死循环调用 task。最后 javac 编译一下,没有报错,那我们就直接 java EndlessLoop 启动一下。

do something...
do something...
do something...
do something...

函数如预期的一样,在死循环打印do something。
此时我们打开新的终端找到下载好的 arthas-boot.jar 去启动它

root@DESKTOP:~/project# java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.4
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 98 EndlessLoop

Arthas 首先会将我们所有 Java application 打印出来,我们输入对应的编号即可,当前环境里只有一个应用,所以按照编号输入 1 就可以 attach 到我们的 EndlessLoop 类上

[INFO] arthas home: /root/.arthas/lib/3.1.4/arthas
[INFO] Try to attach process 4624
[INFO] Attach process 4624 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'


wiki      https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version   3.1.4
pid       4624
time      2019-11-18 22:01:35


version   3.1.4
pid       4624
time      2019-11-18 22:01:35

[arthas@4624]$

看到这个 banner,就说明我们 attach 成功了,也就是说我们可以用 Arthas 在不中断主应用的情况下花式调教我们的程序了。 首先可以用 dashboard 命令,它可以帮助我们看到整个应用的一个概览。 根据上图可以看到,信息非常全面,总共分了三个区域
笔者大致总结一下三个区域可以观察到什么信息:

1.第个区域,可以观察到线程的相关信息,包括优先级、占用 CPU、状态等等。 由此可以看出,线程死锁,和我们今天的死循环例子,可以从该区域直观的看出来了。

2.第个区域,可以观察到内存相关的使用,新生代的 eden 区,幸存者区以及老年代、元空间(1.8+)等信息,甚至还包含各代 GC 的次数,那么 GC 和 JVM 相关的调优,看这块区域肯定是没毛病了。

3.第个区域概括了 OS,jvm 相关的版本信息等,可以方便我们排查版本兼容性相关的问题。

回到我们的例子,在 dashboard 的第一个区域看到了线程 ID 位 1 的线程,CPU 使用率已经到了 100%,则继续使用thread 1命令进行跟踪,1 则是面板上左侧显示的线程 ID

[arthas@98]$ thread 1
"main" Id=1 RUNNABLE
    at java.io.FileOutputStream.writeBytes(Native Method)
    at java.io.FileOutputStream.write(FileOutputStream.java:326)
    at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
    at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
    -  locked java.io.BufferedOutputStream@560dcda3
    at java.io.PrintStream.write(PrintStream.java:482)
    -  locked java.io.PrintStream@cadc7f2
    at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
    at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
    at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
    -  locked java.io.OutputStreamWriter@54df3b5a
    at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
    at java.io.PrintStream.write(PrintStream.java:527)
    -  locked java.io.PrintStream@cadc7f2
    at java.io.PrintStream.print(PrintStream.java:669)
    at java.io.PrintStream.println(PrintStream.java:806)
    -  locked java.io.PrintStream@cadc7f2
    at EndlessLoop.task(EndlessLoop.java:11)
    at EndlessLoop.main(EndlessLoop.java:6)

Affect(row-cnt:0) cost in 65 ms.

arthas 根据命令打印了 ID 为 1 的线程相关的信息,我们和分析异常堆栈一样,找到熟悉的名称,就能定位到 EndlessLoop 这个类了,别忘了正常的应用肯定会有包名,根据包名grep就可以找到,本次因为是直接 VIM 一个单个 Java 文件,所以是没有包名的哦。

找到具体业务代码之后,建议用 sc(Search-Class)和 sm(Search-Method)命令观察一下类和方法的相关信息,带上-d 参数效果更佳! 本次就直接使用最直观的jad命令,反编译查看一下问题代码: [arthas@98]$ jad EndlessLoop --source-only

/*

* Decompiled with CFR.
  */
import java.io.PrintStream;

public class EndlessLoop {
    public static void task() {
        System.out.println("do something...");
    }

    public static void main(String[] arrstring) {
        System.out.println("enter main method");
        do {
            EndlessLoop.task();
        } while (true);
    }
}

可以看到,反编译的代码并不是完全一致的,这个是正常现象,只是代码细微不一样,程序的功能是完全不变的。假定我们是线上环境,想要让程序退出,结束死循环,该怎么办?

arthas 也集成了热部署功能,不愧是瑞士军刀,我们直接用热部署修改代码,先紧急对应线上应用,之后再从开发环境修改代码走流程发布,可以作为一种解决方案。

[arthas@98]$ jad EndlessLoop --source-only > /root/project/temp/EndlessLoop.java

我们先将指定修改的文件反编译到一个临时文件夹中进行修改,去到 arthas 外面进行 VIM 一波

root@DESKTOP:~/project/temp# vim EndlessLoop.java
/*
 * Decompiled with CFR.
 */
import java.io.PrintStream;

public class EndlessLoop {
    public static void task() {
        System.out.println("success exit");
        System.exit(0);
    }

    public static void main(String[] arrstring) {
        System.out.println("enter main method");
        do {
            EndlessLoop.task();
        } while (true);
    }
}
root@DESKTOP:~/project/temp# javac EndlessLoop.java

将 task 方法改为我们想要的打印 success exit 字符串并结束程序,然后编译为 class 文件(此处不用 javac 的话,在 arthas 里面用 mc 命令也可以进行编译)。编译成功后,回到 arthas 中,使用redefine命令就能重新加载该 class 文件了,我们试一下:

[arthas@98]$ redefine /root/project/temp/EndlessLoop.class
redefine success, size: 1

成功,再观察主程序打印的 log,发现修改已经生效了

do something...
do something...
do something...
do something...
do something...
success exit
root@DESKTOP:~/project/test#

与预期结果一致,打印了 success exit 并成功退出,笔记本的嗷嗷的风扇声音也停下来了...

总结

通过尝试,我们知道了如何去生产环境热部署一个 System.exit(0) 划掉! 我们通过了一个小 demo,从 dashboard 一步一步分析,最终得到了想要的效果, get 到了用 Arthas 对于线上应用的调试思路。

一定要学习 Arthash 吗?当然不,学习的不是工具,而是要掌握线上调试的思路,有了思路,我们利用各种工具都能解决问题。 如同代码一样,要掌握 Java,而不是学习 eclipse,Idea。

如果选择了 arthas,可以深入了解一下它更强大的功能,如 watch, ognl,arthas in Docker 等等,可以辅助使用者快速解决更复杂的问题。

这就完了?线上调试就这么简单?大错特错,上文的 demo 也就是冰山一角,线上的突发情况,是洪水猛兽,需要兵来将挡水来土掩,比如多节点的服务,热修改代码后,直接在外部访问测试,就负载到其他机器上看不到效果了吧?curl 一下本地服务是不是才更有效? ...等等等等有很多花式场景,就等着我们去灵活对应了~