Biz-UI團隊在核心業務系統的開發過程中,將具有共性的功能模塊抽象出來,逐漸完成了中臺的構建,為業務邏輯提供了強有力的基礎組件支撐。其中分布式追蹤系統作為一個重要的組成部分,為監控服務之間的調用、定位和調試線上問題,提供了有力的支撐。本文將詳細剖析FreeWheel Biz-UI團隊從0到1構建和改進全鏈路分布式追蹤系統的過程。
小志所在的技術部門剛剛對臃腫的單體應用完成了拆解,推行微服務理念,將之前雜糅得不可開交的代碼按業務模塊拆分成一個一個的微服務。隨著項目的推動,大家確實感受到微服務帶來的收益,拆解完以後對單個微服務維護起來也更加方便。但與此同時也帶來了一些之前未曾遇到的問題......
一陣急促的手機鈴聲打斷了小志的思緒,看著熟悉的來電號碼,小志心想真是怕什麼來什麼,新上的服務凌晨又出問題了。
「喂,小志啊,線上報警了,挺緊急的,你趕緊看一下吧,一會線上聊。」
熟練地翻開報警郵件,處理這類問題對小志來說已經輕車熟路。詳細分析了一下報警內容,小志斷定是下遊服務出問題導致的報警。
「老李,我是小志,我們這邊剛剛出來一個報警,挺嚴重的,剛看了一下系統日誌,是在調你們訂單服務的時候出錯了,你幫看一下吧,我一會把日誌發你釘釘。」
老李經過排查,發現是上遊服務的問題,於是抓緊聯繫老錢。「老錢啊,還沒睡呢吧?剛剛小志那邊出問題,影響了不少客戶。查日誌發現是訂單這邊報錯了,我看了一下訂單服務的日誌,是你們那邊的庫存服務報了不少500,你趕緊起來看一下吧。」
屏幕前小志、老李、老錢正在熱火朝天地捉蟲找bug.
微服務,作為一個近幾年非常火熱的話題,切切實實解決了很多單體應用的痛點,但與此同時也帶來了一些新的痛點。
FreeWheel核心業務部門結合自身的實際情況,以微服務的方式對之前的單一應用做了拆分。同時,為了避免上面故事裡的情況發生,我們引入了分布式追蹤系統用來解決【如何在微服務系統中快速定位問題?】、【如何觀察複雜的調用鏈、分析調用的網絡結構?】等等問題。
分布式追蹤系統(Distributed Tracing System)可以用來解決微服務系統中的常問題定位、bug 追蹤、網絡結構分析等問題。該系統的數據模型最早由Google’s Dapper 論文提出,主要包含如下幾個部分:
在這基礎上,社區為了實現各個程式語言和各種框架的接口統一,發展出了OpenTracing Specification 以及 OpenTracing API。後來業界也相繼推出了幾款比較成熟的產品,如Zipkin、Jaeger、LightStep、DataDog等。
分布式追蹤系統是如何解決跨服務調用時的問題定位的呢?對於一次客戶調用,分布式追蹤系統會在請求的入口處生成一個TraceID,用這個TraceID把客戶請求進入每個微服務中的調用日誌串聯起來,形成一個時序圖。如下圖所示,假設A的兩端表示一次客戶調用的開始和結束,中間會經過類似B、 C、D、 E等後端服務。此時如果E出問題,就可以很快速地定位到,而不用同時讓A、B、C、D都參與進來查問題。
(圖片來源:https://www.jaegertracing.io/img/spans-traces.png)
技術選型
在FreeWheel 核心業務系統微服務搭建過程中,我們深度調研了現有的分布式追蹤系統解決方案,針對其中幾個比較重要的選型指標做了深度的討論。
基於上面幾個指標,我們對市面上主流的開源項目進行篩選,包括Jeager、Zipkin等,考慮到市場佔有率、項目成熟度、項目契合度,我們最終選擇了同為Golang開發的Jaeger。Jeager Tracing框架下主要包含三大模塊:TracingAgent, Tracing Collector 和 Tracing Query。
整體的架構如下圖所示:
(圖片來源:https://www.jaegertracing.io/img/architecture-v1.png)
新系統實施的第一步往往是分析現有技術環境,目的是儘可能地復用已有的功能、模塊,運維環境等。這樣能大大減少後續的維護、運維等成本。
FreeWheel核心業務平臺現有的基礎環境包括如下幾點:
基於以上背景,我們設計了Tracing系統實施方案,並對部分模塊進行了升級改造。
首先,由於各個微服務對外提供的接口也不盡統一,現有的接口包括gRPC、gRPC-Gateway、HTTP,甚至WebSocket。我們在Jeager-client基礎上做了一層封裝,實現了一個Tracing client Lib,該lib可以針對不同的通訊協議對流量進行劫持,並將Trace 信息注入到請求中。還擴展性地加入了過濾器(過濾給定特徵的流量)、 TraceID生成、TraceID提取,與Zipkin Header兼容等功能。這部分會隨著平臺的不斷擴展和改造進行持續的更新和維護。
另外,為了充分利用公司現有的ElasticSearch 集群,我們決定用ElasticSearch作為追蹤系統的後端存儲。由於使用場景為寫多讀少,為了保護ElasticSerach,我們決定用Kafka作為緩衝,即對Collecor進行擴展,將數據進行處理並轉換成ElasticSearch可讀的json格式寫入Kafka, 再通過logstash消費寫入ElasticSearch中。
此外,對於Spark dependency Job,同樣需要將數據轉換為對應的Json格式寫入Kafka,最終存儲到ElasticSearch。這裡對Spark Dependency Job的輸出部分做了擴展,讓其支持向Kafka中導入數據。最後,由於微服務系統內部部署環境的差異,我們提供了兼容K8s sidecar, K8s Daemonset, On-perm daeom process 等部署方式。
新設計的架構如下圖所示:
中間件
對於基於Golang開發的微服務,Trace信息在服務內部傳播主要依賴context.Context。FreeWheel核心業務系統中一般來講支持兩種通訊協議:HTTP and GRPC,其中HTTP接口主要依賴GRPC-Gateway自動生成。當然也有一部分服務不涉及GRPC, 直接對外暴露HTTP接口。這裡HTTP主要面向的調用方是OpenAPI或者前端UI。同時,服務與服務之間一般採用GRPC方式通訊。對於這類場景,Tracinglib提供了必要的組件供業務微服務使用。其傳播過程如下圖所示:
針對入口流量,Tracing Client Lib封裝了HTTP中間件、 GRPC中間件,以及與GRPC-Gateway這一層的兼容。針對出口流量,Tracing Client Lib 封裝了GRPC-Client 中間件。這裡的「封裝」不單單指對Jaeger client lib提供方法的簡單wrapper,還包括諸如Tracing狀態監測、請求過濾等功能。比較典型的像 "/check_alive", "/metrics"這類沒有必要trace的請求可以通過請求過濾的功能過濾掉從而不記錄Trace。
Istio集成
了解Istio的同學應該知道,Istio本身支持Jaeger Tracing集成。對於跨服務的請求,Istio可以劫持諸如GRPC/HTTP等類型的流量,生成對應的Trace信息。因此如果能將業務代碼中的Trace信息與Istio進行集成,就能夠監控到整個調用網絡與業務內部Trace的完整信息,方便查看Istio sidecar到服務這個調用過程的網絡情況。
問題在於,Istio集成Tracing時採取了Zipkin B3 Header標準,其格式如下:
X-B3-TraceId: {TraceID}X-B3-ParentSpanId: {ParentSpanID}X-B3-SpanId: {SpanID}X-B3-Sampled: {SampleFlag}X-B3-TraceId: {TraceID}X-B3-ParentSpanId: {ParentSpanID}X-B3-SpanId: {SpanID}X-B3-Sampled: {SampleFlag}
而FreeWheel核心業務系統內部所採用的TracerHeader格式為:
FW-Trace-ID: {TraceID}:{SpanID}:{ParentSpanID}:{SampleFlag}
並且FW Trace Header被廣泛地應用在業務代碼中,集成了諸如log, change_history等服務,一時間難以被完全替換。針對這個問題,我們重寫了Jaeger Client中的將Injector和Extractor,其接口定義如下:
//Span body{ "traceID": "5082be69746ed84a", "spanID": "5082be69746ed84a", "operationName": "HTTP GET", "startTime": ..., "duration": 616, "references": [ { "refType": "CHILD_OF", "spanID": "14a9e000a96a2671", "traceID": "259f404f8409a4d7" } ], "tags": [ { "key": "http.url", "type": "string", "value": "/services/v3/**.xml" }, { "key": "http.status_code", "type": "int64", "value": "500" }, //... ], "logs": [], "process": { "serviceName": "your_service_name", "tags": [ { "key": "hostname", "type": "string", "value": "xx-mac" }, //... ] }}
新實現的Injector和Extractor同時兼容B3 Header和Freewheel Trace Header。服務接收到請求時會優先查看有沒有B3 Header,在生成新Span的時候同時插入FreeWheel Trace Header。即FreeWheel Trace Header繼續在服務內部使用,跨服務之間的調用以B3 Header為主。
X-B3-TraceId: {TraceID}X-B3-ParentSpanId: {ParentSpanID}X-B3-SpanId: {SpanID}X-B3-Sampled: {SampleFlag}FW-Trace-ID: {TraceID}:{SpanID}:{ParentSpanID}:{SampleFlag}
上文提到數據存儲選用ElasticSearch, 數據的採集與存儲是一個典型的寫多讀少的業務場景。對這類場景,我們引入Kafka作為數據的緩衝與中轉層。基於這個思路我們對Collector進行了改造,加入了Collector Kafka Producer組件,在Collector上將span信息轉為json發給Kafka,然後由Logstash作為Consumer存儲到ElasticSearch。對於Trace信息,ElasticSearch存儲主要分為兩大部分:服務/操作索引和Span 索引。服務/操作索引主要用來為query ui提供快速檢索服務(Service Name)和操作(Operation Name), 結構如下:
//Span body{ "traceID": "5082be69746ed84a", "spanID": "5082be69746ed84a", "operationName": "HTTP GET", "startTime": ..., "duration": 616, "references": [ { "refType": "CHILD_OF", "spanID": "14a9e000a96a2671", "traceID": "259f404f8409a4d7" } ], "tags": [ { "key": "http.url", "type": "string", "value": "/services/v3/**.xml" }, { "key": "http.status_code", "type": "int64", "value": "500" }, //... ], "logs": [], "process": { "serviceName": "your_service_name", "tags": [ { "key": "hostname", "type": "string", "value": "xx-mac" }, //... ] }}
Span結構體由Tracing客戶端生成,主要一下幾大部分:
//Span body{ "traceID": "5082be69746ed84a", "spanID": "5082be69746ed84a", "operationName": "HTTP GET", "startTime": ..., "duration": 616, "references": [ { "refType": "CHILD_OF", "spanID": "14a9e000a96a2671", "traceID": "259f404f8409a4d7" } ], "tags": [ { "key": "http.url", "type": "string", "value": "/services/v3/**.xml" }, { "key": "http.status_code", "type": "int64", "value": "500" }, //... ], "logs": [], "process": { "serviceName": "your_service_name", "tags": [ { "key": "hostname", "type": "string", "value": "xx-mac" }, //... ] }}
這一層主要用於對Trace數據進行持久化和離線分析。利用ElasticSearch會對數據進行分片,分index的存儲,防止歷史數據丟失,方便對歷史問題進行回溯。不過既然提到持久化就難免要考慮數據規模的問題,持續大量的歷史數據寫入到ElasticSeach會不斷增加其負擔,而且對於過於久遠的歷史數據,被檢索到的頻率也相對較小。這裡我們採取定期歸檔的策略,對於超過30天的數據進行歸檔,轉存到ES之外以備不時之需。ElasticSearch只對相對較「熱」的數據提供檢索服務。
離線分析主要用於對EalsticSearch中的Span數據進行分析,上文我們提到一個Span數據結構包含其自身的TraceID和它父節點的TraceID,每一個節點都包含自身從屬與哪個服務。
這裡我們只關心跨服務之間的調用關係,例如上圖,離線分析時只考慮A, B, C, E這幾個節點,由於D節點與C, E節點都在服務3內部,所以將其忽略。分析出來的結果如圖所示
展示層
展示層主要指Query-UI, 功能是從ElacticSearch中查詢數據,對具有相同TraceID的數據進行聚合,並在前端進行渲染。從QueryUI中可以清晰的看到一條請求經歷了幾個不同的服務(以不同顏色標註),在每個服務中的到達時間和結束時間,整個請求總共經歷的時間等。
隨著FreeWheel核心業務平臺不斷地擴充和演進,分布式追蹤系統也需要進行不斷升級改造以適配業務需求。例如部分業務代碼正在嘗試Serverless的方式,也就要求Tracing系統支持諸如AWS Lambda等使用場景。對於這種形式的需求,我們將緊跟業務,持續調研,以期服務更多的場景。此外,現有服務調用拓撲網絡是基於離線數據生成的,我們也期望未來能找到一些在線處理的解決方案,如Flink、Spark Streaming等,做到實時的調用關系統計。
參考閱讀
作者介紹:
馮剛,FreeWheel Biz-UI 高級研發工程師,數年Golang後端研發經驗,熟悉微服務體系架構,熱衷於探索與分享新技術。目前致力於OpenAPI重構,服務化等相關實踐。
延伸閱讀:
關注我並轉發此篇文章,私信我「領取資料」,即可免費獲得InfoQ價值4999元迷你書,點擊文末「了解更多」,即可移步InfoQ官網,獲取最新資訊~