0%

Arthas的基本使用

[TOC]

背景

​ 最近正式环境出现一起事故,业务端经过一些列的业务逻辑之后,使用wkhtmltopdf工具进行html转换为pdf,但出现生成完成之后(正常结束,并未发生异常)业务端再次获取这个pdf文件时,出现文件不存在问题。

排查

​ 经过一系列的排查,最终锁定在wkhtmltopdf工具是否正常生成pdf文件,请看如下代码,这里通过wkhtmltopdf提供的工具类pdf.saveAsDirect(ftpDir + "/" + path);进行pdf的生成,但并未对返回的结果进行判断,所以猜测这里根本没有生成pdf文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean uploadToPdfDirect(WrapperConfig config, String sourceString, String path) {
try {
Pdf pdf = new Pdf(config);
pdf.setAllowMissingAssets();
pdf.addPageFromString(sourceString);
pdf.addParam(pageInfo);
pdf.saveAsDirect(ftpDir + "/" + path);
} catch (IOException | InterruptedException e) {
log.error("wkHtmlToPdf发生异常:{}", e.getMessage());
// 捕获到InterruptedException异常后恢复中断状态
Thread.currentThread().interrupt();
throw new RuntimeException("wkHtmlToPdf发生异常:" + e.getMessage());
}
return true;
}

根据猜测希望将代码修改为:(本想借助Arthas将代码修改以下代码,但由于线上一直产生数据,事故面积越来越大,来不及研究Arthas的使用,只能走繁琐的流程,进行发包替换正式环境的包)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private boolean uploadToPdfDirect(WrapperConfig config, String sourceString, String path) {
try {
Pdf pdf = new Pdf(config);
pdf.setAllowMissingAssets();
pdf.addPageFromString(sourceString);
pdf.addParam(pageInfo);
// 对生成的结果进行判断, 到底有没有生成对应文件
File file = pdf.saveAsDirect(ftpDir + "/" + path);
if(!file.exists()){
throw new BusinessException("文件生成失败");
}
} catch (IOException | InterruptedException e) {
log.error("wkHtmlToPdf发生异常:{}", e.getMessage());
// 捕获到InterruptedException异常后恢复中断状态
Thread.currentThread().interrupt();
throw new RuntimeException("wkHtmlToPdf发生异常:" + e.getMessage());
}
return true;
}

正题

​ 经过漫长的发版流程等待,等到正式环境替换包已经是大半夜。经过测试,确实是pdf文件未生成(如果会使用Arthas,这个时候应该就已经解决问题)。这里要说明下,到目前为止只知道这种方式会生成失败,至于在什么情况下生成失败就不得而知了,因为开发环境、测试环境、预发布环境都是正常生成。痛定思痛,决定学习一下Arthas的使用。

1.使用

简单的使用流程:

  1. 找到需要进行添加代码的类全路径后进行反编译到某个文件夹中。

    jad –source-only com.xxx.service.v3.xxxService > /tmp/xxxService.java

    这里是由.class反编译为.java文件,所以阅读上没有那么友好,其实刚出现问题的时候就想到使用Arthas,但是被这个反编译出来的代码劝退了,反编译出来的代码甚至有指令重排,不太敢修改这个反编译文件。

  2. 修改这个反编译文件

    vim /tmp/UserController.java

    这里实际上是要先退出arthas,第一次使用的时候还以为arthas命令行可以直接操作文件,实际上应该退出arthas使用linux命令的方式进行文件修改。

  3. 查找这个类对应的类加载器

    sc -d *xxxService | grep classLoadHash

    classLoadHash 6bc26251

    这里得到这个类的加载器的哈希值为6bc26251

  4. 使用这个类加载器将修改后的文件编译为.class文件

    mc -c 6bc26251 /tmp/xxxService.java -d /tmp

    这时会在/tmp文件夹下生成一个以包结构为文件路径的.class文件。

  5. 进行热更新

    redefine /tmp/com/xxx/service/v3/xxxService.class

    当看到提示redefine success, size: 1说明替换成功,就可以进行具体的测试。

以上这种方式,出现问题的概率极大,因为要修改反编译文件,且反编译文件好像进行了一些指令重排,导致阅读上比较困难,实际上以上的前四步骤就是为了得到修改之后的.class文件,那么实际上我们可以借助于idea进行处理。

  1. 找到线上代码的标签,拉取修改文件分支。
  2. 检出这个分支,进行特定文件的修改后,直接使用idea工具进行文件的编译。这时就可以通过target文件夹获取到对应修改文件的.class文件。
  3. 将这个.class文件上传到对应服务器。
  4. 直接热更新这个文件。(也就是上述的第5步)

