運行 Pinpoint 系統最簡單的辦法是使用 Docker。java
# 運行 Pinpoint
$ git clone https://github.com/dawidmalina/docker-pinpoint
$ cd docker-pinpoint
$ docker-compose up -d
複製代碼
編譯 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
# 編譯 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 中的存儲庫共享內容,避免每次編譯的時候都要從網絡上下載大量依賴包,提高運行效率。數據庫
Pinpoint 主要由 3 個組件外加 Hbase 數據庫構成,三個組件分別爲:Agent、Collector 和 Web UI。apache
不管 Pinpoint 仍是 Zipkin,都是基於 Google Dapper 的論文實現的。其核心思想就是在服務各節點彼此調用的時候,記錄並傳遞一個應用級別的標記,這個標記能夠用來關聯各個服務節點之間的關係。好比兩個節點之間使用 HTTP 做爲請求協議的話,那麼這些標記就會被加入到 HTTP 頭中。所以如何傳遞這些標記是與節點之間使用的通信協議有關的,有些協議就很容易加入這樣的內容,但有些協議就相對困難甚至不可能,所以這一點就直接決定了實現分佈式追蹤系統的難度。bootstrap
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 的優點在於其使用 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)> 複製代碼
Pinpoint 對代碼注入的封裝很是相似 AOP,當一個類被加載的時候會經過 Interceptor 向指定的方法先後注入 before 和 after 邏輯,在這些邏輯中能夠獲取系統運行的狀態,並經過 TraceContext 建立 Trace 消息,併發送給 Pinpoint 服務器。但與 AOP 不一樣的是,Pinpoint 在封裝接口的時候考慮到了更多與目標代碼的交互能力,所以用 Pinpoint 提供的 API 來編寫注入邏輯會比 AOP 看起來更加容易和專業。(這些內容後面會有更詳細說明)
下圖展示了兩個 Tomcat 服務器應用了 Pinpoint 以後,所收集到的追蹤數據。
開發 Pinpoint Agent 插件只須要關注兩個接口:TraceMetadataProvider 和 ProfilerPlugin,實現類經過 Java 的服務發現機制進行加載。
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
複製代碼
這兩個文件中的每一行都寫明對應實現類的全名稱便可。
TraceMetadataProvider 提供了對 ServiceType 和 AnnotationKey 的管理。
每一個 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 |
Annotation 是包含在 Span 和 SpanEvent 中的更詳盡的數據,以鍵值對的形式存在,鍵就是 AnnotationKey,值能夠是字符串或字節數組。Pinpoint 內置了不少的 AnnotationKey,若是不夠用的話也能夠經過 TraceMetadataProvider 來自定義。AnnotationKey 的數據結構以下:
屬性 | 描述 |
---|---|
name | AnnotationKey 的名稱 |
code | AnnotationKey 的編碼,整形,必須惟一 |
properties | 附加屬性 |
同 ServiceType 的 code 字段同樣,AnnotationKey 的 code 字段也是全局惟一的,Pinpoint 團隊給出的私有區域範圍是 900 到 999。
TraceMetadataProvider 接口只有一個 setup 方法,此方法接收一個 TraceMetadataSetupContext 類型的參數,該類型有三個方法:
方法 | 描述 |
---|---|
addServiceType(ServiceType) | 註冊 ServiceType |
addServiceType(ServiceType, AnnotationKeyMatcher) | 註冊 ServiceType,並將匹配 AnnotationKeyMatcher 的 AnnotationKey 做爲此 ServiceType 的典型註解,這些典型註解會顯示在瀑布視圖的 Argument 列中 |
addAnnotationKey(AnnotationKey) | 註冊 AnnotationKey,這裏註冊的 AnnotationKey 會被標記爲 VIEW_IN_RECORD_SET,顯示在瀑布視圖中是以單獨一行顯示的,且前面有一個藍色的 i 圖標 |
詳細使用方法能夠參考官方提供的樣例文件 SampleTraceMetadataProvider。
ProfilerPlugin 經過字節碼注入的方式攔截目標代碼以實現跟蹤數據的收集。
plugin
目錄下的插件Pinpoint 插件的工做原理看似跟 AOP 很是類似,但仍是有一些區別和自身特點的:
經過上述內容能夠了解,若是要編寫一個 Pinpoint 的插件,除了要對目標代碼的調用邏輯有較深刻的理解,還必須得設計好上下文數據如何存儲、如何傳遞,以及如何經過 Scope 避免信息被重複收集等問題。這些問題在 AOP 的場景下也會存在,只是 Pinpoint 提供了更加一致和便捷的解決方案,而基於 AOP 的實現就要本身去考慮這些問題了。
如前文所述,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 的:
若是以上三個條件都知足,就把當前節點的 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(); } 複製代碼
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() 方法關閉區塊。
以上內容請參考該樣例。
其實 Pinpint 的插件開發 API 還提供了很是豐富的能力,如攔截異步方法、調用鏈跟蹤、攔截器之間共享數據等等,但原理都是基於上述這些內容,只是調用了更加複雜的 API 而已。具體代碼能夠參考官方提供的樣例項目,裏面有很是詳盡的代碼及註釋,相信理解了上面這些內容,再看這個代碼就不會有任何困難了。