Arthas 是阿里开源的一款 Java 应用诊断工具,可以在线排查问题,动态跟踪 Java 代码,以及实时监控 JVM 状态。这个工具的大名我早有耳闻,之前一直听别人推荐,却没有使用过。最近在线上遇到了一个问题,由于开发人员在异常处理时没有将线程堆栈打印出来,只是简单地抛出了一个系统错误,导致无法确定异常的具体来源;因为是线上环境,如果要修改代码重新发布,流程会非常漫长,所以只能通过分析代码来定位,正当我看着繁复的代码一筹莫展的时候,突然想到了 Arthas 这个神器,于是尝试着使用 Arthas 来排查这个问题,没想到轻松几步就定位到了原因,上手非常简单,着实让我很吃惊。正所谓 “工欲善其事,必先利其器”,这话果真不假,于是事后花了点时间对 Arthas 的各种用法学习了一番,此为总结。

一、Arthas官方文档

二、springBoot整合方式

1、pom文件引入
<dependency>
    <groupId>com.taobao.arthas</groupId>
    <artifactId>arthas-spring-boot-starter</artifactId>
    <version>3.6.7</version>
</dependency>

2、yaml文件引入

arthas:
  # telnetPort、httpPort为 -1 ,则不listen telnet端口,为 0 ,则随机telnet端口
  # 如果是防止一个机器上启动多个 arthas端口冲突。可以配置为随机端口,或者配置为 -1,并且通过tunnel server来使用arthas。
  # ~/logs/arthas/arthas.log (用户目录下面)里可以找到具体端口日志
  telnetPort: -1
  httpPort: -1
  # 127.0.0.1只能本地访问,0.0.0.0则可网络访问,但是存在安全问题
  ip: 127.0.0.1
  appName: arthas_test
  # 默认情况下,会生成随机ID,如果 arthas agent配置了 appName,则生成的agentId会带上appName的前缀。
  agent-id: hsehdfsfghhwertyfad
  # tunnel-server地址
  tunnel-server: ws://127.0.0.1:7777/ws

这里建议agent_id提前配置好。后续的控制台连接arthas需要使用。

3、下载arthas-tunnel-server

通过Arthas Tunnel Server/Client 来远程管理/连接多个Agent

1. 下载arthas-tunnel-server-3.6.7-fatjar.jar
https://github.com/alibaba/arthas/releases
2. 运行
windows
java -jar arthas-tunnel-server-3.6.7-fatjar.jar
linux
nohup java -jar arthas-tunnel-server-3.6.7-fatjar.jar > /dev/null 2>&1 &
3.登录查看注册上来的应用
http://127.0.0.1:8080/actuator/arthas 登陆用户名是arthas
密码在arthas tunnel server的日志里可以找到,比如:
Using generated security password: 6e00d3bd-e2b3-4147-b959-63854347cdc1
1. 下载arthas-tunnel-server-3.6.7-fatjar.jar
https://github.com/alibaba/arthas/releases
2. 运行
windows
java -jar arthas-tunnel-server-3.6.7-fatjar.jar
linux
nohup java -jar arthas-tunnel-server-3.6.7-fatjar.jar > /dev/null 2>&1 &
3.登录查看注册上来的应用
http://127.0.0.1:8080/actuator/arthas 登陆用户名是arthas
密码在arthas tunnel server的日志里可以找到,比如:
Using generated security password: 6e00d3bd-e2b3-4147-b959-63854347cdc1

4、启动Arthas Tunnel Server及spring项目




springboot 集成hive 和kerberos springboot集成arthas_spring boot


5、登录Arthas Tunnel Server

输入地址:http://127.0.0.1:8080/并输入agent_id,界面如下图。



springboot 集成hive 和kerberos springboot集成arthas_spring boot_02


6、输入命令进行测试

dashboard,当前系统的实时数据面板



springboot 集成hive 和kerberos springboot集成arthas_github_03


其他命令列表查看命令列表 | arthas

