Pinpoint 插件開發

  • 一、運行 Pinpoint

    運行 Pinpoint 系統最簡單的方法是使用 Docker。

    $ gitclone https://github.com/dawidmalina/docker-pinpoint$ cd docker-pinpoint$ docker-composeup -d 二、編譯環境搭建

    編譯 Pinpoint 1.5.2 的源代碼需要 JDK 6、JDK 7+ 以及 Maven 3.2.x+ 的支持,符合以上要求的最新版本的編譯工具列表如下:

    要求 最新版本 備註 JDK 6 JDK 6u45 已經停止更新 JDK 7+ JDK 8u112 Maven 3.2.x+ Maven 3.2.9 之所以使用 Maven 3.2.x,猜想是因爲只有 Maven 3.2.x 才支持 JDK 6

    並且還要設置兩個環境變量:JAVA_6_HOME 和 JAVA_7_HOME 分別指向對應的 JDK 安裝目錄。

    然後運行以下命令完成編譯:

    $ gitclone https://github.com/naver/pinpoint$ cd pinpoint$ mvninstall -Dmaven.test.skip=true

    使用 Docker 來編譯會更加容易,免去了配置環境的必要。首先將 Pinpoint 的源代碼下載到本地目錄,例如 /projects/pinpoint,然後運行命令:

    $ dockerrun -v /projects/pinpoint:/pinpoint:rw -v </path/to/.m2>:/root/.m2:rwtangrui/pinpoint-development

    其中的兩個 -v 參數是用來映射目錄的。第一個 -v 將本地 /projects/pinpoint 目錄映射到容器的 /pinpoint 目錄;而第二個 -v 是將本地的 Maven Repository 映射到容器的 /root/.m2 下,否則每次編譯 Maven 都會從網絡上下載大量的依賴,非常緩慢,這樣在本地共享一下,以後再編譯就快了。

    三、技術概述 3.1、架構組成

    Pinpoint 插件開發_Java

    Pinpoint 主要由 3 個組件外加 Hbase 數據庫組成,三個組件分別爲:Agent、Collector 和 Web UI。

    3.2、系統特色 分佈式交易追蹤,追蹤分佈式系統中穿梭的消息 自動偵測應用程序拓撲,以幫助指明應用程序的配置 橫向擴展以支持大規模的服務器組 提供代碼級別的可見性,以方便識別故障點和瓶頸 使用字節碼注入技術,無需修改代碼就可以添加新功能 3.3、分佈式追蹤系統如何工作

    無論 Pinpoint 還是 Zipkin,其實現都是基於 Google Dapper 的論文。其核心思想就是在服務各節點彼此調用的時候,記錄並傳遞一個應用級別的標記,這個標記可以用來關聯各個服務節點之間的關係。比如兩個節點之間使用 HTTP 作爲請求協議的話,那麼這些標記就會被加入到 HTTP 頭中。因此如何傳遞這些標記是與節點之間使用的通訊協議有關的,有些協議就很容易加入這樣的內容,但有些協議就相對困難甚至不可能,因此這一點就直接決定了實現分佈式追蹤系統的難度。

    3.4、Pinpoint 的數據結構

    Pinpoint 消息的數據結構主要包含三種類型 Span,Trace 和 TraceId。

    Span 是最基本的調用追蹤單元,當遠程調用到達的時候,Span 指代處理該調用的作業,並且攜帶追蹤數據。爲了實現代碼級別的可見性,Span 下面還包含一層 SpanEvent 的數據結構。每個 Span 都包含一個 SpanId。

    Trace 是一組相互關聯的 Span 集合,同一個 Trace 下的 Span 共享一個 TransactionId,而且會按照 SpanId 和 ParentSpanId 排列成一棵有層級關係的樹形結構。

    TraceId 是 TransactionId、SpanId 和 ParentSpanId 的組合。TransactionId(TxId)是一個交易下的橫跨整個分佈式系統收發消息的 ID,其必須在整個服務器組中是全局唯一的。也就是說 TransactionId 識別了整個調用鏈;SpanId(SpanId)是處理遠程調用作業的 ID,當一個調用到達一個節點的時候隨即產生;ParentSpanId(pSpanId)顧名思義,就是產生當前 Span 的調用方 Span 的 ID。如果一個節點是交易的最初發起方,其 ParentSpanId 是 -1,以標誌其是整個交易的根 Span。下圖能夠比較直觀的說明這些 ID 結構之間的關係。

    Pinpoint 插件開發_Java

    3.5、如何使用 Java Agent

    Pinpoint 的優勢就在於其使用 Java Agent 的方式向節點應用注入字節碼,而不是直接修改源代碼。因此部署一個節點就變得非常容易,只需要在程序啓動的時候加入如下一些啓動參數:

    -javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar-Dpinpoint.agentId=<Agent's UniqueId>-Dpinpoint.applicationName=<Thenameindicating a sameservice (AgentIdcollection)> 3.6、代碼注入是如何工作的

    Pinpoint 插件開發_Java

    Pinpoint 對代碼注入的封裝非常類似 AOP,當一個類被加載的時候會通過 Interceptor 向指定的方法前後注入 before 和 after 邏輯,在這些邏輯中可以獲取系統運行的狀態,並通過 TraceContext 創建 Trace 消息,併發送給 Pinpoint 服務器。但與 AOP 不同的是,Pinpoint 在封裝的時候考慮到了更多與目標代碼的交互能力,因此用 Pinpoint 提供的 API 來編寫代碼會比 AOP 更加容易和專業。(這些內容後面會有更詳細說明)

    3.7、Pinpoint 的應用實例

    下圖展現了兩個 Tomcat 服務器應用了 Pinpoint 之後,被收集到的追蹤數據。

    Pinpoint 插件開發_Java

    四、Agent 插件開發

    開發 Pinpoint Agent 只需要關注兩個接口:TraceMetadataProvider 和 ProfilerPlugin,實現類通過 Java 的服務發現機制進行加載。

    4.1、ServiceLoader 配置

    Pinpoint 的插件是以 jar 包的形式部署的,爲了使得 Pinpoint Agent 能夠定位到 TraceMetadataProvider 和 ProfilerPlugin 兩個接口的實現,需要在 META-INF/services 目錄下創建兩個文件:

    META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider

    META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin

    這兩個文件中的每一行都寫明對應實現類的全名稱即可。

    4.2、TraceMetadataProvider

    TraceMetadataProvider 提供了對 ServiceType 和 AnnotationKey 的管理。

    4.2.1、ServiceType

    每個 Span 和 SpanEvent 都包含一個 ServiceType,用來標明他們屬於哪一個庫(Jetty、MySQL JDBC Client 或者 Apache HTTP Client 等),以及追蹤此類型服務的 Span 和 SpanEvent 該如何被處理。ServiceType 的數據結構如下:

    屬性 描述 name ServiceType 的名稱,必須唯一 code ServiceType 的編碼,短×××,必須唯一 desc 描述 properties 附加屬性

    Pinpoint 爲了儘量壓縮 Agent 到 Collector 的數據包的大小,ServiceType 被設計成不是以序列化字符串的形式發送的,而是以×××數字發送的(code 字段),這就需要建立一個映射關係,將 code 轉換成對應的 ServiceType 實例,這些映射機制就是由 TraceMetadataProvider 實現的。

    ServiceType 的 code 必須全局唯一,爲了避免衝突,Pinpoint 官方對這個映射表進行了嚴格的管理,如果所開發的插件想要聲明新的映射關係,需要通知 Pinpoint 團隊,以便對此映射表進行更新和發佈。與私有 IP 地址段一樣,Pinpoint 團隊也保留了一段私有區域可供開發內部服務的時候使用。具體的 ID 範圍參照表如下:

    ServiceType Code 全部範圍

    類型 範圍 Internal Use 0 ~ 999 Server 1000 ~ 1999 DB Client 2000 ~ 2999 Cache Client 8000 ~ 8999 RPC Client 9000 ~ 9999 Others 5000 ~ 7999

    ServiceType Code 私有區域範圍

    類型 範圍 Server 1900 ~ 1999 DB Client 2900 ~ 2999 Cache Client 8900 ~ 8999 RPC Client 9900 ~ 9999 Others 7500 ~ 7999 4.2.2、AnnotationKey

    Annotation 是包含在 Span 和 SpanEvent 中的更詳盡的數據,以鍵值對的形式存在,鍵就是 AnnotationKey,值可以是字符串或字節數組。Pinpoint 內置了很多的 AnnotationKey,如果不夠用的話也可以通過 TraceMetadataProvider 來自定義。AnnotationKey 的數據結構如下:

    屬性 描述 name AnnotationKey 的名稱 code AnnotationKey 的編碼,×××,必須唯一 properties 附加屬性

    同 ServiceType 的 code 字段一樣,AnnotationKey 的 code 也是全局唯一 的,Pinpoint 團隊給出的私有區域範圍是 900 到 999。

    4.2.3、TraceMetadataProvider 接口

    TraceMetadataProvider 接口只有一個 setup 方法,此方法接收一個 TraceMetadataSetupContext 類型的參數,該類型有三個方法:

    方法 描述 addServiceType(ServiceType) 註冊 ServiceType addServiceType(ServiceType, AnnotationKeyMatcher) 註冊 ServiceType,並將匹配 AnnotationKeyMatcher 的 AnnotationKey 作爲此 ServiceType 的典型註解,這些典型註解會顯示在瀑布視圖的 Argument 列中 addAnnotationKey(AnnotationKey) 註冊 AnnotationKey,這裏註冊的 AnnotationKey 會被標記爲 VIEW_IN_RECORD_SET,顯示在瀑布視圖中是以單獨一行顯示的,且前面有一個藍色的 i 圖標

    詳細使用方法可以參考官方提供的樣例文件 SampleTraceMetadataProvider 。

    4.3、ProfilerPlugin

    ProfilerPlugin 通過字節碼注入的方式攔截目標代碼以實現跟蹤數據的收集。

    4.3.1、插件的工作原理 Pinpoint Agent 隨 JVM 一起啓動 Agent 加載所有 plugin 目錄下的插件 Agent 調用每個已經加載的插件的 ProfilerPlugin.setup(ProfilerPluginSetupContext) 方法 在 setup 方法中,插件定義那些需要被轉換的類,並註冊 TransformerCallback 目標應用啓動 當類被加載的時候,Pinpoint Agent 會尋找註冊到該類的 TransformerCallback 如果 TransformerCallback 被註冊,Agent 就調用它的 doInTransform 方法 TransformerCallback 修改目標類的字節碼(例如添加攔截器、添加字段等) 修改後的代碼返回到 JVM,類型加載的時候就使用修改後的字節碼 應用程序繼續 當調用到被修改的方法的時候,注入的攔截器的 before 和 after 方法被調用 攔截器記錄追蹤數據

    Pinpoint 插件的工作原理看似跟 AOP 非常相似,但還是有一些區別和自身的特色:

    因爲 Pinpoint 需要處理的注入場景比較單一,因此他提供的注入 API 相對簡單;而 AOP 爲了要處理各種可能的切面情況,Pointcut 被設計得非常複雜。 Pinpoint 插件攔截是通過攔截器的 before 和 after 方法實現的,很像 around 切面,如果不想執行其中一個方法,可以通過 @IgnoreMethod 註解來忽略。 Pinpoint 的攔截器可以任意攔截方法,因此被攔截的方法之間可能會有調用關係,這會導致追蹤數據被重複收集,因此 Pinpoint 提供了 Scope 和 ExecutionPolicy 功能。在一個 Scope 內,可以定義攔截器的執行策略:是每次都執行(ExecutionPolicy.ALWAYS),還是在沒有更外層的攔截器存在的時候執行(ExecutionPolicy.BOUNDARY),或者必須在有外層攔截器存在的時候執行(ExecutionPolicy.INTERNAL)。具體請參考這個 樣例 。 在一個 Scope 內的攔截器彼此還可以傳遞數據。同一個 Scope 內的攔截器共享一個 InterceptorScopeInvocation 對象,可以使用他來交換數據。參考 樣例 。 除了攔截方法以外,Pinpoint 還可以向目標類中注入字段以及 getter 和 setter 方法,可以使用它們來保存一些上下文的數據。

    通過上述內容可以瞭解,如果要編寫一個 Pinpoint 的插件,除了要對目標代碼的調用邏輯有較深入的理解,還必須得設計好上下文數據如何存儲、如何傳遞,以及如何通過 Scope 避免信息被重複收集等問題。這些問題在 AOP 的場景下也會存在,只是 Pinpoint 提供了更加一致和便捷的解決方案,而 AOP 的就要自己去考慮這些問題了。

    4.3.2、方法攔截

    如前文所述,Pinpoint 插件需要實現 ProfilerPlugin 接口,該接口只有一個 setup(ProfilerPluginSetupContext) 方法。爲了更容易的操作 Pinpoint 的代碼注入 API,還需要實現一個 TransformTemplateAware 的接口,該接口會注入 TransformTemplate 類。

    public class SamplePluginimplements ProfilerPlugin, TransformTemplateAware {private TransformTemplatetransformTemplate;@Overridepublic void setup(ProfilerPluginSetupContextcontext) {}@Overridepublic void setTransformTemplate(TransformTemplatetransformTemplate) {this.transformTemplate = transformTemplate;}}

    ProfilerPluginSetupContext 有兩個方法:getConfig() 和 addApplicationTypeDetector(ApplicationTypeDetector…)。第一個方法用來獲取 ProfilerConfig 對象,該對象保存了所有插件的配置信息,而第二個方法用來添加 ApplicationTypeDetector。ApplicationTypeDetector 是用來自動檢測節點所運行服務的類型的。例如在 pinpoint-tomcat-plugin 項目中,有 TomcatDetector 類,這個類的作用是通過如下檢測來確定當前服務爲 Tomcat 的:

    檢查 main class 是不是 org.apache.catalina.startup.Bootstrap 檢查是否有系統變量 catalina.home 檢查是否存在某個指定的類,這裏也是 org.apache.catalina.startup.Bootstrap

    如果這三個條件都滿足,就把當前節點的 ServiceType 設置爲 Tomcat。

    TransformTemplate 只有一個方法 transform(String, TransformCallback),第一個參數是需要被轉換的類的全名稱,而第二個參數就是 4.3.1 章節中提到的 TransformCallback 接口,這個接口也只有一個方法叫 doInTransform,所有的注入邏輯都在這裏完成。

    public byte[] doInTransform(Instrumentorinstrumentor,ClassLoaderclassLoader,String className,Class<?> classBeingRedefined,ProtectionDomainprotectionDomain,byte[] classfileBuffer) throws InstrumentException { // 1. Get InstrumentClass of the target class InstrumentClasstarget = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer); // 2. Get InstrumentMethod of the target method. InstrumentMethodtargetMethod = target.getDeclaredMethod("targetMethod", "java.lang.String"); // 3. Add interceptor. The first argument is FQN of the interceptor class,// followed by arguments for the interceptor's constructor. targetMethod.addInterceptor("com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor", va(SamplePluginConstants.MY_SERVICE_TYPE)); // 4. Return resulting byte code. return target.toBytecode();} 注入過程是從獲取 InstrumentClass 類開始的。 如果想要攔截一個方法,或者是添加字段以及 getter、setter 方法,就可以調用 InstrumentClass 對應的 API 來實現,這裏是獲取了一個簽名爲 targetMethod(String) 的方法,返回的對象是 InstrumentMethod 類型。 調用 InstrumentMethod 的 addInterceptor 方法注入攔截器,所有跟蹤信息的收集行爲都是在攔截器中實現的,這裏添加的攔截器是 com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor,這是 Pinpoint 框架默認提供的一個現成的攔截器,裏面收集了一些 targetMethod 的調用信息。後面的 va 是一個靜態方法,即可變參數列表,va 中給出的參數會傳遞到 BasicMethodInterceptor 的構造方法中。 調用 InstrumentClass.toBytecode() 方法即可返回注入後的字節碼,剩下的事情就是 Pinpoint Agent 自己來完成的了。

    BasicMethodInterceptor 類僅提供了對方法調用信息的簡單收集,只收集方法的名稱、參數、返回值以及是否產生異常等等。在某些複雜的場景下,我們會需要收集更多的信息,如當前登錄用戶、線程池、數據庫查詢語句以及任何跟中間件功能有關的信息,這就需要我們定義自己的 Interceptor 類。

    以上內容請參考該 樣例 。

    Interceptor 是一個標記接口,真正有意義的是 AroundInterceptor 接口,該接口定義瞭如下兩個方法:

    public interface AroundInterceptor extends Interceptor { void before(Object target, Object[] args); void after(Object target, Object[] args, Object result, Throwablethrowable);}

    爲了應對被攔截方法的不同個數的參數列表,AroundInterceptor 還有若干子接口:AroundInterceptor0, AroundInterceptor1,…,AroundInterceptor5,分別對應沒有參數,一個參數,到 5 個參數的方法。實現 Interceptor 接口的時候要提供一個如下的構造方法:

    public RecordArgsAndReturnValueInterceptor(TraceContexttraceContext,MethodDescriptordescriptor) { this.traceContext = traceContext; this.descriptor = descriptor;}

    TraceContext 和 MethodDescriptor 會被 Pinpoint Agent 運行時注入進來,當然也可以添加額外的參數,這些額外的參數,需要在 addInterceptor 的時候指定,就像上文中關於 va 的描述那樣。

    有了 TraceContext 對象,就可以開始收集信息了。調用 traceContext.getCurrentTraceObject() 方法可以獲取當前的 Trace,再調用 trace.traceBlockBegin() 就開始記錄一個新的 Trace 塊了(這裏我理解應該就是 Span 了)。在 traceBlockBegin 以後,可以 調用 currentSpanEventRecorder 獲取 SpanEventRecorder 對象,這個對象提供了諸如 recordServiceType、recordApi、recordException 和 recordAttribute 等方法,可以記錄方法的有關信息。但是 SpanEventRecorder 並沒有提供 recordReturnValue 這樣的方法,只能通過 recordAttribute 來記錄。所有自己擴展的信息也是通過 recordAttribute 來記錄的。最後所有信息記錄完成就調用 traceBlockEnd() 方法關閉區塊。

    以上內容請參考該 樣例 。

    五、總結

    其實 Pinpint 的插件開發 API 還提供了非常豐富的能力,如攔截異步方法、調用鏈跟蹤、攔截器之間共享數據等等,但原理都是基於上述這些內容,只是調用了更復雜的 API 而已。具體代碼可以參考官方提供的 樣例項目 ,裏面有非常詳盡的代碼及註釋,相信理解了上面的內容,再看這個代碼就不會有任何困難了。

  • 以上是Pinpoint 插件開發的內容,更多 插件 pinpoint 開發 的內容,請您使用右上方搜索功能獲取相關信息。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章