為什麼需要 Distributed Tracing?#

想像一個使用者點了「購買」按鈕。這個請求經過 API Gateway,轉到訂單服務,訂單服務呼叫庫存服務確認有貨,再呼叫支付服務扣款,最後發一則訊息到通知服務寄送確認信。整趟旅程跨越五個服務、三種通訊協定,耗時 1.2 秒。

問題來了:哪一段慢了?

在單體應用的時代,你可以靠 Stack Trace 一路追到問題根源。但當調用鏈跨越進程邊界,Stack Trace 就斷了——你只能在每個服務的 Log 裡翻找,試圖用時間戳拼湊出完整的故事。這就像把一本小說拆成五本,分別丟掉目錄和頁碼,然後叫你還原劇情。

Distributed Tracing 就是把這些頁碼接回去的技術。

核心概念:Trace、Span、Span Context#

理解 Distributed Tracing 只需要三個概念:

  • Trace:一個請求的完整旅程,從進入系統到回應完成。每個 Trace 有一個唯一的 Trace ID
  • Span:旅程中的一段操作,例如「呼叫庫存服務」或「查詢資料庫」。每個 Span 記錄了操作名稱、開始時間、持續時間、狀態碼,以及它的 Parent Span
  • Span Context:跨服務傳播的上下文資訊,至少包含 Trace ID 和 Span ID,讓下游服務知道自己屬於哪條 Trace 的哪個環節

一條 Trace 由多個 Span 組成,Span 之間透過 Parent-Child 關係形成樹狀結構。當你在 Trace UI 上看到那個經典的瀑布圖(Waterfall View),每一行就是一個 Span,縮排代表調用層級,長度代表耗時。

Span 不只用在服務間的呼叫。一個服務內部的重要操作(如資料庫查詢、快取存取)也可以是獨立的 Span。粒度由你決定,但越細的追蹤帶來越多的效能開銷。

Context Propagation:追蹤鏈是怎麼接上的?#

Distributed Tracing 最關鍵的機制是 Context Propagation——把 Span Context 從一個服務傳到下一個服務。

具體做法很直覺:

  • HTTP 呼叫:將 Trace ID 和 Span ID 放進 HTTP Header(如 traceparent Header,W3C Trace Context 標準)
  • gRPC 呼叫:放進 gRPC Metadata
  • 訊息佇列:放進 Message Header 或 Attributes

當下游服務收到請求時,從 Header 中取出 Span Context,建立一個新的 Child Span,並繼承相同的 Trace ID。如此一來,不管經過多少個服務,所有的 Span 都共享同一個 Trace ID,最終能被組裝回一條完整的 Trace。

Context Propagation 需要所有服務使用相同的傳播格式。如果 A 服務用 W3C Trace Context,B 服務用 B3 格式,追蹤鏈就會在這裡斷裂。這就是為什麼 OpenTelemetry 統一標準如此重要。

歷史發展:從 Dapper 到 OpenTelemetry#

Distributed Tracing 的發展歷程,本身就是一部標準之爭的歷史:

  • 2010 – Google Dapper 論文:奠定了分散式追蹤的理論基礎,提出 Trace、Span、Annotation 等核心概念。幾乎所有後來的系統都受其啟發
  • 2012 – Twitter 開源 Zipkin:第一個廣泛使用的開源追蹤系統,使用 B3 Propagation 格式
  • 2016 – Uber 開源 Jaeger:帶來更好的效能和擴展性,後來成為 CNCF 畢業專案
  • 2016 – OpenTracing:CNCF 推出的追蹤 API 標準,試圖統一各家 SDK 介面
  • 2017 – OpenCensus:Google 推出的計畫,不只做 Tracing 也做 Metrics,但與 OpenTracing 形成競爭
  • 2019 – OpenTelemetry 誕生:OpenTracing 和 OpenCensus 合併,成為 CNCF 的統一可觀測性標準。同時涵蓋 Traces、Metrics、Logs 三種信號

今天開始新專案,直接選 OpenTelemetry 就對了。它是 CNCF 活躍度僅次於 Kubernetes 的專案,已經成為業界的事實標準。你不需要再糾結 OpenTracing 和 OpenCensus 的差異——它們都已經合併進 OpenTelemetry。

Trace 資料的處理流程#

和 Metrics、Logs 一樣,Trace 資料也有自己的處理流程:

生成(Instrumentation)#

在應用程式中埋點產生 Span。可以是 Zero-code(透過 Agent 自動注入)、Library-level(框架整合)或 Manual(手動建立 Span)。

收集(Collection)#

Span 資料從應用程式送出,通常先經過 OTel Collector 這個中繼站。Collector 可以做格式轉換、取樣、過濾等處理。

儲存(Storage)#

處理後的 Trace 資料送到後端儲存,例如 Tempo(物件儲存,低成本)或 Jaeger(支援 Elasticsearch、Cassandra 等)。

使用(Consumption)#

透過 UI 查看完整的追蹤鏈、分析延遲瓶頸、產生 Service Graph。Grafana 可以同時整合 Tempo 和 Jaeger 的資料。

Sampling:為什麼不追蹤每一個請求?#

在高流量的系統中,每個請求都產生完整的 Trace 是不現實的。一個每秒處理一萬個請求的系統,如果每個請求平均產生 20 個 Span,那就是每秒 20 萬個 Span。儲存和傳輸的成本會非常驚人。

因此需要 Sampling(取樣)——只追蹤一部分請求。取樣策略分為兩大類:

  • Head-based Sampling:在請求進入系統時就決定是否追蹤(例如 10% 的機率)。優點是簡單且開銷低;缺點是可能漏掉那些罕見但重要的錯誤請求
  • Tail-based Sampling:先收集所有 Span,在請求完成後再決定是否保留。可以根據結果(如錯誤、高延遲)做出更聰明的決策;缺點是需要暫存大量資料,實作複雜度較高

Head-based Sampling 雖然簡單,但它有一個根本性的盲點:決策發生在請求開始時,此時你還不知道這個請求最終會不會出錯。如果你最關心的是錯誤追蹤,Tail-based Sampling 會是更好的選擇,但請做好承擔其複雜度的準備。

什麼時候該引入 Distributed Tracing?#

不是所有系統都需要 Distributed Tracing。以下是幾個判斷指標:

  • 服務數量:如果你的系統只有 2–3 個服務,靠 Log 和 Metrics 通常就夠了。當服務超過 5 個,跨服務的問題排查開始變得痛苦,這時 Tracing 的價值才真正浮現
  • 延遲敏感度:如果你需要精確知道延遲發生在哪一段,Tracing 比任何其他工具都有效
  • 團隊規模:當多個團隊各自負責不同的服務,Tracing 提供了一個共同的全局視圖,讓跨團隊的問題排查不再靠猜

即使你現在還不需要完整的 Tracing 基礎設施,也可以先在應用程式中導入 OpenTelemetry SDK。Zero-code Instrumentation 的成本極低,而當你未來需要時,資料已經準備好了。