SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分布式中間件,包含了建構金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。
SOFATracer 是一個用于分布式系統調用跟蹤的元件,通過統一的 TraceId 将調用鍊路中的各種網絡調用情況以日志的方式記錄下來,以達到透視化網絡調用的目的,這些鍊路資料可用于故障的快速發現,服務治理等。
本文為《剖析 | SOFATracer 架構》最後一篇,本篇作者yushuqiang,來自小象生鮮。《剖析 | SOFATracer 架構》系列由 SOFA 團隊和源碼愛好者們出品,項目代号:<SOFA:TracerLab/>,目前領取已經完成,感謝大家的參與。
SOFATracer:https://github.com/alipay/sofa-tracer

前言
自 Google
《Dapper,大規模分布式系統的跟蹤系統》論文發表以來,開源 Tracer 系統如雨後春筍般相繼面市,各顯神通,但都是用于分布式系統調用跟蹤的元件,通過統一的 traceId 将調用鍊路中的各種網絡調用情況記錄下來,以達到透視化網絡調用的目的。本文介紹的 SOFATracer 是以日志的形式來記錄的,這些日志可用于故障的快速定位,服務治理等。目前來看 SOFATracer 團隊已經為我們搭建了一個完整的 Tracer 架構核心,包括資料模型、編碼器、跨程序透傳 traceId、采樣、日志落盤與上報等核心機制,并提供了擴充 API 及基于開源元件實作的部分插件,為我們基于該架構打造自己的 Tracer 平台提供了極大便利。
作為一個開源實作,SOFATracer 也盡可能提供大而全的插件實作,但由于多數公司都有自己配套的技術體系,完全依賴官方提供的插件可能無法滿足自身的需要,是以如何基于 SOFATracer 自身 API 的元件埋點機制進行擴充,實作自己的插件是必須掌握的一項本領。
本文将根據 SOFATracer 自身 AP I的擴充點及已提供的插件源碼來分析下 SOFATracer 插件的埋點機制。
SOFATracer 的插件埋點機制
對一個應用的跟蹤要關注的無非就是 用戶端->web 層->rpc 服務->dao 後端存儲、cache 緩存、消息隊列 mq 等這些基礎元件。SOFATracer 插件的作用實際上也就是對不同元件進行埋點,以便基于這些元件采集應用的鍊路資料。
不同元件有不同的應用場景和擴充點,是以對插件的實作也要因地制宜,SOFATracer 埋點方式一般是通過 Filter、Interceptor 機制實作的。
元件擴充入口之 Filter or Interceptor
SOFATracer 目前已實作的插件中,像 SpringMVC 插件是基于 Filter 進行埋點的,httpclient、resttemplate 等是基于 Interceptor 機制進行埋點的。在實作插件時,要根據不同插件的特性和擴充點來選擇具體的埋點方式。正所謂條條大路通羅馬,不管怎麼實作埋點,都是依賴 SOFATracer 自身 API 的擴充機制來實作。
API 擴充點之 AbstractTracer API
SOFATracer 中所有的插件均需要實作自己的 Tracer 執行個體,如 SpringMVC 的 SpringMvcTracer 、HttpClient 的 HttpClientTracer 等。
- 基于 SOFATracer API 埋點方式插件擴充如下:
AbstractTracer 是 SOFATracer 用于插件擴充使用的一個抽象類,根據插件類型不同,又可以分為 clientTracer 和 serverTracer,分别對應于 AbstractClientTracer 和 AbstractServerTracer;再通過 AbstractClientTracer 和 AbstractServerTracer 衍生出具體的元件 Tracer 實作,比如上圖中提到的 HttpClientTracer 、RestTemplateTracer 、SpringMvcTracer 等插件 Tracer 實作。
AbstractTracer
這裡先來看下 AbstractTracer 這個抽象類中具體提供了哪些抽象方法,也就是對于 AbstractClientTracer 和 AbstractServerTracer 需要分别擴充哪些能力。
從上圖 AbstractTracer 類提供的抽象方法來看,不管是 client 還是 server,在具體的 Tracer 插件實作中,都必須提供以下實作:
- DigestReporterLogName :目前元件摘要日志的日志名稱
- DigestReporterRollingKey : 目前元件摘要日志的滾動政策
- SpanEncoder:對摘要日志進行編碼的編碼器實作
- AbstractSofaTracerStatisticReporter : 統計日志 reporter 類的實作類
基于 SOFATracer 自身 API 埋點最大的優勢在于可以通過上面的這些參數來實作不同元件日志之間的隔離,上述需要實作的這些點是實作一個元件埋點正常的擴充點,是不可缺少的。
上面分析了 SOFATracer API 的埋點機制,并且對于一些需要擴充的核心點進行了說明。SOFATracer 自身提供的核心非常簡單,其基于自身 API 的埋點擴充機制為外部使用者定制元件埋點提供了極大的便利。下面以 Thrift 擴充,具體分析如何實作一個元件埋點。
PS : Thrift 是外部使用者基于 SOFATracer API 擴充實作的,目前僅用于其公司内部使用,SOFATracer 官方元件中暫不支援,請知悉;後續會溝通作者提供 PR ,在此先表示感謝。
Thrift 插件埋點分析
這裡我們以 Thrift RPC 插件實作為例,分析如何實作一個埋點插件。
- 1、執行個體工程的分包結構
從上圖插件的工程的包結構可以看出,整個插件實作比較簡單,代碼量不多,但從類的定義來看,直覺的展現了SOFATracer 插件埋點機制所介紹的套路。下面将進行詳細的分析與介紹。
- 2、實作 Tracer 執行個體
RpcThriftTracer 繼承了 AbstractTracer 類,是對 clientTracer、serverTracer 的擴充。
RpcThriftTracer |
PS:如何确定一個元件是 client 端還是 server 端呢?就是看目前元件是請求的發起方還是請求的接受方,如果是請求發起方則一般是 client 端,如果是請求接收方則是 server 端。那麼對于 RPC 來說,即是請求的發起方也是請求的接受方,是以這裡實作了 AbstractTracer 類。
- 3、擴充點類實作
DigestReporterLogName | RpcTracerLogEnum | 目前元件摘要日志的日志名稱 | 目前 SOFATracer 日志名、滾動政策 key 等都是通過枚舉類來定義的,也就是一個元件會對應這樣一個枚舉類,在枚舉類裡面定義這些常量。 |
DigestReporterRollingKey | 目前元件摘要日志的滾動政策 | ||
SpanEncoder | AbstractRpcDigestSpanJsonEncoder RpcClientDigestSpanJsonEncoder RpcServerDigestSpanJsonEncoder | 對摘要日志進行編碼的編碼器實作 | 這個決定了摘要日志列印的格式,和在統計日志裡面的實作要有所區分。 |
AbstractSofaTracerStatisticReporter | AbstractRpcStatJsonReporter RpcClientStatJsonReporter RpcServerStatJsonReporter | 統計日志 reporter 類的實作類 | 這裡就是就是将統計日志添加到日志槽裡,等待被消費(輸出到日志)。具體可以參考:SofaTracerStatisticReporterManager.StatReporterPrinter。 |
RpcSpanTags | 要采集資料 key 的取值定義 |
PS:上面表格中SpanEncoder和AbstractSofaTracerStatisticReporter的實作中,多了一層AbstractRpcDigestSpanJsonEncoder和AbstractRpcStatJsonReporter的抽象,主要是由于client和server端有公共的邏輯處理,為了減少備援代碼,而采用了多繼承模式處理。
- 4、資料傳播格式實作
ThriftRequestCarrier |
SOFATracer 支援使用 OpenTracing 的内建格式進行上下文傳播。
- 5、Thrift Rpc 自身擴充點之請求攔截埋點
FilterThriftBase |
ConsumerTracerFilter |
ProviderTracerFilter |
我們内部 Thrift 支援 SPI Filter 機制,是以要實作對請求的攔截過濾,示例插件埋點的實作就是基于 SPI Filter 機制完成的。其中FilterThriftBase抽象也是為了便于處理consumerFilter和providerFilter公共的邏輯抽象。
插件擴充基本思路總結
對于一個元件來說,一次處理過程一般是産生一個 Span;這個 Span 的生命周期是從接收到請求到傳回響應這段過程。
但是這裡需要考慮的問題是如何與上下遊鍊路關聯起來呢?在 Opentracing 規範中,可以在 Tracer 中 extract 出一個跨程序傳遞的 SpanContext 。然後通過這個 SpanContext 所攜帶的資訊将目前節點關聯到整個 Tracer 鍊路中去,當然有提取(extract)就會有對應的注入(inject);更多請參考
螞蟻金服分布式鍊路跟蹤元件鍊路透傳原理與SLF4J MDC的擴充能力分析 | 剖析。
鍊路的建構一般是 client-server-client-server 這種模式的,那這裡就很清楚了,就是會在 client 端進行注入(inject),然後再 server 端進行提取(extract),反複進行,然後一直傳遞下去。
在拿到 SpanContext 之後,此時目前的 Span 就可以關聯到這條鍊路中了,那麼剩餘的事情就是收集目前元件的一些資料;整個過程大概分為以下幾個階段:
- 從請求中提取 spanContext
- 建構 Span,并将目前 Span 存入目前 tracer上下文中(SofaTraceContext.push(Span)) 。
- 設定一些資訊到 Span 中
- 傳回響應
- Span 結束&上報
下面結合 SOFATracer 自身 API 源碼來逐一分析下這幾個過程。
Thrift 插件中的 Consumer 和 Provider 分别對應于 client 和 server 端存在的,是以在 client 端就是将目前請求線程的産生的 traceId 相關資訊 Inject 到 SpanContext,server 端從請求中 extract 出 spanContext,來還原本次請求線程的上下文。
相關處理邏輯在FilterThriftBase抽象類中,如下圖:
- inject 實作代碼
- extract 實作代碼
擷取 Span & 資料擷取
serverReceive 這個方法是在 AbstractTracer 類中提供了實作,子類不需要關注這個。在 SOFATracer 中也是将請求大緻分為以下幾個過程:
- 用戶端發送請求 clientSend cs
- 服務端接受請求 serverReceive sr
- 服務端傳回結果 serverSend ss
- 用戶端接受結果 clientReceive cr
無論是哪個插件,在請求處理周期内都可以從上述幾個階段中找到對應的處理方法。是以,SOFATracer 對這幾個階段處理進行了封裝。見下圖:
這四個階段實際上會産生兩個 Span,第一個 Span 的起點是 cs,到 cr 結束;第二個 Span 是從 sr 開始,到 ss 結束。
clientSend
serverReceive
...
serverSend
clientReceive
來看下 Thrift Rpc 插件中 Consumer 和 Provider 的實作
紅色框内對應的用戶端發送請求,也就是 cs 階段,會産生一個 Span。
服務端接收請求 sr 階段,産生了一個 Span 。上面appendProviderRequestSpanTags這段代碼是為目前這個 Span 設定一些基本資訊,包括目前應用的應用名、目前請求的 service、目前請求的請求方法以及請求大小等。
傳回響應與結束 Span
在 Filter 鍊執行結束之後,ConsumerTracerFilter(見圖一)和 ProviderTracerFilter(見圖二) 分别在 finally 塊中又補充了目前請求響應結果的一些資訊到 Span 中去。然後分别調用 clientReceive 和 serverSend 結束目前 Span。
- 圖一
- 圖二
關于 clientReceive 和 serverSend 裡面調用 Span.finish 這個方法( opentracing 規範中,Span.finish 的執行标志着一個 Span 的結束(見圖一),當調用finish執行邏輯時同時會進行span資料的上報(見圖二)和目前請求線程MDC資源的清理操作(見圖三)等。
- 圖一:
目前 Span 資料上報,代碼如下:
- 圖二:
清理目前請求線程的 MDC 資源的一些邏輯處理等,代碼如下:
- 圖三:
插件編寫流程總結
上述以自定義 Thrift RPC 插件為例,分析了下 SOFATracer 插件埋點實作的一些細節。前面不僅總結了編寫插件的基本埋點思路而且還對 SOFATracer 自身 API 實作做了相應的分析。基于此本節則從整體思路上來總結如何編寫一個 SOFATracer 的插件:
- 1、确定所要實作的插件,了解該元件的使用場景和擴充點,然後确定以哪種方式來埋點,比如:是 Filter or Interceptor
- 2、實作目前插件的 Tracer 執行個體,這裡需要明确目前插件是以 client 存在還是以 server 存在
- 3、實作一個枚舉類,用來描述目前元件的日志名稱和滾動政策 key 值等
- 4、實作插件摘要日志的 Encoder ,實作目前元件的定制化輸出
- 5、實作插件的統計日志 Reporter 實作類,通過繼承 AbstractSofaTracerStatisticReporter 類并重寫doReportStat
- 6、定義目前插件的傳播格式
- 7、要明确我們需要收集哪些資料
小結
本文通過對 SOFATracer 插件的埋點機制進行分析介紹,并結合自定義 Thrift RPC 插件的埋點實作進行了分析。希望通過本文能夠讓更多的同學了解基于 SOFATracer 自身 API 的埋點實作,能根據自身需要實作自己的插件。
文中涉及到的所有連結:
- 《Dapper,大規模分布式系統的跟蹤系統》: https://bigbully.github.io/Dapper-translation/
- 螞蟻金服分布式鍊路跟蹤元件鍊路透傳原理與SLF4J MDC的擴充能力分析 | 剖析: https://mp.weixin.qq.com/s/DQNOz6QnfKCJ0rhbx1cJLw
歡迎大家共同打造 SOFAStack https://github.com/alipay