當系統演化為由數十、甚至數百個服務組成的分散式架構時,僅靠翻閱各服務的 Log 來釐清一筆 Request 的全貌,會耗費大量心力且效率低落。分散式追蹤(Distributed Tracing)正是為了解決這個痛點而生,本章整理它的演變脈絡、核心概念,以及以 OpenTelemetry (OTel) 為主軸的整體資料處理流程。

分散式追蹤的演變#

分散式追蹤的發展可以從幾個指標性的專案來理解:

  • Google Dapper:2010 年 Google 公開內部使用的 Tracing 系統,提出了 Trace ID、Span、Sampling 等被後續系統廣泛沿用的概念,可視為這個領域的開山之作。
  • Twitter Zipkin:2012 年 Twitter 將內部 Tracing 系統開源,是第一個對外的 Tracing System,提供完整的 SDK、收集、儲存與查詢能力。
  • Uber Jaeger:2015 年 Uber 也將自家 Tracing 系統開源,同樣涵蓋 SDK、收集、儲存與查詢的完整流程。

通用 API 的整併#

為了讓各家工具不再各自為政,業界陸續推出多個跨工具的 API 與 SDK 規範:

  • OpenTracing:2015 年由 Lightstep 與 Jaeger 作者等人共同發起,目標是建立通用 Tracing API,並於 2016 年加入 CNCF。
  • OpenCensus:2017 年由 Google 提出,理念與 OpenTracing 類似。
  • OpenTelemetry (OTel):2019 年將 OpenCensus 與 OpenTracing 合併,成為 CNCF 專案,除了提供統一 API 與多語言 SDK,覆蓋範圍也從 Trace 擴展到 Metrics 與 Log,三者統稱為 Telemetry Data。

經過多年整併,OpenTelemetry 已是分散式追蹤領域的事實標準,同時也讓 Trace、Metric、Log 三類訊號能在同一套 API 與資料模型下整合。

Trace 與 Span 的基本結構#

Trace 由多個 Span 組成,整段呼叫鏈以一個 Trace ID 串接,每個 Span 擁有自己的 Span ID 並透過 Parent Span 來表示父子關係,記錄該段操作的開始時間、結束時間、耗時、屬性與事件等資訊。Span 在跨服務傳遞時遵循 W3C Trace Context 規範,透過 Context Propagation 把 Trace ID 與相關欄位放進 HTTP Header 等載體中。

Trace 資料的處理流程#

以 OpenTelemetry 為主軸來看,整體資料流可拆成以下幾個階段:

  • 生成:開發者透過 SDK 進行自動插樁(Auto Instrumentation)或手動插樁(Manual Instrumentation),在 HTTP 請求、資料庫查詢等關鍵階段建立 Span。
  • 收集:SDK 依照 OTLP(OpenTelemetry Protocol)規範,透過 gRPC 或 HTTP 將資料送往 Tracing Backend,gRPC 因為基於 HTTP/2 通常較為高效。
  • 儲存:依後端不同會落地到不同儲存層,例如 Zipkin 支援 Cassandra、Elasticsearch、MySQL;Jaeger 主要採用 Cassandra 與 Elasticsearch;Tempo 則使用 Object Storage(如 Amazon S3、Google Cloud Storage)。
  • 使用:透過各家專屬 UI 或 Grafana 來查詢與分析 Trace 資訊。

中間若有額外加工需求,資料也可能先經過 OpenTelemetry Collector、Fluent Bit 等處理器再轉發。

常見 Tracing Backend 與 UI#

常見的後端與檢視介面可整理如下:

  • Zipkin:搭配 Zipkin UI 使用。
  • Jaeger:搭配 Jaeger UI 使用。
  • Tempo:以 Grafana 作為主要 UI。
  • Grafana:可同時串接 Zipkin、Jaeger、Tempo 等多種資料源,方便跨後端比較。

取樣策略:Head-based vs Tail-based#

Trace 是三大訊號中資料量最龐大的一種——每一筆請求都可能展開成數十個 Span,每個 Span 又帶著時間戳、屬性與事件。在高流量服務上,全採樣(100% Sampling)幾乎不可能

  • 一個每秒 5 萬請求、平均每筆 30 個 Span 的系統,每秒就產生 150 萬個 Span。
  • 這些資料要經過網路傳輸、後端寫入、長期儲存與查詢索引,成本(頻寬、儲存、運算)會隨流量線性甚至超線性成長。

於是「只保留一部分 Trace」成為生產環境的必然選擇。問題是:在哪個時間點、用什麼依據決定要不要保留?這就是 Head-based 與 Tail-based 兩種取樣策略的分水嶺。

Head-based Sampling(前段取樣)#

在 Trace 一開始(根 Span 建立時)就擲骰子決定整條 Trace 的去留,這個決定隨 Context Propagation 一路傳給所有下游 Span。

  • 優點:簡單、便宜。決策在請求入口瞬間完成,不需要緩衝任何資料,記憶體與運算開銷極低;下游服務只是遵循上游傳來的取樣旗標。
  • 缺點:。決策發生時,請求還沒跑完,你根本不知道這筆請求最後會不會出錯、會不會變慢。換句話說,它會等機率地丟棄掉那些罕見但最有價值的錯誤與慢請求

