Pinpoint技術概述

原文鏈接: 這裏

另一篇完美的介紹:這裏


注: 內容翻譯自官方文檔 Technical Overview Of Pinpoint, 內容有點長,但是強烈推薦閱讀!基本上這是目前pinpoint唯一的一份詳細介紹設計和實現的資料。

Pinpoint是一個分析大型分佈式系統的平臺,提供解決方案來處理海量跟蹤數據。2012年七月開始開發,2015年1月9日作爲開源項目啓動。

本文將介紹Pinpoint: 什麼促使我們開始搭建它, 用了什麼技術, 還有Pinpoint agent是如何優化的。

開始動機 & Pinpoint特點

和如今相比, 過去的因特網的用戶數量相對較小,而因特網服務的架構也沒那麼複雜。web服務通常使用兩層(web 服務器和數據庫)或三層(web服務器,應用服務器和數據庫)架構。然而在如今,隨着互聯網的成長,需要支持大量的併發連接,並且需要將功能和服務有機結合,導致更加複雜的軟件棧組合。更確切地說,比三層層次更多的n層架構變得更加普遍。SOA或者微服務架構成爲現實。

系統的複雜度因此提升。系統越複雜,越難解決問題,例如系統失敗或者性能問題。在三層架構中找到解決方案還不是太難,僅僅需要分析3個組件比如web服務器,應用服務器和數據庫,而服務器數量也不多。但是,如果問題發生在n層架構中,就需要調查大量的組件和服務器。另一個問題是僅僅分析單個組件很難看到大局;當發生一個低可見度的問題時,系統複雜度越高,就需要更長的時間來查找原因。最糟糕的是,某些情況下我們甚至可能無法查找出來。

這樣的問題也發生在NAVER的系統中。使用了大量工具如應用性能管理(APM)但是還不足以有效處理問題。因此我們最終決定爲n層架構開發新的跟蹤平臺,爲n層架構的系統提供解決方案。

Pinpoint, 2012年七月開始開發,在2015年1月作爲一個開源項目啓動, 是一個爲大型分佈式系統服務的n層架構跟蹤平臺。 Pinpoint的特點如下:

  • 分佈式事務跟蹤,跟蹤跨分佈式應用的消息

  • 自動檢測應用拓撲,幫助你搞清楚應用的架構

  • 水平擴展以便支持大規模服務器集羣

  • 提供代碼級別的可見性以便輕鬆定位失敗點和瓶頸

  • 使用字節碼增強技術,添加新功能而無需修改代碼

本文將講述Pinpoint的技術,例如事務跟蹤和字節碼增強。還會解釋應用在pinpoint agent中的優化方法,agent修改字節碼並記錄性能數據。

分佈式事務跟蹤,基於google Dapper

pinpoint跟蹤單個事務中的分佈式請求,基於google Dapper。

在Google Dapper中分佈式事務追蹤是如何工作的

當一個消息從Node1發送到Node2(見圖1)時,分佈式追蹤系統的核心是在分佈式系統中識別在Node1中處理的消息和在Node2中出的消息之間的關係。

圖1. 分佈式系統中的消息關係

問題在於無法在消息之間識別關係。例如,我們無法識別從Node1發送的第N個消息和Node2接收到的N'消息之間的關係。換句話說,當Node1發送完第X個消息時,是無法在Node2接收到的N的消息裏面識別出第X個消息的。有一種方式試圖在TCP或者操作系統層面追蹤消息。但是,實現很複雜而且性能低下,而且需要爲每個協議單獨實現。另外,很難精確追蹤消息。

不過,Google dapper實現了一個簡單的解決方案來解決這個問題。這個解決方案通過在發送消息時添加應用級別的標籤作爲消息之間的關聯。例如,在HTTP請求中的HTTP header中爲消息添加一個標籤信息並使用這個標籤跟蹤消息。

Google's Dapper

關於Google Dapper的更多信息, 請見 "Dapper, a Large-Scale Distributed Systems Tracing Infrastructure."

Pinpoint基於google dapper的跟蹤技術,但是已經修改爲在調用的header中添加應用級別標籤數據以便在遠程調用中跟蹤分佈式事務。標籤數據由多個key組成,定義爲TraceId。

