🌐 This page hasn't been translated yet — showing the original Chinese. Translated posts

追蹤與 Tempo:一個請求走過的路

· tech

#observability#tracing

📑 目錄

三支柱最後一個:traces。它補的是 metric 和 log 之間那個最關鍵的洞——「在哪一段?」 Metric 告訴你「checkout 慢」,但一個請求橫跨了八個服務,到底卡在哪一 hop?Log 是每個服務各自、斷開的。Trace 把一趟請求的完整旅程縫成一條,讓你「看見」是哪一段拖慢了它。

一個 trace,就是一趟請求的 waterfall

一個請求的 trace:spans 排在時間軸上(waterfall) gateway auth checkout payment← 600ms 瓶頸 inventory db write 05001000 ms metric 只說「checkout 慢」;trace 指出是 payment 那 600ms —— 這是 metric/log 給不了的「在哪一段」
一個 trace = 一趟請求的完整旅程;裡面每一段工作是一個 span(有名字、起訖時間 → 長度、以及 parent → 樹狀關係)。把這些 span 依時間排開,就是一張 waterfall——你一眼就看出哪一段最長、哪些平行跑、哪一段出錯。這張圖裡,metric 只能告訴你「checkout 這服務慢」,但 trace 直接指著 payment 那 600ms 說「就是這裡」。「橫跨多服務、到底卡在哪一 hop」——這是三支柱裡只有 trace 能回答的問題

靠什麼把它縫起來:context propagation

那 Tempo 怎麼知道這八個服務的 span,屬於「同一個請求」?靠一個貫穿全程的 trace-id。它在入口生成一次,然後隨著每一次服務呼叫,透過 HTTP header(W3C 標準的 traceparent)一路傳下去;每個服務都把自己的 span 掛在同一個 trace-id 底下回報。這個「把 context 傳下去」的動作,就是整套 tracing 的命脈:

context propagation:一個 trace-id,一路傳下去 Gateway Checkout Payment traceparent=abc123 traceparent=abc123 各自回報 span(都掛 trace=abc123) Tempo用同一個 trace-id 縫成一條 trace 鏈上任一服務忘了轉發 header → trace 就從那裡斷成兩截 ✗
一個 trace-id 在入口生成,靠 HTTP header(traceparent)一路傳下去;每個服務把自己的 span 掛在同一個 trace-id 回報給 Tempo,Tempo 就能把它們縫成一條完整的 trace。這也是 tracing 最脆弱的地方:只要鏈上有一個服務忘了把 header 轉發下去,trace 就從那裡斷成兩截——你會在最想看到全貌時只看到一半。所以導入 tracing,第一件事是確認 propagation 每一跳都沒斷(用 OpenTelemetry 的自動注入能省掉大半手工)

Tempo:給我 trace-id,我給你整條 trace

Tempo 的省錢哲學,跟 Loki 一模一樣:它不索引 span 的內容,只需要 trace-id 就能撈出整條 trace,所有資料丟 object storage。所以它便宜到能收海量 trace。那「我沒有 trace-id、想找出所有很慢的 trace」怎麼辦?靠另外兩支柱給你線索——從一個 metric 尖峰或一行 error log 拿到 trace-id(這叫 exemplar / 關聯,是下一批的主題),再交給 Tempo 撈。心智模型一句話:Tempo 負責「給 ID 換整條 trace」,找 ID 的活交給 metric 和 log。

取樣:留下「有趣的」,丟掉無聊的

每個請求都是一條 trace,量大到你不可能 100% 全留,得取樣。兩種思路:

  • 頭部取樣(head sampling):請求一進來就擲骰子,例如固定留 1%。簡單,但它會用同樣的機率丟掉你最想看的那條出錯 trace
  • 尾部取樣(tail sampling):等整條 trace 跑完再決定——留下所有出錯的、所有很慢的,把無聊的成功請求丟掉。貴一點(要先緩衝整條),但它留下的是異常

判準很清楚:你事後會回去看的,永遠是「不對勁」的那些,不是平均的那些。 所以只要撐得起,尾部取樣「留異常」幾乎總是更值。

反思

trace 補的洞,是微服務自己挖出來的

在單體時代,「一個請求卡在哪」這題根本不存在——就一個程序,堆疊一攤開就看到了。是微服務把「一個請求的因果鏈」打散到了好幾台機器上,才讓「在哪一 hop」變成一個需要專門工具才能回答的問題。這讓我看清一件事:每一種可觀測性訊號,往往是為了補某個架構演化帶出來的盲點。 你把系統拆得越散、跨越的服務越多,trace 就從「有了不錯」變成「沒有就瞎」。所以要不要上 tracing,其實不是看它多潮,而是看你的請求要橫跨幾個服務——服務多到你講不清一個請求的全貌時,它就是必需品,不是奢侈品。

context propagation 教我:分散式系統裡,難的是「把上下文接好」

整套 tracing 幾百萬的價值,全繫在一個小到不能再小的細節:那個 trace-id 有沒有被每一跳都乖乖傳下去。只要鏈上有一個服務忘了轉發 header,trace 就斷了,你就在最需要全貌的時候看到半截。這件事給我的體會遠超 tracing——在分散式系統裡,「上下文的傳遞」往往比「單點的正確」更難、也更容易被忽略。 每個服務單獨看都對,但沒把 context(trace-id、使用者身分、request deadline…)一路接好,整體就是瞎的。所以我現在看任何跨服務的設計,都會多問一句:這條鏈上,該一起傳下去的東西,每一跳都接住了嗎? 這比檢查任何單一服務的正確性都重要。

取樣逼你誠實回答:留不起全部,那你想留下什麼?

traces 貴到你必須取樣,而取樣的設計,會逼你把一個平常含糊帶過的問題攤開:當我留不起全部,我到底想留下什麼? 頭部取樣的隨機 1% 最省事,卻會用同樣機率丟掉那條你最想看的出錯 trace;尾部取樣多花一點,換來的是「所有異常都留著」。想通這個取捨後,我把它變成一條通用原則:凡是留不起全部的地方——trace、log、抽樣分析——都該優先留下『偏離正常』的,而不是隨機的樣本。 因為你會為之行動的,從來是那些不對勁的東西,不是平均值。這正收束回這系列的主軸——觀測的終點是行動,而異常,才是行動的起點。三支柱到此講完,下一批我們把它們串起來:讓一個 metric 尖峰,能一鍵跳到那條 trace、再跳到那行 log。