BTrace 原理淺析

颯然Hang發表於2019-02-27

之前在看agentzh的此篇博文動態追蹤技術漫談時,領會到了動態追蹤技術的強大之處,也一直由於無法在不重啟線上伺服器的情況下排查線上問題在尋找Java中的動態追蹤工具。在公司內部的JavaEE效能檢測框架中,我們使用了asm做位元組碼注入來做線上效能的監測,沿著這個思路,如果要做到動態追蹤應該是需要做位元組碼注入的,但是額外的一點是需要動態載入位元組碼替換掉原有的類的。此外,效能監測框架是需要耦合到業務應用中的,無法做到一個監測工具的靈活性。

後來聽同事提到了BTrace這個工具,於是去嘗試了一下。BTrace是SUN Kenai雲端計算開發平臺下的一個開源專案,旨在為java提供安全可靠的動態跟蹤分析工具。江南白衣的這篇文章calvin1978.blogcn.com/articles/bt…做了比較詳細的描述。

那麼,BTrace這麼神奇的功能是如何實現的呢?既然這是個開源的程式碼,那麼直接從程式碼找原理。BTrace程式碼開源在github.com/btraceio/bt…

總體來說,BTrace是基於動態位元組碼修改技術(Hotswap)來實現執行時java程式的跟蹤和替換。大體的原理可以用下面的公式描述:

Client(Java compile api + attach api) + Agent(指令碼解析引擎 + ASM + JDK6 Instumentation) + Socket複製程式碼

BTrace的入口類在github.com/btraceio/bt…中。在其main方法中,可以看到起最終的核心邏輯是在github.com/btraceio/bt…中。方法呼叫如下:

  • client.compile
  • client.attach
  • client.submit

Client

首先是client.compile方法,使用的是Java compile api,將我們傳遞的java原始檔編譯為.class檔案,當然你如果使用btracec提前編譯了原始碼,那麼這裡就不會有這一步。

針對官方指令碼的一個例子:

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class HelloWorld {
    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    )
    public static void func() {
        println("about to start a thread!");
    }
}複製程式碼

@OnMethod告訴Btrace解析引擎需要代理的類和方法。
這個例子的作用是當java.lang.Thread類的任意一個物件呼叫 start 方法後,會呼叫func方法。

client端在編譯完指令碼之後,進行了一次位元組碼修改,但是僅僅是做了一些相容性,例如域訪問控制器、簡寫等。

接著client.attach中使用java的attach api將agent動態attach到目標jvm程式中(ava agent,通常有兩種方式新增到jvm程式中:動態attach;在目標jvm啟動之前新增agent引數)。

VirtualMachine vm  = VirtualMachine.attach(pid);
...
vm.loadAgent(agentPath, agentArgs);複製程式碼

最後client的submit方法,會向agent傳送監控命令以及傳遞對應code的位元組碼。

Agent

BTrace的agent實現類就在github.com/btraceio/bt…中,具體的實現可以看其main方法,此agent的premain和agentmain方法都是呼叫了這個方法。這裡需要注意的一點:必須要上jdk6,因為jdk5雖然已經有了instrument api,但是其僅僅支援premain方法,也就是僅僅支援在main方法執行之前執行一些動作,而jdk6後加入了agentmain方法和VirtualMachine,是可以在main方法執行後執行的(如果是通過命令列啟動的,那麼agentmain方法不會被呼叫)。此外,在jdk6之前,程式啟動之後是無法再設定boot class載入路徑和system class載入路徑的。而jdk6之後,instrument新增的appendToBootstrapClassLoaderSearch和appendToSystemClassLoaderSearch是可以動態新增classpath的。

agent被提交到目標jvm程式後,首先會新增boot classpath.

...
inst.appendToBootstrapClassLoaderSearch(jf);
...
inst.appendToSystemClassLoaderSearch(jf);複製程式碼

接著開啟一個serversocket等待client的連線。之後client和agent之間的資料通訊,比如生成.class傳送到agent,agent將線上程式列印的資料回傳給
client都是通過socket來進行的。當agent接收到監控命令後,主要有以下兩部分工作:

  • 重寫類:遍歷當前所有的class,根據正則找到匹配的類,用asm重寫
  • 替換類:替換掉原來的class

agent接受到client發來的監控指令以及對應的引數後,會load所有的class,根據正則去匹配指定的類和方法,並使用指令碼解析引擎去處理髮送過來的位元組碼然後使用ASM將指令碼里標註的類java.lang.Thread的位元組碼重寫,植入跟蹤程式碼或新的邏輯。在上面那個例子中,Java.lang.Thread這個類的位元組碼被重寫並在start方法體尾部植入了func方法的呼叫。

BTrace的agent利用instrumentation的retransformClasses方法將原始位元組碼替換掉,使用的transfomer見github.com/btraceio/bt…。如下:

new ClassFileTransformer() {
    public byte[] transform(ClassLoader l, String className, Class c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        // BTrace解析指令碼,利用asm重寫bytecode,然後classLoader載入
    }
}, true);複製程式碼

其中,在agent的agentmain中通過handleNewClient方法啟動一個非同步執行緒進行class transformer,而在這個非同步執行緒中最終是通過呼叫github.com/btraceio/bt…中的retransformLoaded()來進行的。

總結

其實BTrace就是使用了java attach api附加agent.jar,然後使用指令碼解析引擎+asm來重寫指定類的位元組碼,再使用instrument實現對原有類的替換。借鑑這些,我們也完全可以實現自己的動態追蹤工具。

閱讀原文

相關文章