简明的Arthas使用教程

692 阅读13分钟

写在文章开头

Arthas是一款强大的开源Java诊断程序,它可以非常方便的启动并以界面式的方式和Java程序进行交互,支持监控程序的内存使用情况、线程信息、gc情况、甚至可以反编译并修改现上代码等。所以它成为笔者进行线上问题排查的重要手段,而本文将从实际使用的角度介绍一下arthas的基本使用技巧,希望对你有帮助。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/…

常见的运维操作示例

反编译查看代码

上文我们其实已经用到了jad这个反编译命令,对于笔者来说,jad有两种比较常见的用法,除了上述那种反编译类的指令jad --source-only 类的包路径,还有一种定位方法代码段的命令jad --source-only 类的包路径 方法名

例如笔者想定位TestControllerdeadLock代码,我们就可以键入:

jad --source-only  com.example.arthasExample.TestController deadLock

如下图,我们可以直接看到的方法的代码:

查看字段详情

我们希望看到某个类下所有字段的详情,我们就可以使用这条命令

sc -d -f 类的包路径

例如笔者想查看TestController的字段详情,就可以键入这条命令:

sc -d -f com.example.arthasExample.TestController

可以看到这条指令不仅可以查看字段的定义和注解,还可以查看线程池的使用情况以及集合内部的value

查看方法列表

这条命令笔者不是很常用,为了教程的完整性笔者也演示一下,假如我们希望查看某个类的方法,可以使用:

sm 类的包路径

以笔者为例,查看TestController的方法为:

 sm com.example.arthasExample.TestController

输出结果如下:

静态变量监控

Arthas提供了对静态变量的分析,以下面这段代码为例,如果笔者希望看到list 内部详情,我们就可以使用ognl表达式获取:

 private static List<String> list = new ArrayList<>();


    @RequestMapping("add")
    public void add() {
        for (int i = 0; i < 10; i++) {
            list.add("val" + i);
        }
    }

在我们执行完接口完成添加操作之后,我们可以直接使用ognl进行监控。例如我们希望查看上述list的内容,我们就可以使用命令:

 ognl '@类的包路径@变量名'

所以如果笔者查看list的命令为:

 ognl '@com.example.arthasExample.TestController@list'

注意,某些场景下使用ongl表达式查看字段信息需要指明classLoaderhashCode,否则会报错,所以我们需要通过sc指令先获取当前类的hashCode:

sc -d com.example.arthasExample.TestController

如下图TestController的类加载器的哈希码为68de145:

于是对应的指令我们可以改为ognl -c 68de145 '@com.example.arthasExample.TestController@list',而输出结果如下,可以看到变量的类型和数值都可以看到:

当然ognl还有一些比较特殊的用法,例如查看集合的长度,添加元素到集合中等操作,具体可以参考GitHub这个issue:

github.com/alibaba/art…github.com/alibaba/art…

基于Arthas进行问题诊断

定位CPU 100%问题

CPU 100%问题算是生产环境下最难排查的问题了,在没有Arthas之前,我们常规的排查思路大致为:

  1. 使用top确定是否为java进程。
  2. 明确是当前java进程之后,通过jps定位java进程号。
  3. 找到最吃资源的线程号。
  4. 将线程号转为十六进制。
  5. 通过jstack导出日志并使用全局搜索十六进制线程号定位到问题代码段。

有了Arthas之后,问题的定位就会简单快速许多,为了演示这个例子,笔者编写了一段模拟CPU 100%问题的代码段:

private static ExecutorService threadPool = Executors.newCachedThreadPool();


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

我们不断请求该地址,不久后你就会发现CPU直接飙升接近100%,此时我们的arthas就派上用场了,首先我们自然是将arthas启动。

java -jar arthas-boot.jar

此时控制台会出现下面几个选项,它通过不同序号标明不同的Java程序,我们看到我们的目标程序ArthasExampleApplication,序号为1,所以我们输入1按回车。

F:\github>java -jar arthas-boot.jar
[INFO] JAVA_HOME: D:\myinstall\jdk8\jre8
[INFO] arthas-boot version: 3.6.9
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 18720 com.example.arthasExample.ArthasExampleApplication
  [2]: 19300 org.jetbrains.jps.cmdline.Launcher
  [3]: 7876 org.jetbrains.idea.maven.server.RemoteMavenServer
  [4]: 14488

进入控制台,我们直接键入thread命令可以看到,有一个名为 pool-1-thread-1的线程CPU使用率非常高,所以我们需要定位它所工作的代码段。

由控制台可知,它的序号为59,所以我们直接键入:

 thread 59

很快的,我们直接定位到了问题代码段,在TestController的42行。

知道了代码的位置之后,我们根据类的包路径com.example.arthasExample.TestController直接通过Arthas反编译查看源码,命令如下:

jad --source-only  com.example.arthasExample.TestController

