你可以編寫Pinpoint的Profiler 插件去擴展Profiling目標的覆蓋。在進入插件開發(fā)之前,最好去查看下Pinpoint插件的跟蹤數(shù)據(jù)記錄。
1. 追蹤數(shù)據(jù)
在Pinpoint中,一個事務(Transaction)包含一組Span。每一個Span代表事務(Transaction)經(jīng)過的單個邏輯點的跟蹤。
為了更形象的表達,假設下面有這樣一個系統(tǒng)。前端(FrontEnd)服務器接收到用戶的請求,然后發(fā)送給后端(BackEnd)查詢DB數(shù)據(jù)庫,在這些節(jié)點中,假設前端(FrontEnd)和后端(BackEnd)服務器有配置Pinpoint Agent。

當一個請求到達
后端(BackEnd)時,Pinpoint Agent會生成一個新的事務(Transaction)以及創(chuàng)建一個Span。為了處理請求,前端(FontEnd)會調(diào)用后端(BackEnd)服務器。這時候,Pinpoint Agent向調(diào)用消息注入事務ID(Transaction ID)(附加一些其他用于傳播的數(shù)據(jù))。當后端(BackEnd)接收到消息,它將會提取事務ID(Transaction ID)(及其他附加信息)去創(chuàng)建一個Span。其結(jié)果就是,所有的Span將會在一個事務(Transaction)中共享其事務ID(Transaction ID)。一個
Span記錄著重要的方法調(diào)用及相關的數(shù)據(jù)(參數(shù),返回值,及其他),然后將它們封裝為SpanEvents,類似于調(diào)用棧的表示方式。Span本身以及每個SpanEvents表示一個方法調(diào)用。Span和SpanEvent有很多字段,但是大多數(shù)字段都是內(nèi)部處理的通過Pinpoint Agent及大部分插件開發(fā)者不需要關心它們。但是對于這些字段,開發(fā)者必須處理的信息如下:
2. Pinpoint 插件結(jié)構(gòu)
Pinpoint Plugin由TraceMetadataProvider 和ProfilerPlugin組成實現(xiàn)。
-
TraceMetadataProvider:實現(xiàn)提供ServiceType和AnnotationKey對Pinpoint Agent,Web,采集器(Collector)。 -
ProfilerPlugin:實現(xiàn)通過Pinpoint Agent轉(zhuǎn)化成目標類進行記錄跟蹤數(shù)據(jù)。
對于的文件如下:
- META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
- META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
類似如下:

