Arthas | 熱更新線上程式碼
- 2020 年 2 月 26 日
- 筆記
前言
本文是我介紹 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 函數,負責啟動進程,並循環調用
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 之後,執行
$ jad --source-only moe.cnkirito.arthas.demo.HelloService > /tmp/HelloService.java
將位元組碼文件輸出到指定的位置,查看其內容,與示例中的源碼內容一致:
/* * 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 的累加器資訊
$ sc -d moe.cnkirito.arthas.demo.HelloService
輸出:
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 記憶體編譯
$ mc -c 18b4aac2 /tmp/HelloService.java -d /tmp Memory compiler output: /tmp/moe/cnkirito/arthas/demo/HelloService.class
使用 -c
指定類載入器的 hash 值。編譯完成後,/tmp 目錄下會生成對應的 class 位元組碼文件
redefine 熱更新程式碼
$ redefine /tmp/moe/cnkirito/arthas/demo/HelloService.class
檢查結果
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