最终我们定位到了问题代码,即时修复即可:

定位线程死锁问题

对于线程死锁问题,我们也给出下面这样一段示例代码,线程1先取锁1再取锁2,线程2反之,两者取锁顺序构成环路造成死锁。

 //两把锁
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    @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();
    }

了解代码流程之后,我们直接调用这个接口,打开Arthas查看键入thread线程信息可以看到我们的t1和t2两个线程处于等待状态,大概率存在问题。

随后我们直接键入thread -b,发现t2线程被锁住了,由此断定这两个线程看定存在死锁,

由上述结果我们可知两个线程的id分别是65和66,所以使用thread id号的命令直接定位到问题代码段并解决问题即可。

运行耗时性能问题排查

对于统计耗时的操作我们经常会用打日志的方式来监控,在环境复杂的生产环境,我们常因为欠缺考虑而忘记对某些方法进行监控。 同样的Arthas也为我们提供了一些便捷的命令来完成对方法耗时的监控与统计。

这里笔者这里给出一段UserServiceImpl 模拟用户查询时进行参数校验、其他service调用、redis调用、MySQL调用。

@Service
@Slf4j
public class UserServiceImpl {

    public JSONObject queryUser(Integer uid) throws Exception {
        check(uid);
        service(uid);
        redis(uid);
       return mysql(uid);
    }

    public void service(Integer uid) throws Exception {
        log.info("调用其他service。。。。。");
        TimeUnit.SECONDS.sleep(RandomUtil.randomLong(12));

    }

    public void redis(Integer uid) throws Exception {
        log.info("查看redis缓存数据。。。。。");
        TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(100200));
    }

    public JSONObject mysql(Integer uid) throws Exception {
        log.info("查询MySQL数据......");
        TimeUnit.SECONDS.sleep(RandomUtil.randomInt(34));
        JSONObject jsonObject = new JSONObject();
        jsonObject.putOnce("name""xiaoming");
        jsonObject.putOnce("age"18);
        return jsonObject;
    }

    public boolean check(Integer uid) throws Exception {
        if (uid == null || uid < 0) {
            log.error("非法用户id,uid:{}", uid);
            throw new Exception("非法用户id");
        }
        return true;
    }
}

对应的controller代码如下,假如我们在生产环境下发现这个接口非常耗时,我们又没有日志,那么我们如何利用Arthas排查耗时问题呢?

 @Autowired
    private UserServiceImpl userService;

    @GetMapping(value = "/user")
    public JSONObject queryUser(Integer uid) throws Exception {
        return userService.queryUser(uid);

    }

我们可以用trace命令,我们首先用trace追踪一下TestControllerqueryUser耗时的调用:

trace com.example.arthasExample.TestController queryUser

可以发现TestController 并无异常,所有的耗时都在UserServiceImpl :

所以我们再对UserServiceImplqueryUser进行追踪:

 trace com.example.arthasExample.UserServiceImpl queryUser

完成命令键入后,控制台就会阻塞监控这个方法,然后我们调用一下这个接口,可以发现原来是MySQL查询非常耗时,由此我们就可以进一步去推断问题了。

方法耗时统计

有时候我们希望监控某个方法单位时间内请求的耗时和调用情况,我们就可以使用monitor命令,例如我们希望每5s查看TestController 的queryUser的情况,我们就可以键入:

 monitor  -c 5 com.example.arthasExample.TestController queryUser

可以看到控制台会每5s输入请求次数、成功和失败次数以及平均耗时等信息。

定位出入参错误问题

有时候我们希望定位某个日志没有打到的方法的出入参详情,例如上面的mysql()的出入参,我们完全可以通过Arthaswatch方法做到,对应命令为:

watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj}'

可以看到,我们的入参为1,出参是一个对象。

更进一步,假如我们希望可以打印出对象的内容,那么我们就可以使用toString方法做到

watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj.toString()}'

除此之外watch 还支持很多的骚操作,具体可以参考官方文档:

arthas.aliyun.com/doc/watch.h…arthas.aliyun.com/doc/watch.h…

监控方法调用路径

还是以上文mysql方法为例,如果我们希望快速定位到它的调用路径,我们可以使用stack方法:

stack com.example.arthasExample.UserServiceImpl mysql

可以看到详细的调用路径直接输出到控制台。

获取方法调用的过程

我们希望查看方法调用时出现异常的原因,出参和入参时,可以使用tt这条指令,例如我们想查看check方法为何会报错,我们就可以使用tt

tt -t com.example.arthasExample.UserServiceImpl check

从输出结果来看,第二次抛出异常了,我们可以基于1001这个索引去定位问题。

 tt -i 1001

最终可以得出,入参为-1:

如果我们想重新发起调用,可以直接使用:

tt -i 1001 -p

需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。

所以使用完tt指令后,我们建议清除所有的 tt 记录:

