更多內(nèi)容請(qǐng)查看個(gè)人博客codercc
1. 插樁的使用場(chǎng)景
在實(shí)際業(yè)務(wù)開(kāi)發(fā)中,系統(tǒng)層面會(huì)有一些公共模塊需要進(jìn)行實(shí)現(xiàn),類似于校驗(yàn)、權(quán)限等等,在成熟的解決方案中會(huì)通過(guò)AOP的方式進(jìn)行實(shí)現(xiàn)。
通常鏈路日志追蹤上,每個(gè)公司都會(huì)有ELK的解決方案,但是公司的業(yè)務(wù)線眾多的情況下,通常會(huì)要求業(yè)務(wù)系統(tǒng)在日志打印上會(huì)增加不同標(biāo)記來(lái)進(jìn)行區(qū)分,方便后續(xù)不同業(yè)務(wù)部門進(jìn)行成本核算以及權(quán)限管控等等,也就是說(shuō)在日志輸出上會(huì)有一定的格式要求。
另外,在實(shí)際排查問(wèn)題中往往需要完成的上下文參數(shù)才能有助于問(wèn)題的高效排查,因?yàn)槠綍r(shí)在系統(tǒng)中主動(dòng)的編寫日志,實(shí)際上是一種防御式編程了,那么一定是在寫代碼時(shí)就考慮了這種或者那種的業(yè)務(wù)異常情況,基本上在線上出現(xiàn)問(wèn)題的概率會(huì)很小。大多數(shù)情況,出現(xiàn)線上問(wèn)題一定是日常開(kāi)發(fā)中沒(méi)有考慮的地方了,也只能通常arthas去分析。如果涉及到上下游服務(wù)時(shí)進(jìn)行溝通的時(shí)候,往往上下游開(kāi)發(fā)同學(xué)會(huì)詢問(wèn)調(diào)用服務(wù)的參數(shù)以及鏈路的traceid,才能高效的排查。糟糕的是,如果系統(tǒng)中沒(méi)有提前埋入的話,只能臨時(shí)去加代碼,然后發(fā)布到預(yù)發(fā)等環(huán)境上,如果幸運(yùn)的話能夠復(fù)現(xiàn)問(wèn)題,也就能解決。針對(duì)這種情況,如果系統(tǒng)能夠自動(dòng)打印出方法的上下文出入?yún)?shù)的話,在每一條鏈路上并且自動(dòng)種入traceId的話,這樣就能在問(wèn)題排查場(chǎng)景上更加高效,針對(duì)這塊日志標(biāo)準(zhǔn)化的能力可以抽象成公共基礎(chǔ)能力。
因此,在這樣的訴求下,如果涉及到日志標(biāo)準(zhǔn)化改造就需要一套通用的解決方案來(lái)進(jìn)行,來(lái)完成日志格式的改造當(dāng)然有很多的方式來(lái)進(jìn)行推進(jìn),比如堆人集中改造:通過(guò)團(tuán)隊(duì)組織層面,作為技術(shù)驅(qū)動(dòng)的事項(xiàng),有每個(gè)同學(xué)在原先的log.info(其他日志級(jí)別的日志一樣)中按照公司的日志格式要求添加部門特殊的業(yè)務(wù)標(biāo)記KV對(duì)?;蛘邔?shí)現(xiàn)一套spring AOP的方案,定義一些注解提供給各個(gè)業(yè)務(wù)系統(tǒng)使用,但是針對(duì)存量代碼來(lái)說(shuō),需要投入人力去改造,在類或者方法上添加相應(yīng)的注解,這種方式也會(huì)帶來(lái)人效很低的問(wèn)題。
針對(duì)上述這些問(wèn)題,可以通過(guò)agent的方式來(lái)實(shí)現(xiàn)方法級(jí)別的字節(jié)碼插樁并且進(jìn)行日志標(biāo)準(zhǔn)化。AOP是一類解決方案的“指導(dǎo)思想”,具體的落地實(shí)現(xiàn)方式會(huì)有很多,比如aspectJ,cglib等等工具,通過(guò)記錄方案的執(zhí)行耗時(shí)以及異常和方法出入?yún)?lái)完成業(yè)務(wù)鏈路的非侵入監(jiān)控。整體思路是,agent機(jī)制提供了“字節(jié)碼更改”的時(shí)機(jī),字節(jié)碼插樁則是AOP的一種具體落地方式。
在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包,該包提供了一些工具幫助開(kāi)發(fā)人員在 Java 程序運(yùn)行時(shí),動(dòng)態(tài)修改系統(tǒng)中的 Class 類型。其中,使用該軟件包的一個(gè)關(guān)鍵組件就是 Java agent。從名字上看,似乎是個(gè) Java 代理之類的,提供了一個(gè)可以更改class字節(jié)碼的時(shí)機(jī)。有很多開(kāi)發(fā)工具都是基于Java Agent實(shí)現(xiàn)的,例如常見(jiàn)的熱部署JRebel,各種線上診斷工具(btrace, greys),還有阿里最近開(kāi)源的arthas。
2. agent使用
2.1 agent靜態(tài)加載
Javaagent是java命令的一個(gè)參數(shù)。參數(shù) javaagent 可以用于指定一個(gè) jar 包,并且對(duì)該 java 包有2個(gè)要求:
- 這個(gè) jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項(xiàng)。
- Premain-Class 指定的那個(gè)類必須實(shí)現(xiàn) premain() 方法。
premain 方法,從字面上理解,就是運(yùn)行在 main 函數(shù)之前的的類。當(dāng)Java 虛擬機(jī)啟動(dòng)時(shí),在執(zhí)行 main 函數(shù)之前,JVM 會(huì)先運(yùn)行-javaagent所指定 jar 包內(nèi) Premain-Class 這個(gè)類的 premain 方法 。premain方法簽名如下:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
默認(rèn)會(huì)優(yōu)先使用帶有Instrumentation的premain加載,如果加載了第一個(gè)方法,那么第二個(gè)方法就不會(huì)再去加載。如果第一個(gè)方法沒(méi)有,才會(huì)去加載第二個(gè)方法。
agent靜態(tài)啟動(dòng)方式
使用 javaagent 需要幾個(gè)步驟:
- 定義一個(gè) MANIFEST.MF 文件,必須包含 Premain-Class 選項(xiàng),通常也會(huì)加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項(xiàng)。
- 創(chuàng)建一個(gè)Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
- 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
- 使用參數(shù) -javaagent: jar包路徑啟動(dòng)要代理的方法。
在執(zhí)行以上步驟后,JVM 會(huì)先執(zhí)行 premain 方法,大部分類加載都會(huì)通過(guò)該方法,注意:是大部分,不是所有。當(dāng)然,遺漏的主要是系統(tǒng)類,因?yàn)楹芏嘞到y(tǒng)類先于 agent 執(zhí)行,而用戶類的加載肯定是會(huì)被攔截的。也就是說(shuō),這個(gè)方法是在 main 方法啟動(dòng)前攔截大部分類的加載活動(dòng),既然可以攔截類的加載,那么就可以去做重寫類這樣的操作,結(jié)合第三方的字節(jié)碼編譯工具,比如ASM,javassist,cglib等等來(lái)改寫實(shí)現(xiàn)類。
2.2 靜態(tài)加載示例
-
首先創(chuàng)建一個(gè)agent類,其中包含了premian方法,并且通過(guò)實(shí)現(xiàn)ClassFileTransformer接口來(lái)完成一個(gè)自定義重寫字節(jié)碼的類。
public class PremainAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new CustomClassTransformer(), true); } static class CustomClassTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("premain load class !!!"); return classfileBuffer; } } } -
配置MAINFEST.MF文件
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.agent.example.PremainAgent該文件的生成也可以通過(guò)maven插件配置后自動(dòng)生成,具體配置如下:
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.agent.example.PremainAgent</Premain-Class> <Agent-Class>com.agent.example.PremainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> -
配置JVM參數(shù)指定agent路徑,啟動(dòng)應(yīng)用
-javaagent:path-to/agent-core-0.0.1-SNAPSHOT.jar啟動(dòng)應(yīng)用后,在類加載之前會(huì)先被agent先進(jìn)行攔截,可以看示例代碼的輸出:
premain load class !!! premain load class !!! premain load class !!! premain load class !!! premain load class !!! premain load class !!!
2.3 agent動(dòng)態(tài)加載
premain的方式是在應(yīng)用啟動(dòng)執(zhí)行main函數(shù)之前,提供了可以對(duì)類進(jìn)行修改的時(shí)機(jī)。在main函數(shù)執(zhí)行之后或者說(shuō)業(yè)務(wù)應(yīng)用正常運(yùn)行后,再去更改類字節(jié)碼的時(shí)機(jī)只能通過(guò)agentmain方法,具體如下:
//采用attach機(jī)制,被代理的目標(biāo)程序VM有可能很早之前已經(jīng)啟動(dòng),當(dāng)然其所有類已經(jīng)被加載完成,這個(gè)時(shí)候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對(duì)應(yīng)的類可以重新轉(zhuǎn)換,從而激活重新轉(zhuǎn)換的類執(zhí)行ClassFileTransformer列表中的回調(diào)
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
具體的步驟和靜態(tài)加載的基本一致:
新建agent類,其中包含agentmain方法,并在次類中完成對(duì)應(yīng)的agent邏輯。并且,如果需要完成對(duì)字節(jié)碼的更改,同樣可以實(shí)現(xiàn)ClassFileTransformer接口,將實(shí)現(xiàn)類放置到Instrumentation;
-
完成MAINFEST.MF文件,配置Agent-Class等選項(xiàng),具體如下:
Agent-Class: com.agent.example.AgentMainAgent Can-Redefine-Classes: true Can-Retransform-Classes: true對(duì)MAINFEST.MF文件也可以通過(guò)maven插件完成配置,在打包的時(shí)候自動(dòng)生成,具體配置如下:
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>com.agent.example.AgentMainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
2.4 agent掛載
動(dòng)態(tài)agent的方式實(shí)際上是指業(yè)務(wù)應(yīng)用在運(yùn)行中能夠注入一個(gè)agent,借助agent完成相應(yīng)的代理邏輯。那么,怎樣才能在JVM運(yùn)行的時(shí)候向其完成注入,自然而然也就涉及到了兩個(gè)JVM進(jìn)程之間的通信,可以通過(guò)VirtualMachine來(lái)完成。
VirtualMachine 字面意義表示一個(gè)Java 虛擬機(jī),也就是程序需要監(jiān)控的目標(biāo)虛擬機(jī),提供了獲取系統(tǒng)信息(比如獲取內(nèi)存dump、線程dump,類信息統(tǒng)計(jì)(比如已加載的類以及實(shí)例個(gè)數(shù)等), loadAgent,Attach 和 Detach (Attach 動(dòng)作的相反行為,從 JVM 上面解除一個(gè)代理)等方法,可以實(shí)現(xiàn)的功能可以說(shuō)非常之強(qiáng)大 。該類允許我們通過(guò)給attach方法傳入一個(gè)jvm的pid(進(jìn)程id),遠(yuǎn)程連接到j(luò)vm上 。
代理類注入操作只是它眾多功能中的一個(gè),通過(guò)loadAgent方法向jvm注冊(cè)一個(gè)代理程序agent,在該agent的代理程序中會(huì)得到一個(gè)Instrumentation實(shí)例,該實(shí)例可以 在class加載前改變class的字節(jié)碼,也可以在class加載后重新加載。在調(diào)用Instrumentation實(shí)例的方法時(shí),這些方法會(huì)使用ClassFileTransformer接口中提供的方法進(jìn)行處理。
整體流程就是通過(guò)VirtualMachine類的attach(pid)方法,便可以attach到一個(gè)運(yùn)行中的java進(jìn)程上,之后便可以通過(guò)loadAgent(agentJarPath)來(lái)將agent的jar包注入到對(duì)應(yīng)的進(jìn)程,然后對(duì)應(yīng)的進(jìn)程會(huì)調(diào)用agentmain方法。
2.5 動(dòng)態(tài)加載示例
-
首先創(chuàng)建一個(gè)包含了agentmain方法的agent類,并新建實(shí)現(xiàn)ClassFileTransformer接口的類加載到instrument中。
public class AgentMainAgent { public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("start agentmain"); inst.addTransformer(new CusDefinedClass(), true); } static class CusDefinedClass implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("agentMain load class !!!"); return classfileBuffer; } } } 將整個(gè)agent進(jìn)行打包,完成MAINFEST.MF文件配置;
-
在測(cè)試類中中通過(guò)VirtualMainche類完成對(duì)agent動(dòng)態(tài)掛載到正在運(yùn)行的JVM進(jìn)程中
public class AgentTest { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> vms = VirtualMachine.list(); for (VirtualMachineDescriptor vm : vms) { if ("com.agent.example.AgentTest".equals(vm.displayName())) { VirtualMachine machine = VirtualMachine.attach(vm.id()); machine.loadAgent("/path-to/agent-core-0.0.1-SNAPSHOT.jar"); } System.out.println(vm.displayName()); } } }VirtualMachine.list()可以列出當(dāng)前正在運(yùn)行JVM進(jìn)程,示例中通過(guò)具體的進(jìn)程名判斷出當(dāng)前正在執(zhí)行的JVM,然后通過(guò)VirtualMachine.attach與目標(biāo)VM建立連接后,通過(guò)loadAgent的方式將agent掛載到目標(biāo)VM中。示例代碼如下:start agentmain com.agent.example.AgentTest agentMain load class !!! agentMain load class !!!
agent機(jī)制提供了在應(yīng)用執(zhí)行前或者應(yīng)用執(zhí)行后,能夠獲取class字節(jié)碼的時(shí)機(jī),并且能夠通過(guò)更改class字節(jié)碼的方式來(lái)完成相應(yīng)的業(yè)務(wù)邏輯,比如方法級(jí)別的監(jiān)控、日志標(biāo)準(zhǔn)化等等AOP常見(jiàn)的業(yè)務(wù)場(chǎng)景,這種方式對(duì)業(yè)務(wù)應(yīng)用的侵入性是最低的,并且性能是相當(dāng)可觀的。在后續(xù)文章中會(huì)總結(jié)下字節(jié)碼的使用、基于字節(jié)碼插樁完成業(yè)務(wù)監(jiān)控以及實(shí)際開(kāi)發(fā)中遇到問(wèn)題。
參考資料
https://www.cnblogs.com/rickiyang/p/11368932.html#3812389359