三、常见问题的排查思路

了解了 Arthas 的命令之后,接下来总结一些使用 Arthas 对常见问题的排查思路。

1. 使用 watch 监听方法出入参和异常

相信不少人见过类似下面这样的代码,在遇到异常情况时直接返回系统错误,而没有将异常信息和堆栈打印出来。

@PostMapping("/add")
public String add(@RequestBody DemoAdd demoAdd) {
  try {
    Integer result = demoService.add(demoAdd);
    return String.valueOf(result);
  } catch (Exception e) {
    return "系统错误!";
  }
}

有时候只打印了异常信息 e.getMessage(),但是一看日志全是 NullPointerException,一旦出现异常,根本不知道是哪行代码出了问题。这时,Arthas 的 watch 命令就可以派上用场了:

$ watch com.example.demo.service.DemoService add -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 143 ms, listenerId: 1

我们对 demoService.add() 方法进行监听,当遇到正常请求时: 

$ curl -X POST -H "Content-Type: application/json" -d '{"x":1,"y":2}' http://localhost:8080/add
3

watch 的输出如下: 

method=com.example.demo.service.DemoService.add location=AtExit
ts=2023-09-11 08:00:46; [cost=1.4054ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=2)],
    ],
    @DemoService[
    ],
    @Integer[3],
]

location=AtExit 表示这个方法正常结束,result 表示方法在结束时的变量值,默认只监听方法的入参、方法所在的实例对象、以及方法的返回值。

当遇到异常请求时

$ curl -X POST -H "Content-Type: application/json" -d '{"x":1}' http://localhost:8080/add
系统错误!

watch 的输出如下:

method=com.example.demo.service.DemoService.add location=AtExceptionExit
ts=2023-09-11 08:05:20; [cost=0.1402ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=null)],
    ],
    @DemoService[
    ],
    null,
]

可以看到 location=AtExceptionExit 表示这个方法抛出了异常,同样地,result 默认只监听方法的入参、方法所在的实例对象、以及方法的返回值。那么能不能拿到具体的异常信息呢?当然可以,通过自定义观察表达式可以轻松实现。

默认情况下,watch 命令使用的观察表达式为 {params, target, returnObj},所以输出结果里并没有异常信息,我们将观察表达式改为 {params, target, returnObj, throwExp} 重新监听:

$ watch com.example.demo.service.DemoService add "{params, target, returnObj, throwExp}" -x 2

此时就可以输出具体的异常信息了:

method=com.example.demo.service.DemoService.add location=AtExceptionExit
ts=2023-09-11 08:11:19; [cost=0.0961ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=null)],
    ],
    @DemoService[
    ],
    null,
    java.lang.NullPointerException
        at com.example.demo.service.DemoService.add(DemoService.java:11)
        at com.example.demo.controller.DemoController.add(DemoController.java:20)
    ,
]

观察表达式其实是一个 ognl 表达式,可以观察的维度也比较多,参考 表达式核心变量。 

从上面的例子可以看到,使用 watch 命令有一个很不方便的地方,我们需要提前写好观察表达式,当忘记写表达式或表达式写得不对时,就有可能没有监听到我们的调用,或者虽然监听到调用却没有得到我们想要的内容,这样我们就得反复调试。所以 Arthas 又推出了一个 tt 命令,名为 时空隧道(Time Tunnel)

使用 tt 命令时大多数情况下不用太关注观察表达式,直接监听类方法即可:

$ tt -t com.example.demo.service.DemoService add

tt 会自动地将所有调用都保存下来,直到用户按下 Ctrl+C 结束监听;注意如果方法的调用非常频繁,记得用 -n 参数限制记录的次数,防止记录太多导致内存爆炸: 

$ tt -t com.example.demo.service.DemoService add -n 10

 当监听结束后,使用 -l 参数查看记录列表:

$ tt -l
 INDEX  TIMESTAMP            COST(ms)  IS-RET  IS-EXP  OBJECT       CLASS                    METHOD                   