2.示例

  1. 在某个接口的服务层添加一个日志打印信息。
1
2
3
4
public RequireFileVo getFiles(ParamVo ParamVo, Integer pageNum, Integer pageSize) {
log.info("pageSize:{}", pageSize);
// 以下为很复杂的业务逻辑
}
  1. 通过idea对整个服务进行编译后,获取到该文件的.class文件。

  2. 上传到服务器中,由于服务是运行在docker中,所以上传到服务器后移动文件到docker与宿主机的挂载目录中。

  3. 从挂载目录移动到简单目录。(测试的时候使用的挂载目录路径比较长,就移动到简单一点的目录比如/tmp)

  4. 使用redefine命令后看到redefine success, size: 1表示成功

  5. 测试,通过postman调用

    1
    2
    2021-11-18 10:43:30.491 [TraceId=92aeb19c3e8ce62b,SpanId=92aeb19c3e8ce62b,ParentSpanId=] [http-nio-20065-exec-5] INFO  c.b.g.b.s.v.s.xxxService-pageSize:10

其他基础功能

  • reset:重置增强类,此命令会将arthas增强过的类都重置为增强之前的情况。
  • quit:退出,quit只是退出archassession,实际上目标进程arthas server还在运行中。
  • stop:彻底退出arthas,使用此命令后,实际上会执行一次reset命令。

注意:要区别于quitstop两个退出之间的区别。

通过线程id查看线程的具体信息,比如查看线程id为1的线程的具体信息:thread 1

1.watch:查看类里面的某个方法的返回值和入参(实际上只能看参数类型, 是否有值, 并不能看值是多少)

命令 + 类完全限定名 + 需要检测的方法 + 表达式 + (额外参数)

watch com.xxx.xxx.service.v3.xxxService getFiles “{params, returnObj}” -x 2

1
2
3
4
5
6
额外参数:
-x 指定输出结果(默认为1) 这里为2
-b 表示在方法调用之前
-e 表示在方法出异常时
-s 表示在方法返回之后
-f 表示在方法结束之后(正常返回和异常返回)
1
2
3
4
5
6
7
8
9
10
11
表达式核心变量列表:
loader 本次调用类所在的 ClassLoader
clazz 本次调用类的 Class 引用
method 本次调用方法反射引用
target 本次调用类的实例
params 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组
returnObj 本次调用返回的对象。当且仅当 isReturn==true 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 void,则值为 null
throwExp 本次调用抛出的异常。当且仅当 isThrow==true 成立时有效,表明方法调用是以抛出异常的方式结束。
isBefore 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时 isBefore==true 成立,同时 isThrow==false 和 isReturn==false,因为在方法刚开始时,还无法确定方法调用将会如何结束。
isThrow 辅助判断标记,当前的方法调用以抛异常的形式结束。
isReturn 辅助判断标记,当前的方法调用以正常返回的形式结束。

2.trace:方法内部的调用路径,并输出方法路径上的每个节点耗时

命令 + 类完全限定名 + 需要检测的方法

trace com.xxx.xxx.service.v3.xxxService getFiles

3.heapdump:生成快照信息

heapdump (–live) 生成文件路径/xxx.hprof

–live:只dump活着的对象

我是使用jdk自带的jvisualvm来打开xx.hprof文件进行分析。jdk自带的jvisualvm~/home/bin目录下。顺便记录下mac系统查看jdk home所在位置的方式:执行/usr/libexec/java_home -V

4.sysprop:查看Sysetm properties信息

可以指定单个

sysprop @appId

或者使用grep进行过滤

sysprop | grep @appId

再或者直接添加一个新的键值对

sysprop timeout 50

5.sysenv:查看环境变量(使用上和sysprop一致)

注意:系统变量通过System.getProperty(),环境变量通过System.getEnv()。系统变量可以通过-D的方式在启动时添加,环境变量

6.jvm:可以查看jvm虚拟机的一些信息

7.sc:可以查看已经加载的类,如果类是接口,可以获取到已加载的所有实现类,通过-d参数可以打印类加载的具体信息。并且支持通配符查找类。

sc -d xxx

8.sm:查找具体的函数

最后

文档地址[https://arthas.aliyun.com/doc/]

git地址(https://github.com/alibaba/arthas)

使用教程地址(https://arthas.aliyun.com/doc/arthas-tutorials.html?language=cn&id=arthas-advanced)

-------------本文结束感谢您的阅读-------------