對於微服務架構來說,一個看似簡單的用戶請求,實際上可能要橫跨十幾個不同的服務才能完成。當系統出現問題時,我們很難就著單一個服務的錯誤來排查問題。這正是 OpenTelemetry Traces 要解決的核心問題:如何在分散式系統的複雜性中,重建並追蹤每個請求的完整生命週期。
Traces 是一個完整的請求流程,一個 request 會對應到唯一的 trace_id。而我們會透過這唯一的 trace_id 去追蹤在這個 request 裡面所有的相關操作。
一個 trace 則由多個 span 所組成,span 代表著一個系統操作的單位。而這些 span 的關係可以被視覺化成一個有向無環圖(Directed acyclic graph, DAG)。
從圖片中可以看到,同一個 request 下的所有 span 會被賦予相同的 Trace ID,而 span 與 span 之間也互相為彼此的子節點與父節點,例如,Span ID 002 就是 003 和 004 的 parent span。
市面上常見的 trace 工具如 Jaeger等,本身就支援 trace 的可視化功能,在這些工具中,將 span 以時間軸的方式視覺化,也是相當常見的方式。
如上圖所示,在同一個時間軸上,我們可以很清楚地看到每個 span 被處理時所花費的時間,同時,我們也可以了解各個 span 在被處理的時序性。從圖中可以看到:
像這樣的時間軸視圖特別適合用來分析性能瓶頸:我們可以看到哪些操作是序列執行的(如 C、D、E),哪些是並行的(如 B 和 C 的重疊部分)。如果能將序列操作改為並行執行,就可能縮短整體的 trace 處理時間。透過這樣的時間軸表示法,也能對整個系統的運作有更細微的認識。
Span 和 SpanContext 名稱相似,但這兩者的核心概念並不相同,以下是這兩個核心概念的差異:
Span 是一個完整的操作紀錄,包含:
SpanContext 則是 Span 內部的一個不可變物件,專門負責追蹤的關聯性,也是 span 的身分證明,裡面包含:
SpanContext 在產生後就無法做更改,假設要注入新的資訊,就只能再生成一個全新的 SpanContext。
簡單來說,Span 是完整的操作記錄,而 SpanContext 是其中負責跨服務傳遞的身份證明。當請求從一個服務跳到另一個服務時,傳遞的是 SpanContext 中的追蹤資訊,新的服務會基於這些資訊建立新的 Span,並將自己設定為子節點。
我們可以透過操作 Python SDK 來更了解 Span 與 SpanContext 之間的關係:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
# 初始化 Tracer Provider
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
# 將 trace 輸出到 console(方便觀察 span 與 context)
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
trace.get_tracer_provider().add_span_processor(span_processor)
# 建立 root span
with tracer.start_as_current_span("root-operation") as root_span:
print("Root SpanContext:", root_span.get_span_context())
# 建立 child span,會自動繼承 root span 的 SpanContext
with tracer.start_as_current_span("child-operation") as child_span:
print("Child SpanContext:", child_span.get_span_context())
這種設計讓 OpenTelemetry 能夠在分散式系統中實現無縫的 context propagation(上下文傳播:追蹤資訊在服務間自動傳遞的機制),確保每個 span 都能正確地關聯到同一個 trace 中,形成完整的請求追蹤鏈。
為了實現跨服務的 context propagation,OpenTelemetry 採用了 W3C Trace Context 規範,透過 HTTP headers 來傳遞追蹤資訊。主要包含兩個 headers:
traceparent: 包含標準化的追蹤資訊
version-trace_id-parent_span_id-trace_flags
00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
00
: 版本號0af7651916cd43dd8448eb211c80319c
: trace IDb7ad6b7169203331
: parent span ID01
: trace flagstracestate: 包含供應商特定的追蹤資訊
congo=t61rcWkgMzE
當 HTTP 請求在不同服務間傳遞時,這些 headers 確保了追蹤上下文能夠完整地保存和傳遞。
Traces 讓我們釐清不同微服務間的關係與交互,透過統一的 trace ID、結構化的 span 關係,以及自動化的 context propagation,讓原本分散的操作重新具有了清晰的因果關係。
如同前面 Metrics 與 Logs 的設計理念,Traces 不是一個孤立的解決方案,作為整個可觀測性系統的線索,將 logs 和 metrics 串聯成統一的試圖,讓開發者從宏觀的系統狀況到個別服務間的請求軌跡,讓系統可以真正的「被觀察」。
不過在實際的業務場景中,我們往往會希望這個請求能攜帶一些額外的上下文資訊,例如用戶ID、所在地區等。雖然 SpanContext 提供了基本的追蹤關聯性,但對於這類業務上下文的傳遞,OpenTelemetry 提供了另一個重要的機制。
於是明天,我們將詳細探討 OpenTelemetry 如何透過 Baggage 來跨服務地傳遞上下文。
Babak Fakhriloo - Use OpenTelemetry to implement distributed tracing
Pei-Ya Chiu - Observability 三本柱之 Distributed Tracing 介紹
TigerData Blog - What Are Traces and How to Use Them to Build Better Software