一文帶你了解agent機(jī)制

更多內(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è)要求:

  1. 這個(gè) jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項(xiàng)。
  2. 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è)步驟:

  1. 定義一個(gè) MANIFEST.MF 文件,必須包含 Premain-Class 選項(xiàng),通常也會(huì)加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項(xiàng)。
  2. 創(chuàng)建一個(gè)Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數(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)加載示例

  1. 首先創(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;
            }
        }
    
    }
    
  2. 配置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>
    
  3. 配置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)加載的基本一致:

  1. 新建agent類,其中包含agentmain方法,并在次類中完成對(duì)應(yīng)的agent邏輯。并且,如果需要完成對(duì)字節(jié)碼的更改,同樣可以實(shí)現(xiàn)ClassFileTransformer接口,將實(shí)現(xiàn)類放置到Instrumentation;

  2. 完成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)加載示例

  1. 首先創(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;
            }
        }
    }
    
  2. 將整個(gè)agent進(jìn)行打包,完成MAINFEST.MF文件配置;

  3. 在測(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

https://www.cnblogs.com/huanshilang/p/12206644.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容