Bistoury原理解析
今天想和大家聊聊Java中的APM
,簡單介紹Java中的Instrumentation
技術,然後重點分析bistoury
的實現原理
Instrumentation
即Java探針技術,通過Instrumentation
,開發者可以構建一個獨立於應用程式的代理程式(Agent),用來監測和協助執行在JVM上的程式,甚至能夠替換和修改某些類的定義而對業務程式碼沒有侵入,主要場景如APM,常見的框架有:SkyWalking
、Pinpoint
、Zipkin
、CAT
,arthas
和bistoury
其實也算吧。
推薦一篇部落格:Instrumentation
靜態Instrumentation
從JDK1.5開始支援
Agent邏輯在main方法之後執行,兩個關鍵方法:
// 優先順序高
public static void premain(String agentOps,Instrumentation instrumentation);
public static void premain(String agentOps);
複製程式碼
通常agent的包裡面MATE-INF目錄下的MANIFEST.MF中會有這樣一段宣告
Premain-Class: Agent全類名
複製程式碼
在啟動應用的時候,新增Agent引數觸,Agent邏輯在main方法之後執行
java -javaagent:agentJar.jar="Hello World" -jar agent-demo.jar
複製程式碼
動態Instrumentation
從JDK1.6開始支援
Agent邏輯在main方法之後執行,兩個關鍵方法:
// 優先順序高
public static void agentmain(String agentArgs,Instrumentation inst);
public static void agentmain(String agentArgs);
複製程式碼
可以動態觸發,通過VirtualMachine這個類attach到對應的JVM,然後執行VirtualMachine#loadAgent
方法
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentJar路徑);
複製程式碼
在程式執行的過程中,可以通過 Instrumentation API 動態新增自己實現的 ClassFileTransformer
Instrumentation#addTransformer(ClassFileTransformer)
複製程式碼
ClassFileTransformer
An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM
代理程式(即自己的Agent)提供實現類,用於修改class檔案,該操作發生在 JVM 載入 class 之前。它只有一個transform
方法,實現該方法可以修改 class位元組碼,並返回修改後的 class位元組碼,有兩點要注意:
- 如果此方法返回null,表示我們不對類進行處理直接返回。否則,會用我們返回的byte[]來代替原來的類
- ClassFileTransformer必須新增進Instrumentation才能生效 Instrumentation#addTransformer(ClassFileTransformer)
- 當存在多個Transformer時,一個Transformer呼叫返回的byte陣列將成為下一個Transformer呼叫的輸入
byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException;
複製程式碼
例如
// 定義一個 ClassFileTransformer
public abstract class Transformer implements ClassFileTransformer {
private static final Logger logger = BistouryLoggger.getLogger();
@Override
public byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {
try {
if(className.equals(xxxx)){
通過ASM修改位元組碼,並返回修改後的位元組碼
}
return null;
} catch (Throwable e) {
logger.error("","transform failed","Classs: {},ClassLoader: {} transform failed.",className,loader,e);
}
}
}
// 新增一個Agent JDK1.5
public static void premain(String agentArgs,Instrumentation inst) {
inst.addTransformer(new Transformer());
}
// 觸發Agent JDK1.5
java -javaagent:/agent.jar="傳遞的引數" -jar test.jar
// 新增一個Agent JDK1.6
public static void agentmain (String agentArgs,Instrumentation inst) {
inst.addTransformer(new Transformer());
}
// 觸發Agent JDK1.6
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("agent.jar");
複製程式碼
ASM
有關於ASM的簡單介紹,推薦兩篇部落格:ASM訪問者模式、ASM使用
- ClassReader: 資料結構。將位元組陣列或者class檔案讀入到記憶體當中,並以樹的資料結構表示,樹中的一個節點代表著class檔案中的某個區域
- ClassVisitor: 操作。呼叫
ClassReader#accept
方法,傳入一個ClassVisitor
物件,在ClassReader
中遍歷樹結構的不同節點時會呼叫不同ClassVisitor
物件中的visit
方法,從而實現對位元組碼的修改 - ClassWriter: 操作。
ClassWriter
是ClassVisitor
的實現類,它是生成位元組碼的工具類, 將位元組 輸出為 byte[],在責任鏈的末端,呼叫ClassWriter#visitor
進行修改後的位元組碼輸出工作
JMX
JMX(Java Management Extensions)是一個為應用程式植入管理功能的框架。JMX是一套標準的代理和服務,實際上,使用者可以在任何Java應用程式中使用這些代理和服務實現管理
說的有點抽象,推薦一篇部落格 JMX
我自己的理解,JMX分為Server
和Client
,MBean
是它的核心概念
- Server:
MBean
的容器,負責管理所有的MBean
,同時我認為它就是一個Agent程式,在Java應用啟動的時候自己啟動。讓我不太明白的是,為什麼通過jps
命令不能看到這個程式呢? - Client: 即客戶端,可以和
Server
建立連線,常見的客戶端有:jvisualvm、jconsole、自己小工具 - MBean: JMX裡面的一個概念,可以通過自定義
MBean
做一些事情,動態改改屬性值啥的,也就是說,JMX只認識MBean
,不認識別的。基於內建的一些MBean
,可以獲取記憶體、執行緒、系統等指標資訊。所以如果想做一些監控上的事情,可以基於它內建的MBean
Bistoury
去哪兒網開源的一個對應用透明無侵入的Java應用診斷工具,可以讓開發人員無需登入機器或修改系統,就可以從日誌、記憶體、執行緒、類資訊、除錯、機器和系統屬性等各個方面對應用進行診斷,提升開發人員診斷問題的效率和能力。內部集成了arthas,所以它是arthas的超集。其中兩個比較有特色的功能:線上DEBUG、動態監控,就是基於 Instrumentation + ASM 做的。
在開始分析這個框架之前,可以先看看它的整體架構 Bistoury設計檔案
啟動流程
Agent
前置處理
即 bistoury-instrument-agent
模組,這就是Agent,裡面有一個核心類 AgentBootstrap2
,該類同時持有 premain 和 agentmain 方法,並在pom.xml檔案中配置了 Premain-Class
和 Agent-Class
public static void premain(String args,Instrumentation inst) {
main(args,inst);
}
public static void agentmain(String args,inst);
}
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>qunar.tc.bistoury.instrument.agent.AgentBootstrap2</Premain-Class>
<Agent-Class>qunar.tc.bistoury.instrument.agent.AgentBootstrap2</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
複製程式碼
從上可以看出,不管是 premain 和 agentmain 方法,裡面都呼叫了 main 方法,而 main 方法主要負責以下功能
- 類載入器相關,自定義類載入器
- 初始化arthas的
java.arthas.Spy
類,將AdviceWeaver中的各個方法引用賦值給Spy - 初始化bistoury的
qunar.tc.bistoury.instrument.spy.BistourySpys1
類,將GlobalDebugContext
,SnapshotCapture
,AgentMonitor
中的各個方法引用賦值給BistourySpys1,這些方法最總通過ASM方式進行呼叫 - 執行
BistouryBootstrap#bind
方法,啟動一個telnetServer端,所以我們可以通過telnet向其傳送命令
GlobalDebugContext
和線上DEBUG相關,涉及到兩個方法, isHit
和 hasBreakpointSet
,這兩個方法最終通過位元組碼的形式進行呼叫
- isHit: 判斷是否到達斷點
- hasBreakpointSet: 判斷是否已經存在斷點
SnapshotCapture
儲存實 屬性
、靜態屬性
、區域性變數
、方法呼叫堆疊
等資訊,最後返回將這些資訊返回給前端,涉及的方法列表如下:
- putLocalVariable: 儲存區域性變數資訊
- putField: 儲存屬性資訊
- putStaticField: 儲存靜態屬性資訊
- fillStacktrace: 儲存方法呼叫堆疊資訊
- dump: 將上面這些資訊存到DefaultSnapshotStore中
- endReceive: 有點不太懂?
AgentMonitor
AgentMonitor
和動態監控相關,動態監控可以監控方法的呼叫次數、異常次數和執行時間,同時也保留最近幾天的監控資料。而動態監控的實現原理也很簡單,就是在方法執行前後記錄呼叫次數和響應時間,而這部分邏輯就是通過ASM動態插入位元組碼來實現的
- start: 記錄開始時間
- stop: 計算呼叫次數和耗時
- exception : 計算異常數
BistouryBootstrap
上面已經說過,在main方法中會呼叫BistouryBootstrap2#bind
方法,該方法用於啟動一個ShellServer
,這裡指的是TelnetServer
。
這個類參考了ArthasBootstrap
,ArthasBootstrap#bind
方法中,主要啟動了兩個ShellServer
,即: TelnetServer
和HttpServer
, 所以我們在使用arthas的時候可以通過web和telnet方式訪問。
BistouryBootstrap
與ArthasBootstrap
有些不同
-
BistouryBootstrap
只建立了TelnetServer
, 並沒有建立HttpServer
; -
BistouryBootstrap
在arthas
的基礎上實現了一個自己的CommandResolver
,即QBuiltinCommandPack
,該類負責管理所有的Command
,也就是說,從功能上來講,bistoury
是arthas
的超集;
核心bind方法如下,原始碼感興趣的自己看一下
public void bind(Configure configure) throws Throwable {
long start = System.currentTimeMillis();
if (!isBindRef.compareAndSet(false,true)) {
throw new IllegalStateException("already bind");
}
try {
/**
* 涉及到各個 Client 的初始化, 將引數 instrumentation 傳到各個 client 中
*
* JarDebugClient
* AppConfigClient
* QDebugClient
* QMonitorClient
* JarInfoClient
*/
InstrumentClientStore.init(instrumentation);
ShellServerOptions options = new ShellServerOptions()
.setInstrumentation(instrumentation)
.setPid(pid)
.setWelcomeMessage(BistouryConstants.BISTOURY_VERSION_LINE_PREFIX + BistouryConstants.CURRENT_VERSION);
shellServer = new ShellServerImpl(options,this);
QBuiltinCommandPack builtinCommands = new QBuiltinCommandPack();
List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
resolvers.add(builtinCommands);
shellServer.registerTermServer(new TelnetTermServer(
configure.getIp(),configure.getTelnetPort(),options.getConnectionTimeout()));
for (CommandResolver resolver : resolvers) {
shellServer.registerCommandResolver(resolver);
}
shellServer.listen(new BindHandler(isBindRef));
} catch (Throwable e) {
if (shellServer != null) {
shellServer.close();
}
InstrumentClientStore.destroy();
isBindRef.compareAndSet(true,false);
throw e;
}
}
複製程式碼
Main
上面提到的只是前置處理會觸發的邏輯,即Java Instrumentation 會觸發的邏輯,而Agent模組的main方法,其實在是qunar.tc.bistoury.indpendent.agent.Main
中,執行這個方法會觸發以下邏輯
- 根據啟動入參
bistoury.proxy.host
獲取Proxy
地址 - 向
Proxy
傳送一個Http請求,請求地址為proxyIp:9090/proxy/config/
-
Proxy
返回與Agent
建立連線的Ip和埠 - 執行
AgentClient#initNettyClient
方法與Agent
建立TCP連線 - 根據
SPI
載入所有的AgentGlobalTaskFactory
實現類,然後呼叫他們的start
方法 - 開啟一個
失敗重試
的定時任務,每分鐘執行一次
public static void main(String[] args) throws Exception {
log();
AgentClient instance = AgentClient.getInstance();
instance.start();
System.in.read();
}
複製程式碼
至此,Agent的啟動完成。
Proxy
Proxy
的啟動邏輯在qunar.tc.bistoury.proxy.container.Bootstrap#main
方法中,預設Tomcat埠9090
- 獲取配置檔案目錄地址,我們可以在啟動的時候新增一個引數
-Dbistoury.conf=/Workspace/ZTO/forensic/bistoury-proxy/conf
- 啟動內建的Tomcat,整合了Spring
- 在
NettyServerManager
Bean初始化的之前,執行一些初始化操作 - 獲取ZKClient,Proxy的地址會註冊到ZK
- 執行
NettyServerManager#startAgentServer
方法啟動針對Agent的Server端, 處理來自Agent的請求,預設埠為9880
- 執行
NettyServerManager#startUiServer
方法啟動針對UI的Server端, 處理來自UI的Websocket連線,預設埠為9881
UI
UI
的啟動邏輯在qunar.tc.bistoury.ui.container.Bootstrap#main
方法中,預設Tomcat埠9091
- 獲取配置檔案目錄地址,我們可以在啟動的時候新增一個引數
-Dbistoury.conf=/Workspace/ZTO/forensic/bistoury-ui/conf
- 啟動內建的Tomcat,整合了Spring
互動流程
命令的請求過程
UI -> Prosy -> Agent -> Proxy -> UI
複製程式碼
- Proxy 與 UI 維持了一個Websocket連線
- Proxy 和 Agent 維持了一個TCP連線
- 一般我們在前端操作的時候是:前端介面請求UI後臺介面返回 Proxy 的Websocket地址,然後瀏覽器與 Proxy 建立一個Websocket連線
UI傳送請求
以在介面點選檢視主機資訊
為例
- 介面點選檢視主機資訊,請求UI後端的
ConfigController#getProxyWebSocketUrl
介面,入參=agentIp - 從註冊中心(ZK)獲取所有的
Proxy
- 以
agentIp
為入參,請求proxyIP:9090/proxy/agent/get
,此步驟用於判斷agentIp
對應的那個Agent
是否可用 -
Proxy
返回Agent
資訊 -
UI
後後端介面返回前端一個Websocket
地址,瀏覽器和Proxy
通過Websocket
連線ws://10.10.134.174:9881/ws
-
UI
通過Websocket
連線向Proxy
傳送命令 -
Proxy
將命令轉發請求到Agent
-
Agent
收到命令進行邏輯處理,將結果回給Proxy
-
Proxy
將結果返回給UI
UI與Proxy互動
Proxy
接收請求經過 解碼 -> 主機有效性校驗
,最終請求來到UiRequestHandler#channelRead
方法,UiRequestHandler
建構函式包含4個關鍵入參
- CommunicateCommandStore: 預設實現類
DefaultCommunicateCommandStore
,建構函式會注入所有的UiRequestCommand
- UiConnectionStore: 預設實現類
DefaultUiConnectionStore
,維護Channel
和UiConnection
之間的關係,UiConnection#write
方法返回的ListenableFuture
可以添加回調 - AgentConnectionStore : 預設實現類
DefaultAgentConnectionStore
,維護agentIp
和AgentConnection
之間的關係,AgentConnection#write
方法返回的ListenableFuture
可以添加回調 - SessionManager : 預設實現類
DefaultSessionManager
,維護請求Id
和Session
的關係,Session中持有RequestData
AgentConnection
UiConnection
屬性,這是實現請求轉發的關鍵
有關於Session
,下次再重點介紹,它是是實現請求轉發的關鍵
請求流程
- 根據
code(code可以看作是命令的唯一標識)
找到對應的CommunicateCommand
, 然後獲取CommunicateCommand
的CommunicateCommandProcessor
屬性 - 執行
CommunicateCommandProcessor#preprocessor
方法 - 根據
AgentServerInfo
找到對應的AgentConnection
,執行sendMessage
方法,即執行Session#writeToAgent
方法,該方法用於向Agent
傳送命令 - 在回撥中執行
UiConnection#write
方法,用於向UI
返回結果
瀏覽器
與Proxy
建立Websocket
連線的時候,基於Channel
建立一個UiConnection
,然後基於UiConnection
和AgentConnection
建立一個DefaultSession
,AgentConnection
從哪裡來?
AgentConnection
是 Agent
與Proxy
維持心跳時建立的,核心類AgentMessageHandler
、ProxyHeartbeatProcessor
,建立之後快取到DefaultAgentConnectionStore
, key就是agentIp
Proxy與Agent互動
Proxy
- NettyServerManager#startAgentServer
- NettyServerForAgent#start 啟動Server端
- AgentMessageHandler#channelRead0 訊息處理,有3種訊息 ProxyHeartbeatProcessor : 心跳訊息, 在收到心跳的時候,以 agentIp 和 Channel 建立 AgentConnection ,然後 迴應 Agent 的心跳 AgentResponseProcessor : Agent返回的資料,根據請求ID從SessionManager中獲取,然後執行 session#writeToUi 方法,將結果返回瀏覽器 AgentInfoRefreshProcessor : 從DB和配置中獲取最新的Agent資訊,然後返回給 Agent
Agent
- AgentClient#initNettyClient
- AgentNettyClient#start 啟動Client端
- RequestHandler#channelRead 訊息處理,從RemotingHeader獲取code和id,id作為Channel的唯一標識,根據code獲取 Processor
- 執行Processor#process方法,有以下幾種 CancelProcessor : 取消 TaskProcessor 中開啟的任務 HeartbeatProcessor : 心跳 MetaRefreshProcessor : 更新MetaStore裡面的屬性,幹嘛的? MetaRefreshTipProcessor : 更新Agent資訊 TaskProcessor : 處理任務
動態監控功能
- web介面點選新增動態監控按鈕
- 瀏覽器與
Proxy
建立了Websocket
連線,瀏覽器向Proxy
傳送一個指令qmonitoradd
-
Proxy
與Agent
通過Netty建立了TCP連線,Proxy
將命令轉發給Agent
-
Agent
收到訊息,解析指令,通過TelnetClient
與ShellServer
建立telnet連線 -
ShellServer
收到指令,找到對應的Command
,這裡指QMonitorAddCommand
- 執行
QMonitorAddCommand#process
方法,然後執行QMonitorClient#addMonitor
方法,最後執行DefaultMonitor#doAddMonitor
方法 - 然後執行
DefaultMonitor#instrument
方法,這裡面涉及到Java的Instrumentation
技術和ASM技術 - 建立
MonitorClassFileTransformer
物件,它實現了ClassFileTransformer
介面,織入代理邏輯,就是通過這個物件完成的 - 而有關於邏輯的具體織入,是通過
MonitorClassVisitor
完成。涉及到的知識點:ClassReader
、ClassWriter
、ClassVisitor
- 代理邏輯裡面涉及到
AgentMonitor
相關方法的呼叫,而AgentMonitor
的相關方法會將呼叫次數
、響應時間
、異常數
存入Metrics
中 -
qunar.tc.bistoury.agent.task.monitor.TaskRunner
啟動時,呼叫順序如下:QMonitorClient#reportMonitor -> QMonitorMetricsReportor#report -> 獲取Metric
線上除錯功能
原理和動態監控一樣,也是通過 Instrumentation + ASM 實現
- 對應的指令為
qdebugadd
- 對應的Command為
QDebugAddCommand
- 呼叫鏈路:
QDebugClient#registerBreakpoint -> DefaultDebugger#doRegisterBreakpoint -> DefaultDebugger#instrument
- ASM涉及到
DebuggerClassFileTransformer
、DebuggerClassVisitor
、DebuggerMethodVisitor
- 在執行到對應斷點程式碼的時候,通過執行ASM插入的邏輯,將
本地區域性變數
、例項屬性
、靜態變數
、方法呼叫堆疊資訊
儲存到SnapshotCapture
中 - 執行
DebuggerMethodVisitor#processForBreakpoint
方法,將所有相關的資訊存到DefaultSnapshotStore
的快取中 - 在前端點選
新增斷點
按鈕之後,即傳送qdebugadd
指令之後,前端會開啟一個定時任務,每3s向服務端傳送一個qdebugsearch
指令,直到服務端返回資料。服務端收到指令,從DefaultSnapshotStore
中獲取資料返回前端
其它功能下次補充