------------------------------------------------------------------------------------------------------------
 1000   2023-09-15 07:51:10  0.8111     true   false  0x62726348   DemoService              add
 1001   2023-09-15 07:51:16  0.1017     false  true   0x62726348   DemoService              add

 其中 INDEX 列非常重要,我们可以使用 -i 参数指定某条记录查看它的详情:

$ tt -i 1000
 INDEX          1000
 GMT-CREATE     2023-09-15 07:51:10
 COST(ms)       0.8111
 OBJECT         0x62726348
 CLASS          com.example.demo.service.DemoService
 METHOD         add
 IS-RETURN      true
 IS-EXCEPTION   false
 PARAMETERS[0]  @DemoAdd[
                    x=@Integer[1],
                    y=@Integer[2],
                ]
 RETURN-OBJ     @Integer[3]
Affect(row-cnt:1) cost in 0 ms.

 从输出中可以看到方法的入参和返回值,如果方法有异常,异常信息也不会丢了:

$ tt -i 1001
 INDEX            1001                                                                                      
 GMT-CREATE       2023-09-15 07:51:16
 COST(ms)         0.1017
 OBJECT           0x62726348
 CLASS            com.example.demo.service.DemoService                                                      
 METHOD           add
 IS-RETURN        false
 IS-EXCEPTION     true
 PARAMETERS[0]    @DemoAdd[
                      x=@Integer[1],                                                                        
                      y=null,
                  ]
                        at com.example.demo.service.DemoService.add(DemoService.java:21)
                        at com.example.demo.controller.DemoController.add(DemoController.java:21)
                        ...
Affect(row-cnt:1) cost in 13 ms.

tt 命令记录了所有的方法调用,方便我们回溯,所以被称为时空隧道,而且,由于它保存了当时调用的所有现场信息,所以我们还可以主动地对一条历史记录进行重做,这在复现某些不常见的 BUG 时非常有用: 

$ tt -i 1000 -p
 RE-INDEX       1000
 GMT-REPLAY     2023-09-15 07:52:31
 OBJECT         0x62726348
 CLASS          com.example.demo.service.DemoService
 METHOD         add
 PARAMETERS[0]  @DemoAdd[
                    x=@Integer[1],
                    y=@Integer[2],
                ]
 IS-RETURN      true
 IS-EXCEPTION   false
 COST(ms)       0.1341
 RETURN-OBJ     @Integer[3]
Time fragment[1000] successfully replayed 1 times.

另外,由于 tt 保存了当前环境的对象引用,所以我们甚至可以通过这个对象引用来调用它的方法:

$ tt -i 1000 -w 'target.properties()' -x 2
@DemoProperties[
    title=@String[demo title],
]
Affect(row-cnt:1) cost in 148 ms.

四、其他使用场景

Arthas 的使用非常灵活,有时候甚至还会有一些意想不到的功能,除了上面这些使用场景,Arthas 的 Issues 中还收集了一些 用户案例,其中有几个案例对我印象很深,非常有启发性,可供参考。

  • 使用 stack 命令定位 System.exit/System.gc 的调用来源:https://github.com/alibaba/arthas/issues/20
  • 使用 sc 和 jad 排查 NoSuchMethodError 问题:https://github.com/alibaba/arthas/issues/160
  • 使用 redefine 修改 StringBuilder.toString() 定位未知的日志来源:https://github.com/alibaba/arthas/issues/263
  • 使用 trace javax.servlet.Servlet/Filter 排查 Spring Boot 应用 404/401 问题:https://github.com/alibaba/arthas/issues/429
  • 使用 tt 定位 Java 应用 CPU 负载过高问题:https://github.com/alibaba/arthas/issues/1202
  • 使用 profiler 做复杂链路分析,排查性能问题:https://github.com/alibaba/arthas/issues/1416
  • 使用 trace 命令将接口性能优化十倍:https://github.com/alibaba/arthas/issues/1892