Pinpoint中的數據結構

Pinpoint中,核心數據結構由Span, Trace, 和 TraceId組成。

  • Span: RPC (遠程過程調用/remote procedure call)跟蹤的基本單元; 當一個RPC調用到達時指示工作已經處理完成幷包含跟蹤數據。爲了確保代碼級別的可見性,Span擁有帶SpanEvent標籤的子結構作爲數據結構。每個Span包含一個TraceId。

  • Trace: 多個Span的集合; 由關聯的RPC (Spans)組成. 在同一個trace中的span共享相同的TransactionId。Trace通過SpanId和ParentSpanId整理爲繼承樹結構.

  • TraceId: 由 TransactionId, SpanId, 和 ParentSpanId 組成的key的集合. TransactionId 指明消息ID,而SpanId 和 ParentSpanId 表示RPC的父-子關係。

    • TransactionId (TxId): 在分佈式系統間單個事務發送/接收的消息的ID; 必須跨整個服務器集羣做到全局唯一.

    • SpanId: 當收到RPC消息時處理的工作的ID; 在RPC請求到達節點時生成。

    • ParentSpanId (pSpanId): 發起RPC調用的父span的SpanId. 如果節點是事務的起點,這裏將沒有父span - 對於這種情況, 使用值-1來表示這個span是事務的根span。

Google Dapper 和 NAVER Pinpoint在術語上的不同

Pinpoint中的術語"TransactionId"和google dapper中的術語"TraceId"有相同的含義。而Pinpoint中的術語"TraceId"引用到多個key的集合。

TraceId如何工作

下圖描述TraceId的行爲,在4個節點之間執行了3次的RPC調用:

圖2: TraceId行爲示例

在圖2中,TransactionId (TxId) 體現了三次不同的RPC作爲單個事務被相互關聯。但是,TransactionId 本身不能精確描述PRC之間的關係。爲了識別PRC之間的關係,需要SpanId 和 ParentSpanId (pSpanId). 假設一個節點是Tomcat,可以將SpanId想象爲處理HTTP請求的線程,ParentSpanId代表發起這個RPC調用的SpanId.

使用TransactionId,Pinpoint可以發現關聯的n個Span,並使用SpanId和ParentSpanId將這n個span排列爲繼承樹結構。

SpanId 和 ParentSpanId 是 64位長度的整型。可能發生衝突,因爲這個數字是任意生成的,但是考慮到值的範圍可以從-9223372036854775808到9223372036854775807,不太可能發生衝突. 如果key之間出現衝突,Pinpoint和Google Dapper系統,會讓開發人員知道發生了什麼,而不是解決衝突。

TransactionId 由 AgentIds, JVM (java虛擬機)啓動時間, 和 SequenceNumbers/序列號組成.

  • AgentId: 當Jvm啓動時用戶創建的ID; 必須在pinpoinit安裝的全部服務器集羣中全局唯一. 最簡單的讓它保持唯一的方法是使用hostname($HOSTNAME),因爲hostname一般不會重複. 如果需要在服務器集羣中運行多個JVM,請在hostname前面增加一個前綴來避免重複。

  • JVM 啓動時間: 需要用來保證從0開始的SequenceNumber的唯一性. 當用戶錯誤的創建了重複的AgentId時這個值可以用來預防ID衝突。

  • SequenceNumber: Pinpoint agent 生成的ID, 從0開始連續自增;爲每個消息生成一個.

Dapper 和 Zipkin, Twitter的一個分佈式系統跟蹤平臺, 生成隨機TraceIds (Pinpoint是TransactionIds) 並將衝突情況視爲正常。然而, 在Pinpiont中我們想避免衝突的可能,因此實現了上面描述的系統。有兩種選擇:一是數據量小但是衝突的可能性高,二是數據量大但是衝突的可能性低。我們選擇了第二種。

可能有更好的方式來處理transaction。我們起先有一個想法,通過中央key服務器來生成key。如果實現這個模式,可能導致性能問題和網絡錯誤。因此,大量生成key被考慮作爲備選。後面這個方法可能被開發。現在採用簡單方法。在pinpoint中,TransactionId被當成可變數據來對待。