2.1 TraceMetadataProvider
TraceMetadataProvider實現(xiàn)規(guī)定ServiceTypes和AnnotationKeys。
2.1.1 ServiceType
每一個Span和SpanEvent都包含ServiceType。ServiceType表示跟蹤的方法屬于哪個類庫,以及如何處理Span和SpanEvent。
下面顯示ServiceType的屬性。
| 屬性 | 描述 |
|---|---|
| name |
ServiceType的名稱,必須是唯一的 |
| code | short類型的值,必須是唯一的 |
| desc | 描述信息 |
| properties | 屬性 |
ServiceType值必須使用適當?shù)姆秶鷼w類它,下面這張表展示了其分類和對應的value范圍:
| 分類 | 范圍 |
|---|---|
| 內(nèi)部使用 | 0~999 |
| 服務 | 1000~1999 |
| 數(shù)據(jù)庫客戶端 | 2000~2999 |
| 緩存客戶端 | 8000~8999 |
| RPC客戶端 | 9000~9999 |
| 其他 | 5000~7999 |
ServiceType碼必須唯一,所以,當你想寫一個插件并將它公布出去,你必須聯(lián)系Pinpoint開發(fā)團隊去分配一個ServiceType碼定義。如果你的插件僅是私自的使用,你可以隨意使用下邊表的數(shù)值
| 種類 | 范圍 |
|---|---|
| Server | 1900~1999 |
| DB Client | 2900~2999 |
| Cache Client | 8900~8999 |
| RPC Client | 9900~9999 |
| 其他 | 7500~7999 |
ServiceType可以具有以下屬性:
| 屬性 | 描述 |
|---|---|
| TERMINAL |
Span或者SpanEvent調(diào)用遠程節(jié)點,但是該遠程節(jié)點無法用Pinpoint跟蹤 |
| INCLUDE_DESTINATION_ID |
Span或者SpanEvent記錄目的端的iddestination id但遠程服務不是可追蹤類型 |
| RECORD_STATISTICS | Pinpoint Collector應該收集這個Span或SpanEvent的執(zhí)行時間統(tǒng)計信息 |
2.1.2 AnnotationKey
你可以給Spans或者SpanEvents標記更多的信息。一個Annotation是一個鍵值對,其中鍵是一個AnnotationKey類型,值是一個原始類型,字符串或字節(jié)數(shù)組。對于常用的Annotation類型,有預先定義了AnnotationKey,但是如果覺得不夠,可以在TraceMetadataProvider中定義你自己的鍵。
| 屬性 | 描述 |
|---|---|
| name |
AnnotationKey的名稱 |
| code |
AnnotationKey的唯一整型編號 |
| properties | 屬性 |
如果你的公共插件要添加一個新的AnnotationKey,你必須聯(lián)系Pinpoint 開發(fā)團隊來分配一個AnnotationKey代碼。如果你的插件是私人使用的,你可以選擇一個介于900到999之間的值作為AnnotationKey代碼。
下面的表展示了AnnotationKey屬性:
| 屬性 | 描述 |
|---|---|
| VIEW_IN_RECORD_SET | 展示注解在call tree上 |
| ERROR_API_METADATA | 該屬性不是插件的 |
你也可以通過ServiceType來傳遞AnnotationKeyMatcher (TraceMetadata.addServiceType(ServiceType, AnnotationKeyMatcher) 。如果你通過這種方式傳遞一個AnnotationKeyMatcher,那么當ServiceType的Span或SpanEvent在事務調(diào)用樹中顯示時,匹配的Annotation將顯示為典型的Annotation。
2.2 ProfilerPlugin
ProfilerPlugin修改目標庫類來完成采集追蹤數(shù)據(jù)。
ProfilerPlugin按照如下順序進行:
- 在JVM啟動時,Pinpoint Agent啟動
- Pinpoint Agent將會加載
plugin目錄下的全部插件 - Pinpoint Agent將會在每個插件調(diào)用
ProfilerPlugin.setup(ProfilerPluginSetupContext)。 - 在
setup()方法中,插件定義了需要轉(zhuǎn)換的類,并注冊了一個TransformerCallback。 - 目標應用啟動。
- 每當裝載一個類時,Pinpoint Agent就會查找為該類注冊的
TransformerCallback。 - 如果注冊了
TransformerCallback,代理將調(diào)用它的doInTransform()方法。 -
TransformerCallback修改目標類的字節(jié)代碼。(例如,添加攔截器、添加字段等) - 修改后的字節(jié)代碼返回給JVM,并用返回的字節(jié)代碼加載類。
- 應用繼續(xù)運行。
- 當調(diào)用修改的方法時,將調(diào)用注入的攔截器的
before和after方法。 - 攔截器記錄追蹤的數(shù)據(jù)。
最重要的要考慮的點可以歸納為:
- 確定哪些方法足夠必須,需要去跟蹤。
- 注入攔截器去追蹤這些方法。
這些攔截器用來在發(fā)送到Collector之前的數(shù)據(jù)提取,存儲和傳遞跟蹤數(shù)據(jù)。攔截器甚至可以互相協(xié)作,共享上下文。插件還可以向目標類添加getter甚至自定義字段幫助去幫助跟蹤,以便攔截器可以在執(zhí)行期間訪問它們。Pinpoint插件示例展示了TransformerCallback如何修改類,以及注入的攔截器如何跟蹤方法
現(xiàn)在我們將描述攔截器必須做什么來跟蹤不同類型的方法。
2.2.1 Plain method
純方法指的不是節(jié)點的頂級方法,或者與遠程或異步調(diào)用無關的任何方法。示例2展示了如何跟蹤這些普通方法。
2.2.2 Top level method of a node
節(jié)點的頂級方法是其攔截器在節(jié)點中開始新跟蹤的方法。這些方法通常是rpc的接受器,跟蹤記錄為一個Span, ServiceType分類為服務器。
如何記錄Span取決于事務是否已經(jīng)在之前的任何節(jié)點上開始。
2.2.2.1 New transaction
如果當前節(jié)點是記錄事務的第一個節(jié)點,則必須發(fā)出一個新的事務id并記錄它。newtraceobject()將自動處理此任務,因此只需調(diào)用它。
2.2.2.2 Continue Transaction
如果請求來自Pinpoint 代理的另一個節(jié)點,那么事務將已經(jīng)發(fā)出一個事務id;您必須將下面的數(shù)據(jù)記錄到Span中。(這些數(shù)據(jù)中的大多數(shù)是從前面的節(jié)點發(fā)送的,通常打包在請求消息中)
| 名稱 | 描述 |
|---|---|
| transactionId | Transaction ID |
| parentSpanId | 父節(jié)點的Span ID |
| parentApplicationName | 父節(jié)點的應用名 |
| parentApplicationType | 父節(jié)點的應用類型 |
| rpc | 程序名(可選) |
| endPoint | 服務器當前節(jié)點地址 |
| remoteAddr | 調(diào)用者的地址 |
| acceptorHost | 客戶端使用的服務器地址 |
Pinpoint使用acceptorHost查找節(jié)點之間的調(diào)用者-被調(diào)用者關系。在大多數(shù)情況下,acceptorHost與endPoint是相同的。然而,客戶機發(fā)送請求到的地址有時可能與服務器接收請求(代理)的地址不同。要處理這種情況,您必須記錄實際地址的客戶端發(fā)送請求作為acceptorHost。通常,客戶端插件會將這個地址添加到請求消息中,并與事務數(shù)據(jù)一起添加。
此外,還必須使用上一個節(jié)點發(fā)出和發(fā)送的span id。
有時,前一個節(jié)點標記不跟蹤的事務。在這種情況下,你不能跟蹤事務。
正如你所看到的,客戶端插件必須向服務器插件傳遞許多數(shù)據(jù)。如何做到這一點取決于協(xié)議。
你可以在這里找到一個頂級方法服務器攔截器的示例。
2.2.3 Methods invoking a remote node
調(diào)用遠程節(jié)點的方法的攔截器必須記錄以下數(shù)據(jù):
| 名稱 | 描述 |
|---|---|
| endPoint | 目的的服務端地址 |
| destinationId | 目標的邏輯名稱 |
| rpc | 調(diào)用的程序名(可選) |
| nextSpanId | 下個節(jié)點的Span Id(如果需要跟蹤通過Pinpoint) |
2.2.3.1 If the next node is traceable
如果下一個節(jié)點是可跟蹤的,那么攔截器必須將以下數(shù)據(jù)傳播到下一個節(jié)點。如何傳遞它們?nèi)Q于協(xié)議,在最壞的情況下可能根本不可能傳遞它們。
| 名稱 | 描述 |
|---|---|
| transactionId | Transaction ID |
| parentSpanId | 當前節(jié)點的Span ID |
| parentApplicationName | 當前節(jié)點的應用名 |
| parentApplicationType | 當前節(jié)點的應用類型 |
通過匹配客戶端跟蹤的destinationId和服務器跟蹤的acceptorHost,查明調(diào)用者和被調(diào)用者之間的關系。因此客戶端插件必須記錄destinationId,而服務器插件必須記錄相同值的acceptorHost。如果服務器自己無法獲取該值,客戶端插件必須將其傳遞給服務器。
攔截器記錄的ServiceType必須來自RPC client類別。
可以在這里找到這些攔截器的示例。
2.2.3.2 If the next node is not traceable
如果下一個節(jié)點是不可跟蹤的,那么ServiceType必須具有TERMINAL屬性。
如果你想記錄destinationId,它還必須有INCLUDE_DESTINATION_ID屬性。如果你記錄destinationId,服務器映射將顯示每個destinationId節(jié)點,即使它們有相同的端點。
另外,ServiceType必須是一個DB客戶端或緩存客戶端類別。注意,你不需要擔心術語“DB”或“緩存”,因為任何插件跟蹤一個客戶端庫與不可跟蹤的目標服務器可能會使用它們?!癉B”和“Cache”之間的唯一區(qū)別是響應時間直方圖的時間范圍(“Cache”在直方圖中間隔更小)。
2.3.4 Asynchronous task
跟蹤對象被綁定到第一次通過ThreadLocal創(chuàng)建它們的線程,每當執(zhí)行跨越線程邊界時,跟蹤對象就會丟失到新線程。因此,為了跨線程邊界跟蹤任務,必須負責將當前跟蹤上下文傳遞給新線程。這是通過將AsyncContext注入到由調(diào)用線程和執(zhí)行線程共享的對象中來實現(xiàn)的。
調(diào)用線程從當前跟蹤創(chuàng)建一個AsyncContext,并將其注入一個將被傳遞給執(zhí)行線程的對象中。然后,執(zhí)行線程從對象中檢索AsyncContext,從中創(chuàng)建一個新的跟蹤,并將其綁定到自己的ThreadLocal。
因此,必須為兩個方法創(chuàng)建攔截器:
- 一個用于初始化任務(調(diào)用線程)。
- 另一個用于實際處理任務(執(zhí)行線程)。
初始化方法的攔截器必須發(fā)出一個AsyncContext并將其傳遞給處理方法。如何傳遞這個值取決于目標庫。在最壞的情況下,可能根本無法通過它。
然后,處理方法的攔截器必須使用傳播的AsyncContext繼續(xù)跟蹤,并將其綁定到自己的線程。但是,強烈建議簡單地擴展AsyncContextSpanEventSimpleAroundInterceptor,這樣不必手動處理了。
請記住,因為共享對象必須能夠?qū)?code>AsyncContext注入其中,所以必須在類轉(zhuǎn)換期間使用AsyncContextAccessor添加字段??梢栽谶@里找到一個跟蹤異步任務的示例。
2.3.5 Case Study: HTTP
HTTP客戶機是調(diào)用遠程節(jié)點(客戶機)的方法的示例,而HTTP服務器是節(jié)點(服務器)的頂級方法的示例。如前所述,客戶端插件必須有將事務數(shù)據(jù)傳遞給服務器插件的方法來繼續(xù)跟蹤。注意,這個實現(xiàn)是依賴于協(xié)議的,HttpClient3插件的HttpMethodBaseExecuteMethodInterceptor和Tomcat插件的StandardHostValveInvokeInterceptor顯示了一個HTTP的工作示例:
- 將事務數(shù)據(jù)作為HTTP頭傳遞。您可以在這里找到標題名稱
- 客戶端插件記錄服務器的
IP:Port為destinationId。 - 客戶端插件將
destinationId值作為頭傳遞給服務器作為Header.HTTP_HOST頭。 - 服務器插件
Header.HTTP_HOST頭值作為acceptorHost。
必須記住的另一件事是,使用相同協(xié)議的所有客戶機和服務器必須以相同的方式傳遞事務數(shù)據(jù),以確保兼容性。因此,如果你正在編寫其他HTTP客戶端或服務器的插件,你的插件必須如上所述記錄和傳遞交易數(shù)據(jù)。