接上文:

在討論完性能優(yōu)化的方面和策略之后,這次我們的文章更偏向技術(shù)層面,來分享下如何開發(fā)一個(gè)自己的性能分析工具(基于JVM)。
『新』知識
考慮到咱們大多數(shù)還是開發(fā)業(yè)務(wù)為主,所以Java里面一些『鮮為人知』的API可能很多人都不知道,這里就簡單介紹一番,如果想深究的,就自己谷歌一下吧。
- JVMTI(JVM Tool Interface)是 Java 虛擬機(jī)所提供的 native 編程接口,即底層的相關(guān)調(diào)試接口調(diào)用,我們熟知的Java調(diào)試其實(shí)也是基于它。
 - Instrumentation,雖然Java提供了JVMTI,但是對應(yīng)的agent需要用C/C++開發(fā),對Java開發(fā)者而言并不是非常友好。因此在Java SE 5的新特性中加入了Instrumentation機(jī)制。有了Instrumentation,開發(fā)者可以構(gòu)建一個(gè)基于Java編寫的Agent來監(jiān)控或者操作JVM了,比如替換或者修改某些類的定義等。
 

有了上面兩個(gè)知識,其實(shí)我們就可以開發(fā)一個(gè)簡單的Agent了,Instrumentation可以理解為JVM層面的AOP(Aspect Oriented Programming),通過應(yīng)用啟動(dòng)時(shí)掛載Agent,我們可以對每一個(gè)class字節(jié)碼進(jìn)行查看和修改。
- ASM ASM是一種通用Java字節(jié)碼操作和分析框架,它可以用于修改現(xiàn)有的class文件或動(dòng)態(tài)生成class文件,結(jié)合Instrumentation我們可以做到掛載Agent的時(shí)候,對字節(jié)碼進(jìn)行修改,加上我們需要的性能監(jiān)控手段。ASM的學(xué)習(xí)是有難度的,需要對字節(jié)碼有所了解,但由于其性能優(yōu)秀,被各種工具作為修改字節(jié)碼的首選,比如大家熟悉的Cglib。
 - Javassist 依舊是一個(gè)字節(jié)碼的修改工具,但對初學(xué)者更加友好,不需要過多了解字節(jié)碼層面,可以書寫Java語法片段對已有class字節(jié)進(jìn)行修改,缺點(diǎn)是過于模板化,難以優(yōu)化,并且功能有限。我們做性能分析工具,本身是要盡可能減少插入字節(jié)碼對現(xiàn)有代碼的影響,并且注入的速度也要盡可能快,所以一般都會(huì)選擇ASM作為首選項(xiàng)。
 
好了,介紹完Instrumentation和ASM,我們是不是就可以滿足制作性能分析工具的前提條件了呢?你看我們通過Instrumentation進(jìn)行JVM層面的AOP,再通過ASM對JAVA的字節(jié)碼進(jìn)行修改,就可以著手完成性能分析最重要的埋點(diǎn)環(huán)節(jié)了。
看起來沒有錯(cuò),但是誰也不希望我們增強(qiáng)修改過的代碼一直存在內(nèi)存中,分析一次就對環(huán)境造成不可逆的破壞吧。Instrumentation可以通過addTransformer添加字節(jié)碼轉(zhuǎn)換器,也可以將字節(jié)碼恢復(fù)原樣(只需要removeTransformer再retransformClasses就可以恢復(fù)了),但javaAgent畢竟是個(gè)單獨(dú)的jar包,它也會(huì)有一些依賴,將其加載進(jìn)來必然會(huì)引發(fā)新的Class加載甚至是Class的沖突。那么新的問題就出來了,javaAgent如何不對現(xiàn)有的類有影響呢?
ClassLoader 類加載器,我們可以采用一個(gè)新的類加載器,專門加載javaAgent里面的類庫,這樣就可以解決agent的類引發(fā)沖突的問題,在舊版本JDK中我們很難對ClassLoader做卸載,并且類的卸載是很麻煩的事情,限制很多,好在我們現(xiàn)在多數(shù)用的都是jdk1.8,只要遵循類卸載的規(guī)則,對ClassLoader進(jìn)行清理還是很輕松的。
額外的類加載器實(shí)現(xiàn)了業(yè)務(wù)代碼和Agent代碼類的隔離,使它們可以安全引用包,并且可以對Agent的類進(jìn)行卸載,但這樣同時(shí)引入了一個(gè)新的問題。類是隔離的,我在對業(yè)務(wù)代碼進(jìn)行增強(qiáng)時(shí),如何向agent代碼傳遞信息?增強(qiáng)的代碼一定是被加載在AppClassLoader里,如何與AgentClassLoader進(jìn)行通訊呢?
BootStrapClassLoader 啟動(dòng)類加載器,該ClassLoader是JVM在啟動(dòng)時(shí)創(chuàng)建的,理解這一部分知識,就一定要理解ClassLoader的雙親委派機(jī)制。我們可以創(chuàng)建一個(gè)非常簡單的Spy類和一個(gè)SpyHandler接口,Spy類定義好一些靜態(tài)方法用于代碼增強(qiáng)時(shí)調(diào)用,而SpyHandler則是定義一些用于通訊傳參的接口。我們將這兩個(gè)類打成jar包,并通過Instrumentation的appendToBootstrapClassLoaderSearch接口,在agent加載時(shí)引入BootStrapClassLoader類中,這樣我們在各個(gè)ClassLoader中都能訪問Spy類和SpyHandler接口了。
通過上面的介紹,我們現(xiàn)在可以動(dòng)手做一個(gè)自己的APM工具了,通過Instrumentation+ASM,我們可以實(shí)現(xiàn)Class文件的修改增強(qiáng),甚至可以修改JDK自帶的類比如String,通過自定義的ClassLoader我們可以隔離Agent的類和業(yè)務(wù)的類,通過打入BootStrap的Spy,我們可以實(shí)現(xiàn)跨ClassLoader之間的通訊。
萬事俱備,我們現(xiàn)在可以開始動(dòng)手實(shí)現(xiàn)一個(gè)自己的APM工具了吧!
打住,其實(shí)上面這些功能不需要自己一一實(shí)現(xiàn),我們不需要重復(fù)制造輪子,來自阿里開源項(xiàng)目JVM-SANDBOX此時(shí)華麗登場。這個(gè)項(xiàng)目屏蔽了ASM難以使用的缺點(diǎn),也簡化了Instrumentation打樁過程,并且實(shí)現(xiàn)了ClassLoader的隔離,也有了BootStrapClassLoader中的Spy類,我們在此框架的基礎(chǔ)上進(jìn)行開發(fā)更為簡單。