Tail-based Sampling(後段取樣)#

等整條 Trace 的所有 Span 都到齊、請求已經結束,再根據完整結果(是否有錯誤、整體延遲是否超標、是否經過某個關鍵服務)決定保留與否。

  • 優點:聰明。可以制定「保留全部錯誤 Trace、保留所有 P99 以上的慢請求、其餘只抽 1%」這類以價值為導向的規則,把預算花在刀口上。
  • 缺點:複雜且昂貴。決策者必須先把同一條 Trace 的所有 Span 緩衝起來,等待一個時窗(Decision Window)才能判斷;這需要記憶體、需要在分散環境中把同一 trace_id 的 Span 路由到同一個決策節點,還要面對「Span 遲到或永遠不來」的處理。

兩者對比#

面向Head-based(前段取樣)Tail-based(後段取樣)
決策時機Trace 開始時(根 Span)Trace 結束、所有 Span 到齊後
決策依據機率(不看結果)完整結果(錯誤、延遲、路徑等)
複雜度低,無需緩衝高,需緩衝 + 時窗 + Span 路由
資源開銷極低高(記憶體、運算)
漏抓罕見錯誤會(按機率丟棄)不會(可規則化全保留)
典型場景流量巨大、可接受漏抓、求簡單重視錯誤與尾端延遲、願意投入維運成本

動態取樣率#

兩種策略都可搭配動態取樣率(Dynamic / Adaptive Sampling):依流量自動調節。例如低流量端點全採、高流量端點降到 1%,讓罕見服務不至於因固定低取樣率而幾乎沒有樣本;或在事故期間臨時拉高取樣率以保留更多現場。

取樣偏差:別把樣本當全貌#

無論哪種策略,取樣後的 Trace 都是有偏的樣本,這在分析時必須時時警惕:

  • Tail-based 若設定「優先保留錯誤 Trace」,那你看到的 Trace 集合中錯誤比例會遠高於真實流量——不能拿被取樣後的 Trace 直接推算錯誤率
  • 由 Trace 衍生的統計(如服務間呼叫量、延遲分佈)若直接基於取樣後資料計算,會失真。比較穩妥的做法是讓延遲、錯誤率等量化指標走 Metrics(在取樣之前就統計完成,例如 Span Metrics),Trace 只負責提供「代表性個案」的脈絡。

警告:Tail-based 取樣有幾個容易踩的坑。 同一條 Trace 的所有 Span 必須匯聚到同一個決策節點,否則決策者看到的是殘缺的 Trace——這讓 Tail-based 幾乎只能在集中式的 Gateway 層做,不能分散在各節點各自為政。 此外,緩衝時窗一旦設太短,遲到的 Span 會被當成「不存在」而誤判;設太長則記憶體壓力與決策延遲都會上升。

決策路徑大致如下:

flowchart TD
    A["要對 Trace 取樣"] --> B{"流量規模 / 預算?"}
    B -- "極高流量、求簡單、可接受漏抓" --> H["Head-based<br/>入口處按機率決定"]
    B -- "重視錯誤與尾端延遲、願投入維運" --> C{"能否把同一 trace_id<br/>的 Span 匯聚到同一節點?"}
    C -- "否" --> H
    C -- "是" --> T["Tail-based<br/>在 Gateway 層緩衝後依結果決定"]
    H --> M["量化指標另走 Metrics/Span Metrics,<br/>避免取樣偏差影響統計"]
    T --> M

Tail-based 的實作通常落在 OpenTelemetry Collector 的 Gateway 模式上,因為它天生適合「先匯聚、再決策」。一段典型的 Collector Tail Sampling 設定如下:

processors:
  tail_sampling:
    # 等待一條 Trace 所有 Span 到齊的時窗
    decision_wait: 10s
    # 同時追蹤的 Trace 數量上限(記憶體保護)
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      # 1) 只要有任何 Span 標記為錯誤,整條 Trace 全保留
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      # 2) 整體延遲超過 500ms 的慢請求全保留
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 500
      # 3) 其餘正常請求只抽樣 5%
      - name: baseline-sample
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

exporters:
  otlp:
    endpoint: tempo:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling]
      exporters: [otlp]

取樣策略的選擇本質上是「成本 vs 可見度」的工程取捨,屬於訊號的資料模型層議題。至於「該為哪些關鍵路徑投入多少可觀測性預算、Trace 在事故升級流程中如何被引用」,則牽涉 SLO 與事故文化,詳見 SLO 與錯誤預算

小結#

Tracing 在十多年的演進中已逐步成熟,OpenTelemetry 的出現補齊了 API 與生態系的最後一塊拼圖。今日只要選定支援 OTel 的 SDK,無論主後端是 Tempo、Jaeger 還是商業 SaaS,都能透過 OTLP 進行資料交換,未來搬遷或更換廠商的成本也大幅降低。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10333505