Arthas | 热更新线上代码
前言
本文是我介绍 Arthas 系列文章的第一篇。
一般线上问题比开发环境的问题更难解决,一个主要的原因便在于开发态可以任意 debug 断点调试,而线上环境一般不允许远程调试,所以在实践中,我一般习惯用 Arthas 来定位线上的问题。
Arthas 是阿里巴巴开源的 Java 应用诊断利器
Arthas 可以完成很多骚操作,今天给大家介绍的 Arthas 诊断技巧便是 – 热更新线上代码。在生产环境热更新代码,并不是很好的行为,可能会引发一些问题
- 黑屏化的操作可能会导致误操作
- 不符合安全生产的规范,不满足可监控、可回滚、可降级
但有时候也有一些场景可以考虑使用 Arthas 来热更,例如开发环境无法复现的问题、找到修复思路后临时验证等。
本文以 Arthas 3.1.7 版本为例,主要使用到 jad
/mc
/redefine
三个指令。
示例
在 arthas-demo 示例中,一共有两个类,一个 HelloService 类,sayHello 方法负责不断的打印 hello world
:
1 | public class HelloService { |
HelloService 用于模拟我们日常开发的一些业务 Service,另外还有一个 Main 函数,负责启动进程,并循环调用
1 | public class Main { |
需求
假设这段代码运行在线上,我们希望通过 Arthas 将 hello world
的输出更改为 hello arthas
。
Arthas 修改热更的逻辑主要分为三步:
- jad 命令反编译出内存中的字节码,生成 class 文件
- 修改代码,使用 mc 命令内存编译新的 class 文件
- redefine 重新加载新的 class 文件
从而达到热更新的效果
jad 反编译
当挂载上 Arthas 之后,执行
1 | jad --source-only moe.cnkirito.arthas.demo.HelloService > /tmp/HelloService.java |
将字节码文件输出到指定的位置,查看其内容,与示例中的源码内容一致:
1 | /* |
命令中 --source-only
的含义为,只输出源码部分,如果不加这个参数,在反编译出的内容头部会携带类加载器的信息:
1 | ClassLoader: |
在服务器上可以直接使用 vi 等编辑器对源码进行编辑。将 hello world
改为 hello arthas
,为下一步做准备。
sc 查找类加载器
mc 命令编译文件需要传入该类对应类加载器的 hash 值,需要先使用 sc 命令查看 HelloService 的累加器信息
1 | sc -d moe.cnkirito.arthas.demo.HelloService |
输出:
1 | class-info moe.cnkirito.arthas.demo.HelloService |
最后一行 classLoaderHash
即为 HelloService 的类加载器 hash 值。
Arthas 支持 grep,你也可以简化该操作为:
sc -d moe.cnkirito.arthas.demo.HelloService | grep classLoaderHash
mc 内存编译
1 | mc -c 18b4aac2 /tmp/HelloService.java -d /tmp |
使用 -c
指定类加载器的 hash 值。编译完成后,/tmp 目录下会生成对应的 class 字节码文件
redefine 热更新代码
1 | redefine /tmp/moe/cnkirito/arthas/demo/HelloService.class |
检查结果
1 | hello world |
热更新成功
常见问题
redefine 使用限制
不允许新增或者删除 field/method
会出现类似下面的提示
1
redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
运行中的方法不会立刻生效,会在下一次进入该方法时才能生效。
很好理解,并发问题
mc 常见问题
mc 命令有可能失败
因为运行时环境和编译时环境的 JDK 可能有版本差异,mc 可能会失败。如果编译失败可以在本地编译好
.class
文件,再上传到服务器当存在内部类时,一次会生成多个 class 文件
1
2
3
4
5
6
7
8
9
10
11
12public class HelloService {
public void sayHello() {
Inner.test();
}
public static class Inner {
public static void test() {
System.out.println("hello inner");
}
}
}执行 mc
1
2
3
4mc -c 18b4aac2 /tmp/HelloService.java -d /tmp
Memory compiler output:
Inner.class
/tmp/moe/cnkirito/arthas/demo/HelloService.class注意 redefine 时也可以同时传入多个入参
1
2redefine /tmp/moe/cnkirito/arthas/demo/HelloService$Inner.class /tmp/moe/cnkirito/arthas/demo/HelloService.class
redefine success, size: 2
参考文章
https://blog.csdn.net/hengyunabc/article/details/87718469
Arthas 钉钉交流群
Arthas | 热更新线上代码