作者:翟東波、葉書俊
在微服務體系架構下,搜狐智慧媒體使用 Zipkin 進行服務鏈路追蹤(Tracing)的埋點採集,將採集的 Trace 資訊儲存到 StarRocks 中。通過 StarRocks 強大的 SQL 計算能力,對 Tracing 資訊進行多維度的統計、分析等操作,提升了微服務監控能力,從簡單統計的 Monitoring 上升到更多維度探索分析的 Observability。
全文主要分為三個部分:第一節主要介紹微服務下的常用監控方式,其中鏈路追蹤技術,可以串聯整個服務呼叫鏈路,獲得整體服務的關鍵資訊,對微服務的監控有非常重要的意義。第二節主要介紹搜狐智慧媒體是如何構建鏈路追蹤分析體系的,主要包括 Zipkin 的資料採集,StarRocks 的資料儲存,以及根據應用場景對 StarRocks 進行分析計算等三個部分。第三節主要介紹搜狐智慧媒體通過引入 Zipkin 和 StarRocks 進行鏈路追蹤分析取得的一些實踐效果。
01 微服務架構中的鏈路追蹤
近年來,企業 IT 應用架構逐步向微服務、雲原生等分散式應用架構演進,在搜狐智慧媒體內部,應用服務按照微服務、Docker、Kubernetes、Spring Cloud 等架構思想和技術方案進行研發運維,提升部門整體工程效率。
微服務架構提升工程效率的同時,也帶來了一些新的問題。微服務是一個分散式架構,它按業務劃分服務單元,使用者的每次請求不再是由某一個服務獨立完成了,而是變成了多個服務一起配合完成。這些服務可能是由不同的團隊、使用不同的程式語言實現,可能布在了不同的伺服器、甚至不同的資料中心。如果使用者請求出現了錯誤和異常,微服務分散式呼叫的特性決定了這些故障難以定位,相對於傳統的單體架構,微服務監控面臨著新的難題。
Logging、Metrics、Tracing
微服務監控可以包含很多方式,按照監測的資料型別主要劃分為 Logging、Metrics 和Tracing 三大領域:
Logging
使用者主動記錄的離散事件,記錄的資訊一般是非結構化的文字內容,在使用者進行問題分析判斷時可以提供更為詳盡的線索。
具有聚合屬性的採集資料,旨在為使用者展示某個指標在某個時段的執行狀態,用於檢視一些指標和趨勢。
Tracing
記錄一次請求呼叫的生命週期全過程,其中包括服務呼叫和處理時長等資訊,含有請求上下文環境,由一個全域性唯一的 Trace ID 來進行標識和串聯整個呼叫鏈路,非常適合微服務架構的監控場景。
圖 1
三者的關係如上圖所示,這三者之間也是有重疊的,比如 Logging 可以聚合相關欄位生成 Metrics 資訊,關聯相關欄位生成 Tracing 資訊;Tracing 可以聚合查詢次數生成 Metrics 資訊,可以記錄業務日誌生成 Logging 資訊。一般情況下要在 Metrics 和 Logging 中增加欄位串聯微服務請求呼叫生命週期比較困難,通過 Tracing 獲取 Metrics 和 Logging 則相對容易很多。
另外,這三者對儲存資源有著不同的需求,Metrics 是天然的壓縮資料,最節省資源;Logging 傾向於無限增加的,甚至會超出預期的容量;Tracing 的儲存容量,一般介於 Metrics 和 Logging 兩者之間,另外還可通過取樣率進一步控制容量需求。
從 Monitoring 到 Observability
Monitoring tells you whether the system works. Observability lets you ask why it's not working.
– Baron Schwarz
微服務監控從資料分析層次,可以簡單分為 Monitoring 和 Observability。
Monitoring
告訴你係統是否在工作,對已知場景的預定義計算,對各種監控問題的事前假設。對應上圖 Known Knowns 和 Known Unknowns,都是事先假設可能會發生的事件,包括已經明白和不明白的事件。
Observability
可以讓你詢問系統為什麼不工作,對未知場景的探索式分析,對任意監控問題的事後分析。對應上圖 Unknown Knowns 和 Unknown Unknowns,都是事未察覺可能會發生的事件,包括已經明白和不明白的事件。
很顯然,通過預先假設所有可能發生事件進行 Monitoring 的方式,已經不能滿足微服務複雜的監控場景,我們需要能夠提供探索式分析的 Observability 監控方式。在 Logging、Metrics 和 Tracing,Tracing 是目前能提供多維度監控分析能力的最有效方式。
Tracing
鏈路追蹤 Tracing Analysis 為分散式應用的開發者提供了完整的呼叫鏈路還原、呼叫請求量統計、鏈路拓撲、應用依賴分析等工具,可以幫助開發者快速分析和診斷分散式應用架構下的效能瓶頸,提高微服務時代下的開發診斷效率。
Tracing 可以串聯微服務中分散式請求的呼叫鏈路,在微服務監控體系中有著重要的作用。另外,Tracing 介於 Metrics 和 Logging 之間,既可以完成 Monitoring 的工作,也可以進行 Observability 的分析,提升監控體系建設效率。
系統模型
鏈路追蹤(Tracing)系統,需要記錄一次特定請求經過的上下游服務呼叫鏈路,以及各服務所完成的相關工作資訊。
如下圖所示的微服務系統,使用者向服務 A 發起一個請求,服務 A 會生成一個全域性唯一的 Trace ID,服務 A 內部 Messaging 方式呼叫相關處理模組(比如跨執行緒非同步呼叫等),服務 A 模組再通過 RPC 方式並行呼叫服務 B 和服務 C;服務 B 會即刻返回響應,但服務 C 會採用序列方式,先用 RPC 呼叫服務 D,再用 RPC 呼叫服務 E,然後再響應服務 A 的呼叫請求;服務 A 在內部兩個模組呼叫處理完後,會響應最初的使用者請求。
最開始生成的 Trace ID 會在這一系列的服務內部或服務之間的請求呼叫中傳遞,從而將這些請求呼叫連線起來。另外,Tracing 系統還會記錄每一個請求呼叫處理的 Timestamp、服務名等等相關資訊。
圖 3(注:服務內部序列呼叫對系統效能有影響,一般採用並行呼叫方式,後續章節將只考慮並行呼叫場景。)
在 Tracing 系統中,主要包含 Trace 和 Span 兩個基礎概念,下圖展示了一個由 Span 構成的 Trace。
圖 4
Trace 指一個外部請求經過的所有服務的呼叫鏈路,可以理解為一個有服務呼叫組成的樹狀結構,每條鏈路都有一個全域性唯一的 ID 來標識。
Span 指服務內部或服務之間的一次呼叫,即 Trace 樹中的節點,如下圖所示的由 Span 構成的 Trace 樹,樹中的 Span 節點之間存在父子關係。Span 主要包含 Span名稱、Span ID、父 ID,以及 Timestamp、Dration(包含子節點呼叫處理的 duration)、業務資料等其他 log 資訊。
Span 根據呼叫方式可以分為 RPC Span 和 Messaging Span:
RPC Span
由 RPC Tracing 生成,分為 Client 和 Server 兩類 Span,分別由 RPC 服務呼叫的 Client 節點和 Server 節點記錄生成,兩者共享 Span ID、Parent Span ID 等資訊,但要注意,這兩個 Span 記錄的時間是有偏差,這個偏差是服務間的呼叫開銷,一般是由網路傳輸開銷、代理服務或服務介面訊息排隊等情況引起的。
Messaging Span
由 Messaging Tracing 生成,一般用於 Tracing 服務內部呼叫,不同於 RPC Span,Messaging Span 之間不會共享 Span ID 等資訊。
應用場景
根據 Tracing 的系統模型,可獲得服務響應等各類 Metric 資訊,用於 Alerting、DashBoard 查詢等;也可根據 Span 組成的鏈路,分析單個或整體服務情況,發現服務效能瓶頸、網路傳輸開銷、服務內非同步呼叫設計等各種問題。如下圖所示,相比於 Metrics 和 Logging,Tracing 可以同時涵蓋監控的 Monitoring 和 Observability 場景,在監控體系中佔據重要位置,Opentracing、Opencensus、Opentelemetry 等協會和組織都包含對 Tracing 的支援。
圖 5
從微服務的角度,Tracing 記錄的 Span 資訊可以進行各種維度的統計和分析。下圖基於 HTTP API 設計的微服務系統為例,使用者查詢 Service1的 /1/api 介面,Service1 再請求 Service2 的 /2/api,Service2 內部非同步併發呼叫 msg2.1 和 msg2.2,msg2.1 請求 Service3的 /3/api介面,msg2.2 請求 Service4 的 /4/api介面,Service3 內部呼叫 msg3,Service4 再請求 Service5 的 /5/api,其中 Service5 沒有進行 Tracing 埋點,無法採集 Service5 的資訊。
圖 6
針對上圖的微服務系統,可以進行如下兩大類的統計分析操作:
服務內分析
關注單個服務執行情況,比如對外服務介面和上游介面查詢的效能指標等,分析場景主要有:
1、上游服務請求
如 Service1 提供的 /1/api ,Service4 提供的 /4/api等,統計獲得次數、QPS、耗時百分位數、出錯率、超時率等等 metric 資訊。
2、下游服務響應
如 Service1 請求的 /2/api 、Service4 請求的 /5/api等,統計查詢次數、QPS、耗時百分位數、出錯率、超時率等等 Metric 資訊。
3、服務內部處理
服務對外介面在內部可能會被分拆為多個 Span,可以按照 Span Name 進行分組聚合統計,發現耗時最長的 Span 等,如 Service2 介面 /2/api ,介面服務內部 Span 包括 /2/api 的 Server Span,call2.1 對應的 Span 和 call2.2 對應的 Span,通過 Span 之間的依賴關係可以算出這些 Span 自身的耗時 Duraion,進行各類統計分析。
服務間分析
在進行微服務整體分析時,我們將單個服務看作黑盒,關注服務間的依賴、呼叫鏈路上的服務熱點等,分析場景主要有:
1、服務拓撲統計
可以根據服務間呼叫的 Client Span 和 Server Span,獲得整個服務系統的拓撲結構,以及服務之間呼叫請求次數、Duration 等統計資訊。
2、呼叫鏈路效能瓶頸分析
分析某個對外請求介面的呼叫鏈路上的效能瓶頸,這個瓶頸可能是某個服務內部處理開銷造成的,也可能是某兩個服務間的網路呼叫開銷等等原因造成的。
對於一次呼叫涉及到數十個以上微服務的複雜呼叫請求,每次出現的效能瓶頸很可能都會不一樣,此時就需要進行聚合統計,算出效能瓶頸出現頻次的排名,分析出針對效能瓶頸熱點的服務或服務間呼叫。
以上僅僅是列舉的部分分析場景,Tracing 提供的資訊其實可以支援更多的 Metric 統計和探索式分析場景,本文不再一一例舉。
02 基於 Zipkin 和 StarRocks 構建鏈路追蹤分析系統
鏈路追蹤系統主要分為資料採集、資料儲存和分析計算三大部分,目前使用最廣泛的開源鏈路追蹤系統是 Zipkin,它主要包括資料採集和分析計算兩大部分,底層的儲存依賴其他儲存系統。搜狐智慧媒體在構建鏈路追蹤系統時,最初採用 Zipkin + ElasticSearch 得方式進行構建,後增加 StarRocks 作為底層儲存系統,並基於 StarRocks 進行分析統計,系統總體架構如下圖。
圖 7
資料採集
Zipkin 支援客戶端全自動埋點,只需將相關庫引入應用程式中並簡單配置,就可以實現 Span 資訊自動生成,Span 資訊通過 HTTP 或 Kafka 等方式自動進行上傳。Zipkin 目前提供了絕大部分語言的埋點採集庫,如 Java 語言的 Spring Cloud 提供了 Sleuth 與 Zipkin 進行深度繫結,對開發人員基本做到透明使用。為了解決儲存空間,在使用時一般要設定 1/100 左右的取樣率,Dapper 的論文中提到即便是 1/1000 的取樣率,對於跟蹤資料的通用使用層面上,也可以提供足夠多的資訊。
資料模型
對應 圖 6,下面給出了 Zipkin Span 埋點採集示意圖 (圖 8),具體流程如下:
圖 8
- 使用者傳送給 Service1 的 Request 中,不含有 Trace 和 Span 資訊,Service1 會建立一個 Server Span,隨機生成全域性唯一的 TraceID(如圖中的 X)和 SpanId(如圖中的 A,此處的 X 和 A 會使用相同的值),記錄 Timestamp 等資訊;Service1 在給使用者返回 Response 時,Service1 會統計 Server Span 的處理耗時 Duration,會將包含 TraceID、SpanID、Timestamp、Duration 等資訊的 Server Span 完整資訊進行上報。
- Service1 向 Service2 傳送的請求,會建立一個 Client Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 B),記錄 Timestamp 等資訊,同時 Service1 會將 Trace ID(X)和 SpanID(B)傳遞給 Service2(如在 HTTP 協議的 HEADER 中新增 TraceID 和 SpanID 等相關欄位);Service1 在收到 Service2 的響應後,Service1 會處理 Client Span 相關資訊,並將 Client Span 進行上報
- Service2 收到 Service1 的 Request 中,包含 Trace(X)和 Span(B)等資訊,Service2 會建立一個 Server Span,使用 X 作為 Trace ID,B 作為 SpanID,內部呼叫msg2.1 和 msg2.2 同時,將 Trace ID(X)和 SpanID(B)傳遞給它們;Service2 在收到 msg2.1 和 msg2.2 的返回後,Service1 會處理 Server Span 相關資訊,並將此 Server Span 進行上報
- Service2 的 msg2.1 和 msg2.2 會分別建立一個 Messaging Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 C 和 F),記錄 Timestamp 等資訊,分別向 Service3 和 Service4 傳送請求;msg2.1 和 msg2.2 收到響應後,會分別處理 Messaging Span 相關資訊,並將兩個 Messaging Span 進行上報
- Service2 向 Service3 和 Service4 傳送的請求,會各建立一個 Client Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 D 和 G),記錄 Timestamp 等資訊,同時 Service2 會將 Trace ID(X)和 SpanID(D 或 G)傳遞給 Service3 和 Service4;Service12 在收到 Service3 和 Service3 的響應後,Service2 會分別處理 Client Span 相關資訊,並將兩個 Client Span 進行上報
- Service3 收到 Service2 的Request中,包含 Trace(X)和Span(D)等資訊,Service3 會建立一個 Server Span,使用 X 作為 Trace ID,D 作為 SpanID,內部呼叫 msg3;Service3 在收到 msg3 的返回後,Service3 會處理此 Server Span 相關資訊,並將此 Server Span 進行上報
- Service3 的 msg3 會分別建立一個 Messaging Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 E),記錄 Timestamp 等資訊,msg3 處理完成後,處理此 Messaging Span 相關資訊,並將此 Messaging Span 進行上報
- Service4 收到 Service2 的 Request 中,包含 Trace(X)和 Span(G)等資訊,Service4 會建立一個 Server Span,使用 X 作為 Trace ID,G 作為 SpanID,再向 Service5 傳送請求;Service4 在收到 Service5 的響應後,Service4 會處理此 Server Span 相關資訊,並將此 Server Span 進行上報
- Service4 向 Service5 傳送的請求,會建立一個 Client Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 H),記錄 Timestamp 等資訊,同時 Service4 會將 Trace ID(X)和 SpanID(H)傳遞給 Service5;Service4 在收到 Service5 的響應後,Service4 會處理 Client Span 相關資訊,並將此 Client Span 進行上報
上面整個 Trace X 呼叫鏈路會生成的 Span 記錄如下圖,每個 Span 主要會記錄 Span Id、Parent Id、Kind(CLIENT 表示 RPC CLIENT 端 Span,SERVER 表示 RPC SERVER 端 SPAN,NULL 表示 Messaging SPAN),SN(Service Name),還會包含 Trace ID,時間戳、Duration 等資訊。Service5 沒有進行 Zipkin 埋點採集,因此不會有 Service5 的 Span 記錄。
圖 9
資料格式
設定了 Zipkin 埋點的應用服務,預設會使用 Json 格式向 Kafka 上報 Span 資訊,上報的資訊主要有如下幾個注意點:
每個應用服務每次會上報一組 Span,組成一個 Json 陣列上報
Json 陣列裡包含不同 Trace的Span,即不是所有的 Trace ID都 相同
不同形式的介面(如 Http、Grpc、Dubbo 等),除了主要欄位相同外,在 tags 中會各自記錄一些不同的欄位
[
{
"traceId": "3112dd04c3112036",
"id": "3112dd04c3112036",
"kind": "SERVER",
"name": "get /2/api",
"timestamp": 1618480662355011,
"duration": 12769,
"localEndpoint": {
"serviceName": "SERVICE2",
"ipv4": "172.24.132.32"
},
"remoteEndpoint": {
"ipv4": "111.25.140.166",
"port": 50214
},
"tags": {
"http.method": "GET",
"http.path": "/2/api",
"mvc.controller.class": "Controller",
"mvc.controller.method": "get2Api"
}
},
{
"traceId": "3112dd04c3112036",
"parentId": "3112dd04c3112036",
"id": "b4bd9859c690160a",
"name": "msg2.1",
"timestamp": 1618480662357211,
"duration": 11069,
"localEndpoint": {
"serviceName": "SERVICE2"
},
"tags": {
"class": "MSG",
"method": "msg2.1"
}
},
{
"traceId": "3112dd04c3112036",
"parentId": "3112dd04c3112036",
"id": "c31d9859c69a2b21",
"name": "msg2.2",
"timestamp": 1618480662357201,
"duration": 10768,
"localEndpoint": {
"serviceName": "SERVICE2"
},
"tags": {
"class": "MSG",
"method": "msg2.2"
}
},
{
"traceId": "3112dd04c3112036",
"parentId": "b4bd9859c690160a",
"id": "f1659c981c0f4744",
"kind": "CLIENT",
"name": "get /3/api",
"timestamp": 1618480662358201,
"duration": 9206,
"localEndpoint": {
"serviceName": "SERVICE2",
"ipv4": "172.24.132.32"
},
"tags": {
"http.method": "GET",
"http.path": "/3/api"
}
},
{
"traceId": "3112dd04c3112036",
"parentId": "c31d9859c69a2b21",
"id": "73cd1cab1d72a971",
"kind": "CLIENT",
"name": "get /4/api",
"timestamp": 1618480662358211,
"duration": 9349,
"localEndpoint": {
"serviceName": "SERVICE2",
"ipv4": "172.24.132.32"
},
"tags": {
"http.method": "GET",
"http.path": "/4/api"
}
}
]
圖 10
資料儲存
Zipkin 支援 MySQL、Cassandra 和 ElasticSearch 三種資料儲存,這三者都存在各自的缺點:
- MySQL:採集的 Tracing 資訊基本都在每天上億行甚至百億行以上,MySQL 無法支撐這麼大資料量。
- Cassandra:能支援對單個 Trace 的 Span 資訊分析,但對聚合查詢等資料統計分析場景支援不好
- ElasticSearch:能支援單個 Trace 的分析和簡單的聚合查詢分析,但對於一些較複雜的資料分析計算不能很好的支援,比如涉及到 Join、視窗函式等等的計算需求,尤其是任務間依賴計算,Zipkin 目前還不能實時計算,需要通過離線跑 Spark 任務計算任務間依賴資訊。
我們在實踐中也是首先使用 ElasticSearch,發現了上面提到的問題,比如 Zipkin 的服務依賴拓撲必須使用離線方式計算,便新增了 StarRocks 作為底層資料儲存。將 Zipkin 的 trace 資料匯入到StarRocks很方便,基本步驟只需要兩步,CREATE TABLE + CREATE ROUTINE LOAD。
另外,在呼叫鏈路效能瓶頸分析場景中,要將單個服務看作黑盒,只關注 RPC SPAN,遮蔽掉服務內部的 Messaging Span,使用了 Flink 對服務內部 span 進行 ParentID 溯源,即從 RPC Client SPAN,一直追溯到同一服務同一 Trace ID 的 RPC Server SPAN,用 RPC Server SPAN 的 ID 替換 RPC Client SPAN 的parentId,最後通過Flink-Connector-StarRocks將轉換後的資料實時寫入StarRocks。
基於 StarRocks 的資料儲存架構流程如下圖所示。
圖 11
CREATE TABLE
建表語句示例參考如下,有如下幾點注意點:
- 包括 Zipkin 和 zipkin_trace_perf 兩張表,zipkin_trace_perf 表只用於呼叫鏈路效能瓶頸分析場景,其他統計分析都適用 Zipkin 表
- 通過採集資訊中的 Timestamp 欄位,生成 dt、hr、min 時間欄位,便於後續統計分析
- 採用 DUPLICATE 模型、Bitmap 索引等設定,加快查詢速度
- Zipkin 表使用id作為分桶欄位,在查詢服務拓撲時,查詢計劃會優化為 Colocate Join,提升查詢效能。
Zipkin
CREATE TABLE `zipkin` (
`traceId` varchar(24) NULL COMMENT "",
`id` varchar(24) NULL COMMENT "Span ID",
`localEndpoint_serviceName` varchar(512) NULL COMMENT "",
`dt` int(11) NULL COMMENT "",
`parentId` varchar(24) NULL COMMENT "",
`timestamp` bigint(20) NULL COMMENT "",
`hr` int(11) NULL COMMENT "",
`min` bigint(20) NULL COMMENT "",
`kind` varchar(16) NULL COMMENT "",
`duration` int(11) NULL COMMENT "",
`name` varchar(300) NULL COMMENT "",
`localEndpoint_ipv4` varchar(16) NULL COMMENT "",
`remoteEndpoint_ipv4` varchar(16) NULL COMMENT "",
`remoteEndpoint_port` varchar(16) NULL COMMENT "",
`shared` int(11) NULL COMMENT "",
`tag_error` int(11) NULL DEFAULT "0" COMMENT "",
`error_msg` varchar(1024) NULL COMMENT "",
`tags_http_path` varchar(2048) NULL COMMENT "",
`tags_http_method` varchar(1024) NULL COMMENT "",
`tags_controller_class` varchar(100) NULL COMMENT "",
`tags_controller_method` varchar(1024) NULL COMMENT "",
INDEX service_name_idx (`localEndpoint_serviceName`) USING BITMAP COMMENT ''
) ENGINE=OLAP
DUPLICATE KEY(`traceId`, `parentId`, `id`, `timestamp`, `localEndpoint_serviceName`, `dt`)
COMMENT "OLAP"
PARTITION BY RANGE(`dt`)
(PARTITION p20220104 VALUES [("20220104"), ("20220105")),
PARTITION p20220105 VALUES [("20220105"), ("20220106")))
DISTRIBUTED BY HASH(`id`) BUCKETS 100
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.time_zone" = "Asia/Shanghai",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "2",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "100",
"in_memory" = "false",
"storage_format" = "DEFAULT"
);
zipkin_trace_perf
CREATE TABLE `zipkin_trace_perf` (
`traceId` varchar(24) NULL COMMENT "",
`id` varchar(24) NULL COMMENT "",
`dt` int(11) NULL COMMENT "",
`parentId` varchar(24) NULL COMMENT "",
`localEndpoint_serviceName` varchar(512) NULL COMMENT "",
`timestamp` bigint(20) NULL COMMENT "",
`hr` int(11) NULL COMMENT "",
`min` bigint(20) NULL COMMENT "",
`kind` varchar(16) NULL COMMENT "",
`duration` int(11) NULL COMMENT "",
`name` varchar(300) NULL COMMENT "",
`tag_error` int(11) NULL DEFAULT "0" COMMENT ""
) ENGINE=OLAP
DUPLICATE KEY(`traceId`, `id`, `dt`, `parentId`, `localEndpoint_serviceName`)
COMMENT "OLAP"
PARTITION BY RANGE(`dt`)
(PARTITION p20220104 VALUES [("20220104"), ("20220105")),
PARTITION p20220105 VALUES [("20220105"), ("20220106")))
DISTRIBUTED BY HASH(`traceId`) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.time_zone" = "Asia/Shanghai",
"dynamic_partition.start" = "-60",
"dynamic_partition.end" = "2",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "12",
"in_memory" = "false",
"storage_format" = "DEFAULT"
);
ROUTINE LOAD
ROUTINE LOAD 建立語句示例如下:
CREATE ROUTINE LOAD zipkin_routine_load ON zipkin COLUMNS(
id,
kind,
localEndpoint_serviceName,
traceId,
`name`,
`timestamp`,
`duration`,
`localEndpoint_ipv4`,
`remoteEndpoint_ipv4`,
`remoteEndpoint_port`,
`shared`,
`parentId`,
`tags_http_path`,
`tags_http_method`,
`tags_controller_class`,
`tags_controller_method`,
tmp_tag_error,
tag_error = if(`tmp_tag_error` IS NULL, 0, 1),
error_msg = tmp_tag_error,
dt = from_unixtime(`timestamp` / 1000000, '%Y%m%d'),
hr = from_unixtime(`timestamp` / 1000000, '%H'),
`min` = from_unixtime(`timestamp` / 1000000, '%i')
) PROPERTIES (
"desired_concurrent_number" = "3",
"max_batch_interval" = "50",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
"max_error_number" = "1000000",
"strict_mode" = "false",
"format" = "json",
"strip_outer_array" = "true",
"jsonpaths" = "[\"$.id\",\"$.kind\",\"$.localEndpoint.serviceName\",\"$.traceId\",\"$.name\",\"$.timestamp\",\"$.duration\",\"$.localEndpoint.ipv4\",\"$.remoteEndpoint.ipv4\",\"$.remoteEndpoint.port\",\"$.shared\",\"$.parentId\",\"$.tags.\\\"http.path\\\"\",\"$.tags.\\\"http.method\\\"\",\"$.tags.\\\"mvc.controller.class\\\"\",\"$.tags.\\\"mvc.controller.method\\\"\",\"$.tags.error\"]"
)
FROM
KAFKA (
"kafka_broker_list" = "IP1:PORT1,IP2:PORT2,IP3:PORT3",
"kafka_topic" = "XXXXXXXXX"
);
Flink 溯源 Parent ID
針對呼叫鏈路效能瓶頸分析場景中,使用 Flink 進行 Parent ID 溯源,程式碼示例如下:
env
// 新增kafka資料來源
.addSource(getKafkaSource())
// 將採集到的Json字串轉換為JSONArray,
// 這個JSONArray是從單個服務採集的資訊,裡面會包含多個Trace的Span資訊
.map(JSON.parseArray(_))
// 將JSONArray轉換為JSONObject,每個JSONObejct就是一個Span
.flatMap(_.asScala.map(_.asInstanceOf[JSONObject]))
// 將Span的JSONObject物件轉換為Bean物件
.map(jsonToBean(_))
// 以traceID+localEndpoint_serviceName作為key對span進行分割槽生成keyed stream
.keyBy(span => keyOfTrace(span))
// 使用會話視窗,將同一個Trace的不同服務上的所有Span,分發到同一個固定間隔的processing-time視窗
// 這裡為了實現簡單,使用了processing-time session視窗,後續我們會使用starrocks的UDAF函式進行優化,去掉對Flink的依賴
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
// 使用Aggregate視窗函式
.aggregate(new TraceAggregateFunction)
// 將經過溯源的span集合展開,便於呼叫flink-connector-starrocks
.flatMap(spans => spans)
// 使用flink-connector-starrocks sink,將資料寫入starrocks中
.addSink(
StarRocksSink.sink(
StarRocksSinkOptions.builder().withProperty("XXX", "XXX").build()))
分析計算
以 圖 6 作為一個微服務系統用例,給出各個統計分析場景對應的 StarRocks SQL 語句。
服務內分析
上游服務請求指標統計
下面的 SQL 使用 Zipkin 表資料,計算服務 Service2 請求上游服務 Service3 和上游服務 Service4 的查詢統計資訊,按小時和介面分組統計查詢指標
select
hr,
name,
req_count,
timeout / req_count * 100 as timeout_rate,
error_count / req_count * 100 as error_rate,
avg_duration,
tp95,
tp99
from
(
select
hr,
name,
count(1) as req_count,
AVG(duration) / 1000 as avg_duration,
sum(if(duration > 200000, 1, 0)) as timeout,
sum(tag_error) as error_count,
percentile_approx(duration, 0.95) / 1000 AS tp95,
percentile_approx(duration, 0.99) / 1000 AS tp99
from
zipkin
where
localEndpoint_serviceName = 'Service2'
and kind = 'CLIENT'
and dt = 20220105
group by
hr,
name
) tmp
order by
hr
下游服務響應指標統計
下面的 SQL 使用 Zipkin 表資料,計算服務 Service2 響應下游服務 Service1 的查詢統計資訊,按小時和介面分組統計查詢指標。
select
hr,
name,
req_count,
timeout / req_count * 100 as timeout_rate,
error_count / req_count * 100 as error_rate,
avg_duration,
tp95,
tp99
from
(
select
hr,
name,
count(1) as req_count,
AVG(duration) / 1000 as avg_duration,
sum(if(duration > 200000, 1, 0)) as timeout,
sum(tag_error) as error_count,
percentile_approx(duration, 0.95) / 1000 AS tp95,
percentile_approx(duration, 0.99) / 1000 AS tp99
from
zipkin
where
localEndpoint_serviceName = 'Service2'
and kind = 'SERVER'
and dt = 20220105
group by
hr,
name
) tmp
order by
hr
服務內部處理分析
下面的 SQL 使用 Zipkin 表資料,查詢服務 Service2 的介面 /2/api,按 Span Name 分組統計 Duration 等資訊。
with
spans as (
select * from zipkin where dt = 20220105 and localEndpoint_serviceName = "Service2"
),
api_spans as (
select
spans.id as id,
spans.parentId as parentId,
spans.name as name,
spans.duration as duration
from
spans
inner JOIN
(select * from spans where kind = "SERVER" and name = "/2/api") tmp
on spans.traceId = tmp.traceId
)
SELECT
name,
AVG(inner_duration) / 1000 as avg_duration,
percentile_approx(inner_duration, 0.95) / 1000 AS tp95,
percentile_approx(inner_duration, 0.99) / 1000 AS tp99
from
(
select
l.name as name,
(l.duration - ifnull(r.duration, 0)) as inner_duration
from
api_spans l
left JOIN
api_spans r
on l.parentId = r.id
) tmp
GROUP BY
name
服務間分析
服務拓撲統計
下面的 SQL 使用 Zipkin 表資料,計算服務間的拓撲關係,以及服務間介面 Duration 的統計資訊。
with tbl as (select * from zipkin where dt = 20220105)
select
client,
server,
name,
AVG(duration) / 1000 as avg_duration,
percentile_approx(duration, 0.95) / 1000 AS tp95,
percentile_approx(duration, 0.99) / 1000 AS tp99
from
(
select
c.localEndpoint_serviceName as client,
s.localEndpoint_serviceName as server,
c.name as name,
c.duration as duration
from
(select * from tbl where kind = "CLIENT") c
left JOIN
(select * from tbl where kind = "SERVER") s
on c.id = s.id and c.traceId = s.traceId
) as tmp
group by
client,
server,
name
呼叫鏈路效能瓶頸分析
下面的 SQL 使用 zipkin_trace_perf 表資料,針對某個服務介面響應超時的查詢請求,統計出每次請求的呼叫鏈路中處理耗時最長的服務或服務間呼叫,進而分析出效能熱點是在某個服務或服務間呼叫。
select
service,
ROUND(count(1) * 100 / sum(count(1)) over(), 2) as percent
from
(
select
traceId,
service,
duration,
ROW_NUMBER() over(partition by traceId order by duration desc) as rank4
from
(
with tbl as (
SELECT
l.traceId as traceId,
l.id as id,
l.parentId as parentId,
l.kind as kind,
l.duration as duration,
l.localEndpoint_serviceName as localEndpoint_serviceName
FROM
zipkin_trace_perf l
INNER JOIN
zipkin_trace_perf r
on l.traceId = r.traceId
and l.dt = 20220105
and r.dt = 20220105
and r.tag_error = 0 -- 過濾掉出錯的trace
and r.localEndpoint_serviceName = "Service1"
and r.name = "/1/api"
and r.kind = "SERVER"
and r.duration > 200000 -- 過濾掉未超時的trace
)
select
traceId,
id,
service,
duration
from
(
select
traceId,
id,
service,
(c_duration - s_duration) as duration,
ROW_NUMBER() over(partition by traceId order by (c_duration - s_duration) desc) as rank2
from
(
select
c.traceId as traceId,
c.id as id,
concat(c.localEndpoint_serviceName, "=>", ifnull(s.localEndpoint_serviceName, "?")) as service,
c.duration as c_duration,
ifnull(s.duration, 0) as s_duration
from
(select * from tbl where kind = "CLIENT") c
left JOIN
(select * from tbl where kind = "SERVER") s
on c.id = s.id and c.traceId = s.traceId
) tmp1
) tmp2
where
rank2 = 1
union ALL
select
traceId,
id,
service,
duration
from
(
select
traceId,
id,
service,
(s_duration - c_duration) as duration,
ROW_NUMBER() over(partition by traceId order by (s_duration - c_duration) desc) as rank2
from
(
select
s.traceId as traceId,
s.id as id,
s.localEndpoint_serviceName as service,
s.duration as s_duration,
ifnull(c.duration, 0) as c_duration,
ROW_NUMBER() over(partition by s.traceId, s.id order by ifnull(c.duration, 0) desc) as rank
from
(select * from tbl where kind = "SERVER") s
left JOIN
(select * from tbl where kind = "CLIENT") c
on s.id = c.parentId and s.traceId = c.traceId
) tmp1
where
rank = 1
) tmp2
where
rank2 = 1
) tmp3
) tmp4
where
rank4 = 1
GROUP BY
service
order by
percent desc
SQL 查詢的結果如下圖所示,在超時的 Trace 請求中,效能瓶頸服務或服務間呼叫的比例分佈。
圖 12
03 實踐效果
目前搜狐智慧媒體已在 30+ 個服務中接入 Zipkin,涵蓋上百個線上服務例項,1% 的取樣率每天產生近 10億 多行的日誌。
通過 Zipkin Server 查詢 StarRocks,獲取的 Trace 資訊如下圖所示:
圖 13
通過 Zipkin Server 查詢 StarRocks,獲取的服務拓撲資訊如下圖所示:
圖 14
基於 Zipkin StarRocks 的鏈路追蹤體系實踐過程中,明顯提升了微服務監控分析能力和工程效率:
提升微服務監控分析能力
- 在監控報警方面,可以基於 StarRocks 查詢統計線上服務當前時刻的響應延遲百分位數、錯誤率等指標,根據這些指標及時產生各類告警;
- 在指標統計方面,可以基於 StarRocks 按天、小時、分鐘等粒度統計服務響應延遲的各項指標,更好的瞭解服務執行狀況;
- 在故障分析方面,基於 StarRocks 強大的 SQL 計算能力,可以進行服務、時間、介面等多個維度的探索式分析查詢,定位故障原因。
提升微服務監控工程效率
Metric 和 Logging 資料採集,很多需要使用者手動埋點和安裝各種採集器 Agent,資料採集後儲存到 ElasticSearch 等儲存系統,每上一個業務,這些流程都要操作一遍,非常繁瑣,且資源分散不易管理。
而使用 Zipkin + StarRocks 的方式,只需在程式碼中引入對應庫 SDK,設定上報的 Kafka 地址和取樣率等少量配置資訊,Tracing 便可自動埋點採集,通過 zikpin server 介面進行查詢分析,非常簡便。
04 總結與展望
基於 Zipkin+StarRocks 構建鏈路追蹤系統,能夠提供微服務監控的 Monitoring 和 Observability 能力,提升微服務監控的分析能力和工程效率。
後續有幾個優化點,可以進一步提升鏈路追蹤系統的分析能力和易用性:
- 使用 StarRocks 的 UDAF、視窗函式等功能,將 Parent ID 溯源下沉到 StarRocks計算,通過計算後置的方式,取消對 Flink 的依賴,進一步簡化整個系統架構。
- 目前對原始日誌中的 tag s等欄位,並沒有完全採集,StarRocks 正在實現 Json 資料型別,能夠更好的支援 tags 等巢狀資料型別。
- Zipkin Server 目前的介面還稍顯簡陋,我們已經打通了 Zipkin Server 查詢 StarRokcs,後續會對 Zipkin Server 進行 U I等優化,通過 StarRocks 強大的計算能力實現更多的指標查詢,進一步提升使用者體驗。
05 參考文件
- 《雲原生計算重塑企業IT架構 - 分散式應用架構》:
https://developer.aliyun.com/article/717072 - What is Upstream and Downstream in Software Development?
https://reflectoring.io/upstream-downstream/ - Metrics, tracing, and logging:
https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html - The 3 pillars of system observability:logs, metrics and tracing:
https://iamondemand.com/blog/the-3-pillars-of-system-observability-logs-metrics-and-tracing/ - observability 3 ways: logging, metrics and tracing:
https://speakerdeck.com/adriancole/observability-3-ways-logging-metrics-and-tracing - Dapper, a Large-Scale Distributed Systems Tracing Infrastructure:
https://static.googleusercontent.com/media/research.google.com/en//archive/papers/dapper-2010-1.pdf - Jaeger:www.jaegertracing.io
- Zipkin:https://zipkin.io/
- opentracing.io:
https://opentracing.io/docs/ - opencensus.io:
https://opencensus.io/ - opentelemetry.io:
https://opentelemetry.io/docs/ - Microservice Observability, Part 1: Disambiguating Observability and Monitoring:
https://bravenewgeek.com/microservice-observability-part-1-disambiguating-observability-and-monitoring/ - How to Build Observable Distributed Systems:
https://www.infoq.com/presentations/observable-distributed-ststems/ - Monitoring and Observability:
https://copyconstruct.medium.com/monitoring-and-observability-8417d1952e1c - Monitoring Isn't Observability:
https://orangematter.solarwinds.com/2017/09/14/monitoring-isnt-observability/ - Spring Cloud Sleuth Documentation:
https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/getting-started.html#getting-started