记一次Arthas热更新的使用

2,263 阅读5分钟

这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

背景

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

排查

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

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的使用,只能走繁琐的流程,进行发包替换正式环境的包)

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. 在某个接口的服务层添加一个日志打印信息。
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调用

    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
    
    

总结

​ 使用arthas对生产服务进行不停机的情况下排查问题,省去了一些繁琐的提测流程。并且arthas还提供了很多其他的功能用于生产环境问题的排查,有内存情况的查看、jvm的一些参数、接口的调用链、参数类型等等。