一文帶你了解agent機制

更多內(nèi)容請查看個人博客codercc

1. 插樁的使用場景

在實際業(yè)務(wù)開發(fā)中,系統(tǒng)層面會有一些公共模塊需要進行實現(xiàn),類似于校驗、權(quán)限等等,在成熟的解決方案中會通過AOP的方式進行實現(xiàn)。

通常鏈路日志追蹤上,每個公司都會有ELK的解決方案,但是公司的業(yè)務(wù)線眾多的情況下,通常會要求業(yè)務(wù)系統(tǒng)在日志打印上會增加不同標記來進行區(qū)分,方便后續(xù)不同業(yè)務(wù)部門進行成本核算以及權(quán)限管控等等,也就是說在日志輸出上會有一定的格式要求。

另外,在實際排查問題中往往需要完成的上下文參數(shù)才能有助于問題的高效排查,因為平時在系統(tǒng)中主動的編寫日志,實際上是一種防御式編程了,那么一定是在寫代碼時就考慮了這種或者那種的業(yè)務(wù)異常情況,基本上在線上出現(xiàn)問題的概率會很小。大多數(shù)情況,出現(xiàn)線上問題一定是日常開發(fā)中沒有考慮的地方了,也只能通常arthas去分析。如果涉及到上下游服務(wù)時進行溝通的時候,往往上下游開發(fā)同學會詢問調(diào)用服務(wù)的參數(shù)以及鏈路的traceid,才能高效的排查。糟糕的是,如果系統(tǒng)中沒有提前埋入的話,只能臨時去加代碼,然后發(fā)布到預發(fā)等環(huán)境上,如果幸運的話能夠復現(xiàn)問題,也就能解決。針對這種情況,如果系統(tǒng)能夠自動打印出方法的上下文出入?yún)?shù)的話,在每一條鏈路上并且自動種入traceId的話,這樣就能在問題排查場景上更加高效,針對這塊日志標準化的能力可以抽象成公共基礎(chǔ)能力。

因此,在這樣的訴求下,如果涉及到日志標準化改造就需要一套通用的解決方案來進行,來完成日志格式的改造當然有很多的方式來進行推進,比如堆人集中改造:通過團隊組織層面,作為技術(shù)驅(qū)動的事項,有每個同學在原先的log.info(其他日志級別的日志一樣)中按照公司的日志格式要求添加部門特殊的業(yè)務(wù)標記KV對。或者實現(xiàn)一套spring AOP的方案,定義一些注解提供給各個業(yè)務(wù)系統(tǒng)使用,但是針對存量代碼來說,需要投入人力去改造,在類或者方法上添加相應(yīng)的注解,這種方式也會帶來人效很低的問題。

針對上述這些問題,可以通過agent的方式來實現(xiàn)方法級別的字節(jié)碼插樁并且進行日志標準化。AOP是一類解決方案的“指導思想”,具體的落地實現(xiàn)方式會有很多,比如aspectJ,cglib等等工具,通過記錄方案的執(zhí)行耗時以及異常和方法出入?yún)硗瓿蓸I(yè)務(wù)鏈路的非侵入監(jiān)控。整體思路是,agent機制提供了“字節(jié)碼更改”的時機,字節(jié)碼插樁則是AOP的一種具體落地方式。

在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包,該包提供了一些工具幫助開發(fā)人員在 Java 程序運行時,動態(tài)修改系統(tǒng)中的 Class 類型。其中,使用該軟件包的一個關(guān)鍵組件就是 Java agent。從名字上看,似乎是個 Java 代理之類的,提供了一個可以更改class字節(jié)碼的時機。有很多開發(fā)工具都是基于Java Agent實現(xiàn)的,例如常見的熱部署JRebel,各種線上診斷工具(btrace, greys),還有阿里最近開源的arthas。

2. agent使用

2.1 agent靜態(tài)加載

Javaagent是java命令的一個參數(shù)。參數(shù) javaagent 可以用于指定一個 jar 包,并且對該 java 包有2個要求:

  1. 這個 jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項。
  2. Premain-Class 指定的那個類必須實現(xiàn) premain() 方法。

premain 方法,從字面上理解,就是運行在 main 函數(shù)之前的的類。當Java 虛擬機啟動時,在執(zhí)行 main 函數(shù)之前,JVM 會先運行-javaagent所指定 jar 包內(nèi) Premain-Class 這個類的 premain 方法 。premain方法簽名如下:

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

默認會優(yōu)先使用帶有Instrumentation的premain加載,如果加載了第一個方法,那么第二個方法就不會再去加載。如果第一個方法沒有,才會去加載第二個方法。

agent靜態(tài)啟動方式

使用 javaagent 需要幾個步驟:

  1. 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 創(chuàng)建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數(shù) -javaagent: jar包路徑啟動要代理的方法。

