最近几天公司的服务器经常出现接口调用超时的情况,接口的状态一直是 pending 的状态,起初以为是程序的堆栈空间爆掉了,重启了项目并不好使,发现问题并没有这么简单,好在之前自己用过 arthas,想起了这个工具,于是打算重拾 arthas 完成这次的 bug 排查。

首先去官网下载了最新的 arthas,因为我用的 java 程序启动的,直接把整个包拖到服务器中,再使用 java -jar 启动起来就可以。之后会出现一排的 java 服务,前面会有很多序号,我们选择需要调试的 java 程序序号就可以进入该程序的内部,准备开始调优。

进入到下面这个页面就表示我们成功使用 arthas 连接到了 java 程序内

image-20230505212436246

arthas 官网命令列表

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

我先说几个常用的命令

  1. dashboard 查看控制台,这个命令可以让我们快速的浏览目前的程序情况,包括线程的状态,新生代老年代以及运行 java 程序的系统信息等等

    image-20230505212919528

  2. jvm 查看 jvm 的信息,这里信息太多只截了部分,例如线程部分从上到下依次是活跃数、守护线程活跃数、曾经的最大活跃数、总启动线程次数、当前死锁数

  3. thread 查看线程信息 使用 thread -all 查看所有线程信息。

    thread -b 查看当前阻塞其他线程的线程,个人认为这个很有用,尤其是发生死锁的时候。thread -n 3 打印出当前最忙的前 3 个线程。thread ‘线程 ID’ 查看该线程信息

    image-20230505213755720

  4. jad 反编译字节码文件 这个对于当前看不了源码的人还是很友好的,只要知道错误的类路径 使用 jad 类路径就可以查看类的源码

    image-20230505214102760

  5. heapdump 生成 hprof 文件 这个就不多解释了 内存溢出或泄漏肯定要看的文件

  6. sc -d 类名 例如 sc -d com.sora.system.Admin 则可以获取到 Admin 这个类的信息 -d 参数则表示

    输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的 ClassLoader 等详细信息。如果一个类被多个 ClassLoader 所加载,则会出现多次

    image-20231024143335156

继续说回线上排查 bug 的问题,因为公司是内网开发,只能文字描述😶。首先是用了 dashboard 命令,大致看了一下目前的堆栈情况,以及 gc 次数。发现没有异常。继续使用 thread -all 查看所有线程,果不其然,发现有好几个线程是阻塞的,为 BLOCKED 状态。继续使用 jvm 查看 jvm 信息,发现有 3 个死锁。有死锁就好办了,直接用 thread -b 找到死锁的根源,发现指向一个日志收集器,里面有一个 OpenFeign 远程调用的地方发生了死锁。至此,成功发现死锁的根源。

线上调试示例

这里我推荐安装一个插件,可以直接复制 arthas 的命令,就不需要记过多繁琐的命令了。

image-20231024143831485

动态执行线上方法并获取结果

有时我们想直接获取线上的方法返回结果,但本地没有环境,线上我们又没有账户去实时测试,这个时候我们可以使用 vmtool 命令。

这里我模拟出我们常用的三个情况,分别是无参的 get 请求、有多个参数的 get 请求、传递对象的 post 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@GetMapping("select")
public Result selectUserList() {
List<Employee> employeeList = userMapper.selectList(Wrappers.lambdaQuery(Employee.class));
return Result.success(employeeList);
}

@GetMapping("select/{id}/{id2}")
public Result selectUserById(@PathVariable("id") Integer id,@PathVariable("id2") Integer id2) {
Employee employee = userMapper.selectById(id);
Employee employee2 = userMapper.selectById(id2);
return Result.success(Lists.newArrayList(employee2,employee));
}

@PostMapping("select/obj")
public Result selectUserByObcj(@RequestBody Employee employee) {
// 制定查询规则
LambdaQueryWrapper<Employee> wrapper = Wrappers.lambdaQuery(Employee.class)
.eq(Employee::getName, employee.getName())
.eq(Employee::getId, employee.getId());
List<Employee> employeeList = userMapper.selectList(wrapper);
return Result.success(employeeList);
}

首先我们先来说 get 请求,get 请求很简单,我们直接在想要请求的方法名字上右键(注意必须是方法名上才会有提示)

image-20231024144224656

这个界面就是 vmtool 命令的配置界面,其中下面的 classloader 是我们传递对象参数时用的,get 请求使用不到的,我们直接点击右下角复制命令。

image-20231024144248140

这里我们对命令进行一些简单的介绍,首先是 - x 表示结果的展开层次,默认是 1,这里我们改为 3,可以看到更多信息;-className 则是方法所在类

1
vmtool -x 3 --action getInstances --className com.sora.controller.UserController  --express 'instances[0].selectUserList()' 

在 arthas 界面直接粘贴执行,就可以获得这个方法的返回值

image-20231024144358687

多个参数的 get 请求也十分简单,我们直接复制命令,可以发现只是在方法参数中多了 2 个参数,我们直接将对应的占位符替换成我们的具体参数就可以,下面的命令表示查询 id 为 1 和 2 的员工对象。

1
vmtool -x 3 --action getInstances --className com.sora.controller.UserController  --express 'instances[0].selectUserById(1,2)'

最后重点说一下 post,因为 post 请求需要传递参数,我们需要先获取到能表示参数唯一的 classLoaderhash。这里我的参数是 Employee 类型的对象,所以我需要使用 sc 命令获取到该对象的 classLoaderhash。

1
sc -d 类路径

image-20231024145341523

之后我们就获取到了 post 的命令,其中对象具体属性的赋值又分为两种,分别是全参构造方法赋值和 set 赋值。前者适合用于字段少且每个字段的值均使用到的情况,后者则更多用于从对象中抽取部分字段来使用。(当然你也可以在类中专门写一个只赋值部分字段的方法)

1
vmtool -x 3 --action getInstances --className com.sora.controller.UserController  --express 'instances[0].selectUserByObcj(new com.sora.domain.user.Employee())'  -c 1055e4af

首先我们来看第一种全参构造赋值,需要给所有的字段指定值

1
vmtool -x 3 --action getInstances --className com.sora.controller.UserController  --express 'instances[0].selectUserByObcj(new com.sora.domain.user.Employee(19,"19",null,null,null,null,null,null,null,null,null,null,null,null,null))'  -c 1055e4af

image-20231024153152400

第二种方法则为 set 赋值,我们创建一个空对象 obj,并针对特定字段进行赋值(注意使用括号将整个过程包裹)

1
vmtool -x 3 --action getInstances --className com.sora.controller.UserController  --express 'instances[0].selectUserByObcj((#obj = new com.sora.domain.user.Employee(),#obj.setId(19),#obj.setName("19"),#obj))'  -c 1055e4af

image-20231024153209235