tt --delete-all

内存溢出问题

以下面这段代码为例,通过不断发起请求调用,复现OOM问题:

final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1001001, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活

    @RequestMapping(value = "/oom")
    public String oom(HttpServletRequest request) {
        poolExecutor.execute(() -> {
            Byte[] c = new Byte[410241024];
            localVariable.set(c);// 为线程添加变量

        });
        return "success";
    }

然后通过arthas发现老年代内存几乎已满,gc也十分频繁。

由此我们可以直接使用Arthasheapdump 导出文件到mat中进行进一步分析。

 heapdump D://heap.hprof

导出的结果如下,后续我们就可以点击detail推断到问题的源头了。

最终我们很快速的定位到了问题代码:

Arthas的一些进阶操作

线上替换代码

有时候我们测试难免会遗漏一些情况,如下所示,我们业务要求id小于1才抛出异常,但是我们因为粗心而将判断条件写成id<2,结果懵懵懂懂的就将这段代码部到了生产环境,导致业务查询出了问题。

@GetMapping(value={"/user/{id}"})
    public JSONObject findUserById(@PathVariable Integer id) {
        log.info("id: {}",id);
        if (id != null && id < 2) {
             throw new IllegalArgumentException("id < 1");
        }
        return new JSONObject().putOnce("name","user"+id).putOnce("age",18);
    }

对于生产环境,我们肯定是无法立刻重启替换jar包的,对于这类问题,我们完全可以使用arthas实现在线热更新。

首先第一步,我们将生产环境的字节码反编译并导出到本地,如下所示

jad  --source-only com.example.arthasExample.TestController >  d://TestController.java

然后我们修改一下对应的代码段

然后我们需要找到这个类对应类加载器的hash码

sc -d *TestController | grep classLoaderHash

找到对应hash码之后,我们就可以基于这个类加载器将修改后的Java文件编译成字节码文件:

 mc -c 18b4aac2 d://TestController.java -d d://

最后我们将代码热更新到正在运行的程序:

redefine d://com/example/arthasExample/TestController.class

此时我们传1作为参数就不会报错了,说明代码热更新成功了。

获取spring上下文进行进一步分析操作

有时候我们希望在线上获取Spring容器分析定位问题,我们完全可以通过arthas拦截到这个类并进行进一步调用和分析。

读过Spring MVC源码的读者可能都知道,每当又HTTP请求发送到web容器时都会进行请求进行映射转发处理时都会经过RequestMappingHandlerAdapter这个适配器上,查看RequestMappingHandlerAdapter的类图,可以看到它继承了WebApplicationObjectSupport,而该类有一个方法getApplicationContext它就可以直接获得Spring上下文,所以接下来笔者就演示如何通过tt指令获得RequestMappingHandlerAdapter从而调用getApplicationContext拿到上下文,开始各种骚操作。

首先我们使用tt指令对RequestMappingHandlerAdapter 进行追逐,如下所示,笔者通过-t知名要追踪的类的全路径和方法,后续只要有请求调用该方法,tt指令就会显示追踪的信息:

tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod

然后我们随便调用一个接口,得到调用记录:

我们就以索引为1000的调用记录,通过-w指定ognl获取到spring上下文并获取到testController然后完成一个方法调用:

tt -i 1000 -w 'target.getApplicationContext().getBean("testController").findUserById(3)'

如下图,可以看到,我们成功的完成了调用并得到了返回结果。

常见问题

arthas统计方法耗时watch指令的原理是什么

watch指令示例如下:

watch com.example.MyService sayHello "{params, result, throwExp} -> { return 'Exception: ' + throwExp; }" -E

我们从指令即可看出他可以获取到类的全路径,对此我们不拿理解,它就是基于这个类的全路径定位到类的全路径,并对该类型通过字节码桩技术对类的方法进行增强,即插入一段代码进行在方法执行前后插入时间,实现耗时统计:

小结

自此我们对Arthas的常见操作都已演示完成,更多关于Arthas的使用读者可以参考Arthas官网,希望对你有帮助:

arthas.aliyun.com/doc/:arthas.aliyun.com/doc/

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/…

参考

Arthas官网:arthas.aliyun.com/doc/

Arthas基础(一):blog.csdn.net/wfw123123/a…

Arthas - Java 线上问题定位处理的终极利器| 8月更文挑战: juejin.cn/post/699313…

Arthas排查jvm相关问题的利器: www.qinglite.cn/doc/4396647…

新的开始 | Arthas GitHub Star 破万后的回顾和展望:juejin.cn/post/684490…

阿里巴巴Arthas实践--jad/mc/redefine线上热更新一条龙:mp.weixin.qq.com/s?__biz=Mzk…

Alibaba Arthas实践--获取到Spring Context,然后为所欲为:mp.weixin.qq.com/s?__biz=Mzk…

本文使用 markdown.com.cn 排版