在執(zhí)行以上步驟后,JVM 會先執(zhí)行 premain 方法,大部分類加載都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統(tǒng)類,因為很多系統(tǒng)類先于 agent 執(zhí)行,而用戶類的加載肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,既然可以攔截類的加載,那么就可以去做重寫類這樣的操作,結(jié)合第三方的字節(jié)碼編譯工具,比如ASM,javassist,cglib等等來改寫實現(xiàn)類。

2.2 靜態(tài)加載示例

  1. 首先創(chuàng)建一個agent類,其中包含了premian方法,并且通過實現(xiàn)ClassFileTransformer接口來完成一個自定義重寫字節(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
    

    該文件的生成也可以通過maven插件配置后自動生成,具體配置如下:

    <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路徑,啟動應(yīng)用

    -javaagent:path-to/agent-core-0.0.1-SNAPSHOT.jar
    

    啟動應(yīng)用后,在類加載之前會先被agent先進行攔截,可以看示例代碼的輸出:

    premain load class !!!
    premain load class !!!
    premain load class !!!
    premain load class !!!
    premain load class !!!
    premain load class !!!
    

2.3 agent動態(tài)加載

premain的方式是在應(yīng)用啟動執(zhí)行main函數(shù)之前,提供了可以對類進行修改的時機。在main函數(shù)執(zhí)行之后或者說業(yè)務(wù)應(yīng)用正常運行后,再去更改類字節(jié)碼的時機只能通過agentmain方法,具體如下:

//采用attach機制,被代理的目標程序VM有可能很早之前已經(jīng)啟動,當然其所有類已經(jīng)被加載完成,這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對應(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方法,并在次類中完成對應(yīng)的agent邏輯。并且,如果需要完成對字節(jié)碼的更改,同樣可以實現(xiàn)ClassFileTransformer接口,將實現(xiàn)類放置到Instrumentation;

  2. 完成MAINFEST.MF文件,配置Agent-Class等選項,具體如下:

    Agent-Class: com.agent.example.AgentMainAgent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    
    

    對MAINFEST.MF文件也可以通過maven插件完成配置,在打包的時候自動生成,具體配置如下:

    <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掛載

動態(tài)agent的方式實際上是指業(yè)務(wù)應(yīng)用在運行中能夠注入一個agent,借助agent完成相應(yīng)的代理邏輯。那么,怎樣才能在JVM運行的時候向其完成注入,自然而然也就涉及到了兩個JVM進程之間的通信,可以通過VirtualMachine來完成。

VirtualMachine 字面意義表示一個Java 虛擬機,也就是程序需要監(jiān)控的目標虛擬機,提供了獲取系統(tǒng)信息(比如獲取內(nèi)存dump、線程dump,類信息統(tǒng)計(比如已加載的類以及實例個數(shù)等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現(xiàn)的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(進程id),遠程連接到j(luò)vm上 。

代理類注入操作只是它眾多功能中的一個,通過loadAgent方法向jvm注冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以 在class加載前改變class的字節(jié)碼,也可以在class加載后重新加載。在調(diào)用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。

整體流程就是通過VirtualMachine類的attach(pid)方法,便可以attach到一個運行中的java進程上,之后便可以通過loadAgent(agentJarPath)來將agent的jar包注入到對應(yīng)的進程,然后對應(yīng)的進程會調(diào)用agentmain方法。

2.5 動態(tài)加載示例

  1. 首先創(chuàng)建一個包含了agentmain方法的agent類,并新建實現(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. 將整個agent進行打包,完成MAINFEST.MF文件配置;

  3. 在測試類中中通過VirtualMainche類完成對agent動態(tài)掛載到正在運行的JVM進程中

    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()可以列出當前正在運行JVM進程,示例中通過具體的進程名判斷出當前正在執(zhí)行的JVM,然后通過VirtualMachine.attach與目標VM建立連接后,通過loadAgent的方式將agent掛載到目標VM中。示例代碼如下:

    start agentmain
    com.agent.example.AgentTest
      
    agentMain load class !!!
    agentMain load class !!!
    

agent機制提供了在應(yīng)用執(zhí)行前或者應(yīng)用執(zhí)行后,能夠獲取class字節(jié)碼的時機,并且能夠通過更改class字節(jié)碼的方式來完成相應(yīng)的業(yè)務(wù)邏輯,比如方法級別的監(jiān)控、日志標準化等等AOP常見的業(yè)務(wù)場景,這種方式對業(yè)務(wù)應(yīng)用的侵入性是最低的,并且性能是相當可觀的。在后續(xù)文章中會總結(jié)下字節(jié)碼的使用、基于字節(jié)碼插樁完成業(yè)務(wù)監(jiān)控以及實際開發(fā)中遇到問題。

參考資料

https://www.cnblogs.com/rickiyang/p/11368932.html#3812389359

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容