字節碼增強,無需代碼修改

前面我們解釋了分佈式事務跟蹤。實現的方法之一是開發人員自己修改代碼。當發生RPC調用時容許開發人員添加標籤信息。但是,修改代碼會成爲包袱,即使這樣的功能對開發人員非常有用。

Twitter的 Zipkin 使用修改過的類庫和它自己的容器(Finagle)來提供分佈式事務跟蹤的功能。但是,它要求在需要時修改代碼。我們期望功能可以不修改代碼就工作並希望得到代碼級別的可見性。爲了解決這個問題,pinpoint中使用了字節碼增強技術。Pinpoint agent干預發起RPC的代碼以此來自動處理標籤信息。

克服字節碼增強的缺點

字節碼增強在手工方法和自動方法兩者之間屬於自動方法。

  • 手工方法: 開發人員使用ponpoint提供的API在關鍵點開發記錄數據的代碼

  • 自動方法: 開發人員不需要代碼改動,因爲pinpoint決定了哪些API要調節和開發

下面是每個方法的優點和缺點:

Table1 每個方法的優缺點


優點缺點
手工跟蹤1. 要求更少開發資源 2. API可以更簡單並最終減少bug的數量1. 開發人員必須修改代碼 2. 跟蹤級別低
自動跟蹤1. 開發人員不需要修改代碼 2. 可以收集到更多精確的數據因爲有字節碼中的更多信息1. 在開發pinpoint時,和實現一個手工方法相比,需要10倍開銷來實現一個自動方法 2. 需要更高能力的開發人員,可以立即識別需要跟蹤的類庫代碼並決定跟蹤點 3. 增加bug發生的可能性,因爲使用瞭如字節碼增強這樣的高級開發技巧

字節碼增強是一種高難度和高風險的技術。但是,綜合考慮使用這種技術開所需要的資源和難度,使用它仍然有很多的益處。

雖然它需要大量的開發資源,在開發服務上它需要很少的資源。例如,下面展示了使用字節碼增強的自動方法和使用類庫的手工方法(在這裏的上下文中,開銷是爲澄清而假設的隨機數)之間的開銷。

  • 自動方法: 總共 100

    • Pinpoint開發開銷: 100

    • 服務實施的開銷: 0

  • 手工方法: 總共 30

    • Pinpoint開發開銷: 20

    • 服務實施的開銷: 10

上面的數據告訴我們手工方法比自動方法有更合算。但是,不適用於我們的在NAVER的環境。在NAVER我們有幾千個服務,因此在上面的數據中需要修改用於服務實施的開銷。如果我們有10個服務需要修改,總開銷計算如下:

Pinpoint開發開銷 20 + 服務實施開銷 10 x 10 = 120

基於這個結果,自動方法是一個更合算的方式。

我們很幸運的在pinpoint團隊中擁有很多高能力而專注於Java的開發人員。因此,我們相信克服pinpoint開發中的技術難題只是個時間問題。

字節碼增強的價值

我們選擇字節碼增強的理由,除了前面描述的那些外,還有下面的強有力的觀點:

隱藏API

一旦API被暴露給開發人員使用,我們作爲API的提供者,就不能隨意的修改API。這樣的限制會給我們增加壓力。

我們可能修改API來糾正錯誤設計或者添加新的功能。但是,如果做這些受到限制,對我們來說很難改進API。解決這個問題的最好的答案是一個可升級的系統設計,而每個人都知道這不是一個容易的選擇。如果我們不能掌控未來,就不可能創建完美的API設計。

而使用字節碼增強技術,我們就不必擔心暴露跟蹤API而可以持續改進設計,不用考慮依賴關係。對於那些計劃使用pinpoint開發應用的人,換一句話說,這代表對於pinpoint開發人員,API是可變的。現在,我們將保留隱藏API的想法,因爲改進性能和設計是我們的第一優先級。

容易啓用或者禁用

使用字節碼增強的缺點是當Pinpoint自身類庫的採樣代碼出現問題時可能影響應用。不過,可以通過啓用或者禁用pinpoint來解決問題,很簡單,因爲不需要修改代碼。

