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