當系統演化為由數十、甚至數百個服務組成的分散式架構時,僅靠翻閱各服務的 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 --> MTail-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