通過增加下面三行到JVM啓動腳本中就可以輕易的爲應用啓用pinpoint:

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
-Dpinpoint.agentId=<Agent's UniqueId>
-Dpinpoint.applicationName=<The name indicating a same service (AgentId collection)>

如果因爲pinpoint發生問題,只需要在JVM啓動腳本中刪除這些配置數據。

字節碼如何工作

由於字節碼增強技術處理java字節碼, 有增加開發風險的趨勢,同時會降低效率。另外,開發人員更容易犯錯。在pinpoint,我們通過抽象出攔截器(interceptor)來改進效率和可達性(accessibility)。pinpoint在類裝載時通過介入應用代碼爲分佈式事務和性能信息注入必要的跟蹤代碼。這會提升性能,因爲代碼注入是在應用代碼中直接實施的。

圖3: 字節碼增強行爲

在pinpoint中,攔截器API在性能數據被記錄的地方分開(separated)。爲了跟蹤,我們添加攔截器到目標方法使得before()方法和after()方法被調用,並在before()方法和after()方法中實現了部分性能數據的記錄。使用字節碼增強,pinpoint agent可以記錄需要方法的數據,只有這樣採樣數據的大小才能變小。

pinpoint agent的性能優化

最後,我們描述用於pinpoint agent的性能優化的方式。

使用二進制格式(thrift)

通過使用二進制格式(thrift)可以提高編碼速度,雖然它使用和調試要難一些。也有利於減少網絡使用,因爲生成的數據比較小。

使用變長編碼和格式優化數據記錄

如果將一個長整型轉換爲固定長度的字符串, 數據大小一般是8個字節。然而,如果你用變長編碼,數據大小可以是從1到10個字符,取決於給定數字的大小。爲了減小數據大小,pinpoint使用thrift的CompactProtocol協議(壓縮協議)來編碼數據,因爲變長字符串和記錄數據可以爲編碼格式做優化。pinpoint agent通過基於跟蹤的根方法的時間開始來轉換其他的時間來減少數據大小。

圖4 說明了上面章節描述的想法:

圖4: 固定長度編碼和可變長度編碼的對比

爲了得到關於三個不同方法(見圖4)被調用時間的數據,不得不在6個不同的點上測量時間,用固定長度編碼這需要48個字節(6 * 8)。

以此同時,pinpoint agent 使用可變長度編碼並根據對應的格式記錄數據。然後在其他時間點通過和參考點比較來計算時間值(在vector中),根方法的起點被確認爲參考點。這只需要佔用少量的字節,因爲vector使用小數字。圖4中消耗了13個字節。

如果執行方法花費了更多時間,即使使用可變長度編碼也會增加字節數量。但是,依然比固定長度編碼更有效率。

用常量表替換重複的API信息,SQL語句和字符串

我們希望pinpoint能開啓代碼級別的跟蹤。然而,存在增大數據大小的問題。每次高精度的數據被髮送到服務器將增大數據大小,導致增加網絡使用。

爲了解決這個問題,我們使用了在遠程HBase中創建常量表的策略。例如,每次調用"Method A"的信息被髮送到pinpoint collector, 數據大小將很大。pinpoint agent 用一個ID替換"method A",在HBase中作爲一個常量表保存ID和"method A"的信息,然後用ID生成跟蹤數據。然後當用戶在網站上獲取跟蹤數據時,pinpoint web在常量表中搜索對應ID的方法信息並組合他們。使用同樣的方式來減少SQL或者頻繁使用的字符串的數據大小。

處理大量請求的採樣

我們在線門戶服務有海量請求。單個服務每天處理超過200億請求。容易跟蹤這樣的請求:方法是添加足夠多的網絡設施和服務器來跟蹤請求並擴展服務器來收集數據。然後,這不是處理這種場景的合算的方法,僅僅是浪費金錢和資源。

在Pinpoint,可以收集採樣資料而不必跟蹤每個請求。在開發環境中請求量很小,每個數據都收集。而在產品環境請求量巨大,收集小比率的數據如1~5%,足夠檢查整個應用的狀態。有采樣後,可以最小化應用的網絡開銷並降低諸如網絡和服務器的設施費用。

