Pinpoint 插件開發

1、運行 Pinpoint 系統

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

# 運行 Pinpoint
$ git clone https://github.com/dawidmalina/docker-pinpoint
$ cd docker-pinpoint
$ docker-compose up -d
複製代碼

2、編譯環境搭建

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

要求 最新版本 備註
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

而且還要設置兩個環境變量:github

JAVA_6_HOME
JAVA_7_HOME
分別指向對應的 JDK 安裝目錄,而後運行如下命令完成編譯:

# 編譯 Pinpoint
$ git clone https://github.com/naver/pinpoint
$ cd pinpoint
$ mvn install -Dmaven.test.skip=true
複製代碼

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

# 使用 Docker 編譯 Pinpoint
$ docker run -v /projects/pinpoint:/pinpoint:rw -v </path/to/.m2>:/root/.m2:rw tangrui/pinpoint-development
複製代碼

其中的兩個 -v 參數是用來映射目錄的。第一個 -v 參數是將本地的 /projects/pinpoint 目錄映射到容器的 /pinpoint 目錄;而第二個 -v 參數是將本地的 Maven 存儲庫映射到容器的 /root/.m2 目錄,這樣作的目的是讓本地存儲庫與 Docker 中的存儲庫共享內容,避免每次編譯的時候都要從網絡上下載大量依賴包,提高運行效率。數據庫

3、技術概述

3.一、架構組成

Pinpoint 架構

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

3.二、系統特點

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

3.三、分佈式追蹤系統如何工做

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

3.四、Pinpoint 的數據結構

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

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

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

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

Pinpoint ID 結構之間的關係

3.五、如何使用 Java Agent

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

# 使用 Java Agent
-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
-Dpinpoint.agentId=<Agent's UniqueId> -Dpinpoint.applicationName=<The name indicating a same service (AgentId collection)> 複製代碼

3.六、代碼注入是如何工做的

Pinpoint 字節碼注入

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

3.七、Pinpoint 的應用實例

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

Pinpoint 數據收集

4、Agent 插件開發

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

4.一、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.二、TraceMetadataProvider

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

4.2.一、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.二、AnnotationKey

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

屬性 描述
name AnnotationKey 的名稱
code AnnotationKey 的編碼,整形,必須惟一
properties 附加屬性

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

4.2.三、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.三、ProfilerPlugin

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

4.3.一、插件的工做原理

  1. Pinpoint Agent 隨 JVM 一塊兒啓動
  2. Agent 加載全部 plugin 目錄下的插件
  3. Agent 調用每一個已經加載的插件的 ProfilerPlugin.setup(ProfilerPluginSetupContext) 方法
  4. 在 setup 方法中,插件定義那些須要被轉換的類,並註冊 TransformerCallback
  5. 目標應用啓動
  6. 當類被加載的時候,Pinpoint Agent 會尋找註冊到該類的 TransformerCallback
  7. 若是 TransformerCallback 被註冊,Agent 就調用它的 doInTransform 方法
  8. TransformerCallback 修改目標類的字節碼 (例如添加攔截器、添加字段等)
  9. 修改後的代碼返回到 JVM,類型加載的時候就使用修改後的字節碼
  10. 應用程序繼續
  11. 當調用到被修改的方法的時候,攔截器的 before 和 after 方法被調用
  12. 攔截器記錄追蹤數據

Pinpoint 插件的工做原理看似跟 AOP 很是類似,但仍是有一些區別和自身特點的:

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

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

4.3.二、方法攔截

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

public class SamplePlugin implements ProfilerPlugin, TransformTemplateAware {

  private TransformTemplate transformTemplate;

  @Override
  public void setup(ProfilerPluginSetupContext context) {
  }

  @Override
  public void setTransformTemplate(TransformTemplate transformTemplate) {
    this.transformTemplate = transformTemplate;
  }

}
複製代碼

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

  1. 檢查 main class 是否是 org.apache.catalina.startup.Bootstrap
  2. 檢查是否有系統變量
    catalina.home
  3. 檢查是否存在某個指定的類 (對於檢測 Tomcat 而言,這個特定的類型也是 org.apache.catalina.startup.Bootstrap)

若是以上三個條件都知足,就把當前節點的 ServiceType 設置爲 Tomcat。

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

public byte[] doInTransform(Instrumentor instrumentor,
        ClassLoader classLoader,
        String className,
        Class<?>; classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) throws InstrumentException {

  // 1. Get InstrumentClass of the target class
  InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);

  // 2. Get InstrumentMethod of the target method.
  InstrumentMethod targetMethod = 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(); } 複製代碼
  1. 注入過程是從獲取 InstrumentClass 類開始的。
  2. 若是想要攔截一個方法,或者是添加字段以及 getter、setter 方法,就能夠調用 InstrumentClass 對應的 API 來實現,這裏是獲取了一個簽名爲 targetMethod(String) 的方法,返回的對象是 InstrumentMethod 類型。
  3. 調用 InstrumentMethod 的 addInterceptor 方法注入攔截器,全部跟蹤信息的收集行爲都是在攔截器中實現的,這裏添加的攔截器是 com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor,這是 Pinpoint 框架默認提供的一個基本攔截器,裏面收集了一些 targetMethod 的調用信息。後面的 va 是一個靜態方法 (表明可變參數列表),va 中給出的參數會傳遞到 BasicMethodInterceptor 的構造方法中。
  4. 調用 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, Throwable throwable);

}
複製代碼

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

public RecordArgsAndReturnValueInterceptor(TraceContext traceContext,
        MethodDescriptor descriptor) {

  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() 方法關閉區塊。

以上內容請參考該樣例

5、總結

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

相關文章
相關標籤/搜索