萬字詳解!搜狐智慧媒體基於 Zipkin 和 StarRocks 的微服務鏈路追蹤實踐

StarRocks發表於2022-03-28
作者:翟東波、葉書俊

在微服務體系架構下,搜狐智慧媒體使用 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
圖 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

  1. 使用者傳送給 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 完整資訊進行上報。
  2. 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 進行上報
  3. 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 進行上報
  4. Service2 的 msg2.1 和 msg2.2 會分別建立一個 Messaging Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 C 和 F),記錄 Timestamp 等資訊,分別向 Service3 和 Service4 傳送請求;msg2.1 和 msg2.2 收到響應後,會分別處理 Messaging Span 相關資訊,並將兩個 Messaging Span 進行上報
  5. 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 進行上報
  6. Service3 收到 Service2 的Request中,包含 Trace(X)和Span(D)等資訊,Service3 會建立一個 Server Span,使用 X 作為 Trace ID,D 作為 SpanID,內部呼叫 msg3;Service3 在收到 msg3 的返回後,Service3 會處理此 Server Span 相關資訊,並將此 Server Span 進行上報
  7. Service3 的 msg3 會分別建立一個 Messaging Span,使用 X 作為 Trace ID,隨機生成全域性唯一的 SpanID(如圖中的 E),記錄 Timestamp 等資訊,msg3 處理完成後,處理此 Messaging Span 相關資訊,並將此 Messaging Span 進行上報
  8. Service4 收到 Service2 的 Request 中,包含 Trace(X)和 Span(G)等資訊,Service4 會建立一個 Server Span,使用 X 作為 Trace ID,G 作為 SpanID,再向 Service5 傳送請求;Service4 在收到 Service5 的響應後,Service4 會處理此 Server Span 相關資訊,並將此 Server Span 進行上報
  9. 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 能力,提升微服務監控的分析能力和工程效率。
後續有幾個優化點,可以進一步提升鏈路追蹤系統的分析能力和易用性:

  1. 使用 StarRocks 的 UDAF、視窗函式等功能,將 Parent ID 溯源下沉到 StarRocks計算,通過計算後置的方式,取消對 Flink 的依賴,進一步簡化整個系統架構。
  2. 目前對原始日誌中的 tag s等欄位,並沒有完全採集,StarRocks 正在實現 Json 資料型別,能夠更好的支援 tags 等巢狀資料型別。
  3. Zipkin Server 目前的介面還稍顯簡陋,我們已經打通了 Zipkin Server 查詢 StarRokcs,後續會對 Zipkin Server 進行 U I等優化,通過 StarRocks 強大的計算能力實現更多的指標查詢,進一步提升使用者體驗。

05 參考文件

  1. 《雲原生計算重塑企業IT架構 - 分散式應用架構》:
    https://developer.aliyun.com/article/717072
  2. What is Upstream and Downstream in Software Development?
    https://reflectoring.io/upstream-downstream/
  3. Metrics, tracing, and logging:
    https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html
  4. The 3 pillars of system observability:logs, metrics and tracing:
    https://iamondemand.com/blog/the-3-pillars-of-system-observability-logs-metrics-and-tracing/
  5. observability 3 ways: logging, metrics and tracing:
    https://speakerdeck.com/adriancole/observability-3-ways-logging-metrics-and-tracing
  6. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure:
    https://static.googleusercontent.com/media/research.google.com/en//archive/papers/dapper-2010-1.pdf
  7. Jaeger:www.jaegertracing.io
  8. Zipkin:https://zipkin.io/
  9. opentracing.io:
    https://opentracing.io/docs/
  10. opencensus.io:
    https://opencensus.io/
  11. opentelemetry.io:
    https://opentelemetry.io/docs/
  12. Microservice Observability, Part 1: Disambiguating Observability and Monitoring:
    https://bravenewgeek.com/microservice-observability-part-1-disambiguating-observability-and-monitoring/
  13. How to Build Observable Distributed Systems:
    https://www.infoq.com/presentations/observable-distributed-ststems/
  14. Monitoring and Observability:
    https://copyconstruct.medium.com/monitoring-and-observability-8417d1952e1c
  15. Monitoring Isn't Observability:
    https://orangematter.solarwinds.com/2017/09/14/monitoring-isnt-observability/
  16. Spring Cloud Sleuth Documentation:
    https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/getting-started.html#getting-started

相關文章