pinpoint採樣方法

Pinpoint 支持計數採樣,如果設置爲10則只採樣10分之一的請求。我們計劃增加新的採樣器來更有效率的收集數據。

注:對應的配置項在agent下的pinpoint.config文件中,默認"profiler.sampling.rate=1"表示全部

使用異步數據傳輸來最小化應用線程中止

pinpoint不阻塞應用線程,因爲編碼後的數據或者遠程消息被其他線程異步傳輸。

使用UDP傳輸數據

和gogole dapper不同,pinpoint通過網絡傳輸數據來確保數據速度。作爲一個服務間使用的通用設施,當數據通訊持續突發時網絡會成爲問題。在這種情況下,pinpoint agent使用UDP協議來給服務讓出網絡連接優先級。

注意

數據傳輸API可以被替換,因爲它是接口分離的。可以修改實現爲用其他方式存儲數據,比如本地文件。

pinpoint應用示例

這裏給出一個例子關於如何從應用獲取數據,這樣就可以全面的理解前面講述的內容。

圖5 展示了當在 TomcatA 和 TomcatB 中安裝pinpoint的數據。可以把單個節點的跟蹤數據看成single traction,提現分佈式事務跟蹤的流程。

圖5:示例1:pinpoint應用

下面闡述pinpoint爲每個方法做了什麼:

  1. 當請求到達TomcatA時, Pinpoint agent 產生一個 TraceId.

  • TX_ID: TomcatA^TIME^1

  • SpanId: 10

  • ParentSpanId: -1(Root)

  • 從spring MVC 控制器中記錄數據

  • 插入HttpClient.execute()方法的調用並在HTTPGet中配置TraceId

    • HttpGet.setHeader(PINPOINT_TX_ID, "TomcatA^TIME^1")

    • HttpGet.setHeader(PINPOINT_SPAN_ID, "20")

    • HttpGet.setHeader(PINPOINT_PARENT_SPAN_ID, "10")

    • TX_ID: TomcatA^TIME^1 -> TomcatA^TIME^1

    • SPAN_ID: 10 -> 20

    • PARENT_SPAN_ID: -1 -> 10 (父 SpanId)

    • 創建一個子TraceId

    • 在HTTP header中配置子 TraceId

  • 傳輸打好tag的請求到TomcatB.

    • TX_ID: TomcatA^TIME^1

    • SPAN_ID: 20

    • PARENT_SPAN_ID: 10

    • TomcatB 檢查傳輸過來的請求的header

      HttpServletRequest.getHeader(PINPOINT_TX_ID)

    • TomcatB 作爲子節點工作因爲它識別了header中的TraceId

  • 從spring mvc控制器中記錄數據並完成請求

    圖6 示例2:pinpoint應用

  • 當從tomcatB回來的請求完成時,pinpoint agent發送跟蹤數據到pinpoint collector就此存儲在HBase中

  • 在對tomcatB的HTTP調用結束後,TomcatA的請求也完成了。pinpoint agent發送跟蹤數據到pinpoint collector就此存儲在HBase中

  • UI從HBase中讀取跟蹤數據並通過排序樹來創建調用棧

  • 結論

    pinpoint是和應用一起運行的另外的應用。使用字節碼增強使得pinpoint看上去不需要代碼修改。通常,字節碼增強技術讓應用容易造成風險。如果問題發生在pinpoint中,它會影響應用。目前,我們專注於改進pinpoint的性能和設計,而不是移除這樣的威脅,因爲我們任務這些讓pinpoint更加有價值。因此你需要決定是否使用pinpoint。

    我們還是有大量的工作需要完成來改進pinpoint,儘管不完整,pinpoint還是作爲開源項目發佈了。我們將持續努力開發並改進pinpoint以便滿足你的期望。

    Woonduk Kang編寫

    在2011年, 關於我自己我這樣寫到 — 作爲一個開發人員,我想開發人們願意付款的軟件程序,像Microsoft 或者 Oracle. 當Pinpoint被作爲一個開源項目啓動,看上去我的夢想稍微實現了一點。目前, 我的願望是讓pinpoint對用戶更加有價值和更惹人喜歡.


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