原圖鏈接:https://github.com/alibaba/jvm-sandbox/wiki/img/jvm-sandbox-classloader.png
集『大』成
我們擁有了JVM-SANDBOX這一利器,似乎節(jié)約了我們很多的時(shí)間,我們現(xiàn)在終于可以著手性能分析了。
那么怎么進(jìn)行性能分析呢?
- Zipkin,開源的鏈路追蹤。
 - Jaeger,開源的鏈路追蹤支持Zipkin協(xié)議,個(gè)人感覺更為好用。
 
我們可以引入Zipkin或者Jaeger作為收集者和UI展現(xiàn),根據(jù)自己的喜好選擇一個(gè)好用的開源工具。通過sandbox提供的功能,我們可以很方便編寫埋點(diǎn)代碼,將我們的鏈路追蹤工具集成到Agent里面,最終實(shí)現(xiàn)無侵入的定制化鏈路追蹤。
通過集成ZipkinClient或者JaegerClient我們可以進(jìn)行埋點(diǎn)收集,我們似乎把一些功能以搭積木的方式組裝起來,解決了一個(gè)頗為復(fù)雜的實(shí)現(xiàn),這就是開源的魅力所在。其實(shí)在實(shí)際的過程中我們還遇到了一些困難,比如如何追蹤異步調(diào)用,如何追蹤跨線程的調(diào)用,如何處理線程池,如何處理ForkJoin?
其中最為復(fù)雜的是如何處理那些跨線程的派發(fā),我們?nèi)绾螌㈡溌返纳舷挛脑诙鄠€(gè)線程中傳遞。JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞。但對于使用線程池等會(huì)池化復(fù)用線程的執(zhí)行組件的情況,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的;這時(shí)父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒有意義,應(yīng)用需要的實(shí)際上是把 任務(wù)提交給線程池時(shí)的ThreadLocal值傳遞到任務(wù)執(zhí)行時(shí)。
說起來可能不好理解,總得來說無論是ThreadLocal還是InheritableThreadLocal都無法處理線程池或者ForkJoin帶來的線程復(fù)用的副作用,即無法有效準(zhǔn)確安全的傳遞鏈路的上下文,不信大家可以試一試。
那么怎么解決這個(gè)問題呢?沒錯(cuò),就是修改JDK源碼,讓線程池在進(jìn)行調(diào)度的時(shí)候具有安全準(zhǔn)確傳遞上下文信息的能力,比如對Runnable和Callable接口進(jìn)行增強(qiáng)處理,讓其可以攜帶線程的上下文。如果要對JDK的代碼進(jìn)行增強(qiáng),我們需要非常熟悉線程調(diào)度、線程池、Forkjoin的源碼,還需要小心處理值的傳遞確保安全,聽起來就很危險(xiǎn),也很困難。不用擔(dān)心我們不是第一次遇到這種問題的人,我們再次搬來了阿里的開源產(chǎn)品TTL,這個(gè)庫解決的就是上面描述的問題。
但是找到開源產(chǎn)品也并不一定能解決所有的問題,transmittable-thread-local雖然能夠解決線程復(fù)用時(shí)傳值的問題,但是它的實(shí)現(xiàn)對JDK代碼進(jìn)行了『過分』的修改,以至于Instrumentation不能進(jìn)行動(dòng)態(tài)增強(qiáng),它需要在啟動(dòng)時(shí)未加載到ClassLoader的時(shí)候?qū)DK的源碼進(jìn)行增強(qiáng),并不能對已加載的JDK源碼進(jìn)行動(dòng)態(tài)增強(qiáng),也就是說這種增強(qiáng)只能發(fā)生在一開始,不能發(fā)生在中間時(shí)間,且不可卸載。
這是因?yàn)镮nstrumentation的redefineClasses這個(gè)方法存在限制:重定義不得添加、移除、重命名字段或方法;不得更改方法簽名、繼承關(guān)系(不然那些商業(yè)的熱重載技術(shù)怎么賺錢。。)。而TTL的增強(qiáng)違反了這個(gè)原則,我們需要對其修改,并集成到Agent中。這個(gè)改造比較無趣也不好解說,可以直接看改造后的JVM-SANDBOX,我們?yōu)榱撕罄m(xù)使用方便,將TTL庫直接用BootStrapClassLoader加載了進(jìn)去。
開源
最終開源的性能分析工具可以在這里找到:https://github.com/tmtbe/PVisualization,配合改造后的JVM-SANDBOX,可以實(shí)現(xiàn)360度無死角的性能鏈路追蹤分析,開發(fā)埋點(diǎn)也非常便捷,也無需考慮任何線程池的問題。

原圖鏈接:https://github.com/tmtbe/PVisualization/raw/master/source/img.png















 
 
 









 
 
 
 