Greys主要實現原理

hoc0113發表於2019-03-02

轉載自:https://www.iflym.com/index.php/code/201801170001.html

greys是一個使用java management tool程式注入javaagent實現線上系統的診斷一個工具。原github為(https://github.com/oldmanpushcart/greys-anatomy),其主要的功能在於系統不停機的情況下。可以檢視系統中的執行緒資訊,cpu使用情況,jmx資訊,以及某個方法在執行時的呼叫棧,呼叫引數等。

一個典型的場景就是線上某個功能出bug,但是系統中並沒有記錄引數資訊,這時候即可通過這個功能注入agent,臨時地列印出這個呼叫方法的引數,以方便定位相應的問題。如無此工具,則只能改程式碼,然後重新上線。在這種情況下,可能出錯的場景就不能再復現。(當然也有其它工具(如log monitor)作到線上系統引數記錄開關的目的,這裡不作描述)

本文主要描述greys是如何工作的,包括如何注入到線上系統,然後指令碼client與注入後server端的互動,以及如何實現一個簡單的引數攔截記錄功能。從整個實現機制層面描述其工作原理。

 

1. 將agent注入到線上系統中

自java 6之後,jvm提供了一系列的tools工具,用於與jvm進行互動。其中在attach中,即可以通過相應的api拿到本地上正在執行的jvm例項,進而獲取一系列的資料資訊。一個簡單的操作如下所示:

01

02

03

04

05

06

07

08

09

10

11

12

public static VirtualMachine findByPid(String pid) throws Exception {

    List<VirtualMachineDescriptor> virtualMachineList = VirtualMachine.list();

    for(VirtualMachineDescriptor descriptor : virtualMachineList) {

        //這裡的id即與jps中的pid相同,即程式id

        String id = descriptor.id();

        if(Objects.equals(id, pid)) {

            return VirtualMachine.attach(descriptor);

        }

    }

 

    throw new RuntimeException("不能找到虛擬機器:" + pid);

}

如上的程式碼,即可通過pid找到一個目標虛擬機器,然後再通過 VirtualMachine中的相關方法可以拿到相關的資訊,但更主要的即是可以通過此類注入一個新的agent到目標jvm中,然後此agent即可以通過instrument物件進行一系列的操作了。相應的程式碼如下參考所示:

1

2

3

4

5

6

7

8

9

public static void injectAgent(VirtualMachine virtualMachine, String agentPath, String otherConfig) throws Exception {

    //agent path即實際要注入的agent jar的實際地址,如/data/x/agent.jar

    //otherConfig在相應的agent main中,jvm會將此引數傳遞此目標方法引數中

    virtualMachine.loadAgent(agentPath, otherConfig);

}

 

public static void premain(String agentArgs, Instrumentation inst) {

     

}

在上面的程式碼中,通過將agent.jar注入到目標系統,並且通過otherConfig引數進一步提供其它引數,這樣可以進一步的進行一系列操作。
在greys中,實際注入過程如下。

  1. greys整體分為2個jar, 1個為greys-agent.jar,另一個為greys-core.jar,認為greys-agent.jar是注入引導類,core為主要邏輯實現。(後續簡稱agent.jar, core.jar)
  2. 通過loadAgent將agent.jar注入在目標系統,並且傳遞了core.jar的目標地址,以及其它引數
  3. 傳遞給agent.jar的引數分為2部分,一部分即為core.jar路徑資訊,另一部分即為後續agent server啟動時的配置引數
  4. agent解析引數,拿到core.jar的地址資訊,並由此構建一個全新的classLoader,通過classLoader可以拿到此jar中的所有類資訊
  5. 由classLoader構建出全域性單例物件 gaServer,並將instrument物件繫結其中,後續的類掃描,重定義等均通過instrument來引導處理
  6. gaServer開始解析由agent.jar中傳遞過來的配置資訊,並且啟動監聽server,監聽指定埠,準備接收相應的請求

至此注入過程完成。相應的agent.jar和core.jar都已經線上上系統中存在。其中 core.jar由單獨classLoader持有,線上系統感知不到.

2. 指令client與agent server的互動

通過注入過程,相當於線上上系統開了一個後門,並且後門中可以看到重要的資訊,比如通過mxbean拿到線上系統資訊,同時instrument功能可以重定義物件。
整個過程由類GaServer完成,以下詳細描述過程:

  1. 通過GaServer#bind 讀取配置資訊,初始化ServerSocketChannel物件,並繫結ip和埠
  2. 通過一個簡單的nio程式碼(GaServer#activeSelectorDaemon),開啟一個新執行緒,讀取客戶端請求連線和輸入資料
  3. 客戶端通過連線此ip和埠(GreysConsole類實現),併傳送簡單的指令來完成互動過程,一個連線過程稱之為一個Session,其中封裝相應的流資訊以及控制邏輯
  4. 通過定義簡單的通訊協議,一行為一個指令,輸入回車即完成此指令,並等待server返回,然後將此資訊列印在控制檯
  5. 服務端讀取到指令之後(GaServer#doRead),簡單解析(DefaultCommandHandler#executeCommand 和 Commands#newCommand), 並構建出command物件
  6. 通過command物件以及相應的action,根據相應的型別執行不同的action動作,並將響應資訊以及執行結果(封裝為Affect),寫回session動作中,並由簡單的迴圈讀取響應資訊,最終輸入回客戶端中

上述的動作,4-6是可以多次處理的,即客戶端在執行完一個指令之後,可以再輸入下一條指令,繼續執行其它指令。
同時,如上所述,所有的指令均是通過Command完成,並通過全域性掃描自動新增至Commands中,相應的互動實際即線上上系統中執行相應的command指令。

3. agent server線上系統監控

command物件主要的執行過程主要還是通過讀取mxBean和類增強兩大類來完成。其中前者不需要改變線上系統內部行為,後者需要改變線上行為,並對類的執行過程產生影響。

3.1 JvmCommand指令
此指令非常簡單,即列印出線上機器當前的情況,其Action為silentAction,即不會改變線上結果,僅簡單返回資料資訊。主要資訊通過ManagementFactory拿到各個mxBean,從中獲取到如類載入資訊,gc資訊,記憶體資訊,作業系統資訊等,然後進行資料組合,最後封裝為TTable物件,展現為一個控制檯表格形式的資料。直接返回即可。

3.2 TraceCommand指令

此指令用於輸出單個方法在呼叫時,此方法的程式碼所訪問的每一個方法的呼叫資訊,主要包括耗時資訊。其輸出如下參考所示:

1

2

3

4

5

`---+Tracing for : thread_name="http-nio-8080-exec-9" thread_id=0xa9;is_daemon=true;priority=5;

    `---+[1,1ms]xxx.YController:zzz()

        +---[1,0ms]org.springframework.web.servlet.ModelAndView:<init>(@45)

        +---[1,0ms]org.springframework.web.servlet.ModelAndView:addObject(@47)

        `---[1,0ms]org.springframework.web.servlet.ModelAndView:setViewName(@48)

其實現機制即通過重定義要進行監控的類,然後在執行過程中列印相應的呼叫過程。整個過程詳細如下:

  1. 通過client指令(如 trace abc.* methodA)中解析出類資訊,這裡的類資訊為一個類正規表示式,可以匹配多個
  2. 通過之前儲存的instrument物件拿到線上所有已載入物件(instrument#getAllLoadedClasses), 與類匹配資訊進行匹配,提出所要增強的類
  3. 同時,通過增強類列表,再通過方法正則匹配表示式,找到需要監控的方法列表,並同形成 Map<Class<?>, Matcher<AsmMethod>> 物件,即這些類的這些方法需要處理
  4. 通過這些資料以及增強類command所提供action,組合形成一個類增強器Enhancer(使用asm進行程式碼增強)。 此增強器實現了ClassFileTransformer介面,即可通過instrument#addTransformer新增到線上系統中
  5. 呼叫instrument#retransformClasses 重新轉換這些指定的類,通過之前的匹配,可以避免增強不必要的類,這樣對線上系統的影響最小
  6. 在相應的enhance實現中,通過反向勾子,最終利用asm,在整個方法指令集中,通過監控 執行方法前 呼叫三方方法前, 呼叫三方方法呼叫後, 呼叫三方方法異常後, 執行方法後 這些相應的點,並反向呼叫相應command提供的監聽器。 這裡為TraceCommand提供的AdviceListener物件
  7. 在監聽器中,即記錄起相應的三方方法資訊,儘可能啟動更多詳細的資訊,並通過一個簡單的樹狀資訊元件 TTree 將這些資訊收集在一起(具體可參考相應程式碼實現)
  8. 最終方法執行後,將此執行資訊寫回session,即完成一次trace過程

TraceCommand指令主要使用了instrument中的幾個關鍵方法,如getAllLoadedClasses返回所有已載入類,監控的類即從這些類中查詢。 retransformClasses重轉換類。這些方法都是由jvm自身提供,不需要通過classLoader來中間跳轉,並且完成不影響jvm原資訊邏輯。 

同時,轉換類,即類增強使用了asm,通過在位元組碼層面處理儘可能多的一些資訊,增強這些資訊,以拿到線上資訊。當然也可以使用javassist(據說greys早期即使用javassist)

值得注意的是,因為每一次增強相當於對線上系統類進行了一次處理,為避免可能對後續執行產生影響,在完成一次類增強之後,greys通過removeTransformer將此次轉換器移除掉了,這樣此次指令的增強將在下一次指令的增強時失效, 這樣可以避免多次相同作用增強。避免出現奇怪的問題.

最後

至此,整個greys的實現分析完畢。整個實現使用到了一些關鍵的技術,列舉如下:

  1. jvm tools attach,用於連線線上jvm資訊
  2. classLoader,用於隔離類資訊
  3. javaagent & instrument, 用於提供類轉換過程, 修改載入類位元組碼
  4. asm & bytecode, 用於位元組碼處理,以方便對類進行增強及處理

其中classLoader,instrument和位元組碼在日常的關鍵開發中,均起到重要的作用。這些技術有助於技術能力的提升。