【從一台 Server 到分散式架構】第 20 篇:出問題怎麼回溯現場?——Log 與分散式追蹤
告警系統上線後,小明的睡眠品質好多了。
但有一天,告警在下午兩點跳出來:「結帳 API P99 > 3s,持續 5 分鐘。」
Grafana 上的圖很清楚,確實有問題。可問題是:結帳 API 背後走了五個服務——訂單服務、庫存服務、支付服務、優惠券服務、通知服務——哪個環節拖慢了?
小明打開各服務的 log,發現這些 log 的格式都不一樣:
- 訂單服務:
2026-03-27 14:02:33 INFO - 訂單建立成功 - 支付服務:
[ERROR] payment failed, user=1234 - 通知服務:
Notification sent.
沒有統一格式、沒有關聯 ID、沒有時間串聯——根本不知道這幾筆 log 是不是同一個請求。
小傑看了一眼:「你現在做的事叫『手工 log 比對』,五個服務還可以,五十個服務你就放棄了。需要兩個東西:結構化 Log 和 分散式追蹤。」
生活化的比喻:麵包屑路徑 vs 事後重建
調查一個多服務系統的問題,像是調查一場多部門的流程失誤。
如果每個部門各自保留的紀錄格式不同、時間沒對齊、沒有統一的事件編號——你只能靠猜、靠電話問,效率極低。
好的 Log 設計像是:每個部門都用同一套表單,每件事都有「案件編號」,你拿著案件編號就能把所有部門的處理記錄全部撈出來,按時間排好,完整還原整個流程。
結構化 Log:讓機器看得懂,而不只是人看得懂
傳統 log 是一行文字,給人閱讀:
2026-03-27 14:02:33 INFO 訂單建立成功 user_id=1234 order_id=9999
這對人來說還行,但對程式(log 搜尋工具)來說很麻煩——每行格式不統一,很難過濾、聚合。
結構化 Log 改成輸出 JSON(或其他結構化格式):
{
"timestamp": "2026-03-27T14:02:33Z",
"level": "INFO",
"service": "order-service",
"event": "order_created",
"user_id": 1234,
"order_id": 9999,
"duration_ms": 45
}有了結構化格式,你可以用 log 搜尋平台(例如 Elasticsearch + Kibana,或 Grafana Loki)做到:
- 查所有
user_id=1234的 log - 統計
event=order_created的平均duration_ms - 找出所有
level=ERROR且service=payment-service的紀錄
這就是 log 從「給人看」升級成「給機器分析」的關鍵轉變。
Request ID:給每一筆請求貼上「身分證」
多服務架構裡,一個前端請求可能觸發五個服務的互相呼叫。如果每個服務的 log 只有自己的資訊,沒有把「這筆 log 是由哪個原始請求引起的」記下來,你就沒辦法把散落在五個地方的記錄串在一起。
解法:在請求進來的那一刻,產生一個唯一 ID(Request ID / Trace ID),並在所有後續的 log 都帶上這個 ID。
工程師只需要拿著一個 trace_id,就能把跨越所有服務的 log 全部撈出來,按時間排好,完整看到哪一步出了問題。
分散式追蹤:不只是 ID,還有「時間花在哪裡」
Request ID 能幫你「找到相關 log」,但還有一個問題:時間花在哪一段?
比如結帳請求總共花了 3 秒,你想知道:
- API Gateway → 訂單服務:花了多少?
- 訂單服務 → 支付服務:花了多少?
- 支付服務本身處理:花了多少?
只靠 log 很難算清楚。這就是**分散式追蹤(Distributed Tracing)**的用武之地。
分散式追蹤系統(例如 Jaeger、Zipkin,或雲端的 AWS X-Ray)會幫你把一個請求拆成多個 Span(時間段),每個 Span 記錄「這個服務做了什麼、花了多久」,最後組合成一棵「追蹤樹(Trace Tree)」:
Trace: abc-123(總時間:3024ms)
├── API Gateway (12ms)
├── 訂單服務 (85ms)
│ ├── DB 查詢 (30ms)
│ └── 呼叫支付服務 (2910ms) ← 問題在這
│ ├── 支付 API 內部處理 (2900ms) ← 問題在這
│ └── 更新訂單狀態 (10ms)
└── 通知服務 (15ms)
一眼就看出:支付 API 內部處理花了 2.9 秒,這才是瓶頸。
Log 層級的紀律
Log 不是越多越好。log 太多:
- 磁碟空間燒掉很快
- 搜尋時訊號淹沒在雜訊裡
- 效能有影響(每一行 log 都是 I/O)
標準做法是用**層級(Log Level)**管理:
| 層級 | 用途 | 範例 |
|---|---|---|
| DEBUG | 開發時偵錯,正式環境通常關掉 | 函數進入/離開、變數值 |
| INFO | 正常業務事件 | 訂單建立、用戶登入 |
| WARN | 異常但不致命的情況 | 快取 miss、重試成功 |
| ERROR | 需要關注的錯誤 | 付款失敗、DB 連線逾時 |
| FATAL | 導致服務崩潰的嚴重錯誤 | 啟動時設定錯誤 |
正式環境通常只開 INFO 以上;出問題時臨時開 DEBUG 收集詳細資訊,事後再關掉。
小明的落地做法
1. 統一 Log 格式
所有服務都輸出 JSON,包含:timestamp、level、service、
event、trace_id、user_id(如有)、duration_ms(如有)
2. 在 API Gateway 產生 Trace ID
每個進來的請求,Gateway 產生一個 UUID 作為 trace_id,
透過 HTTP header(X-Trace-ID)傳給下游所有服務
3. 集中 Log 收集
用 Grafana Loki(或 ELK Stack)把所有服務的 log 集中收集,
提供統一的搜尋介面
4. 逐步導入追蹤工具
先靠 trace_id 在 log 裡手動關聯;
等服務數量多到追蹤困難時,再評估接入 Jaeger / Zipkin
小結與預告
這篇小明學到的重點是:
- 結構化 Log:輸出 JSON,讓機器能搜尋、過濾、聚合,而不只是給人閱讀。
- Request ID / Trace ID:在入口產生唯一 ID,所有下游服務都帶上,讓跨服務的 log 可以串在一起。
- 分散式追蹤:不只是「找到相關 log」,還要知道「時間花在哪一段」——Span 樹讓你一眼看出瓶頸。
- Log 層級紀律:太多 log 是噪音,用層級管理,只在需要時開詳細模式。
現在小明的系統有了監控、有了 log、有了追蹤。但每次部署新版本還是很痛苦:「在我電腦上能跑,上到測試環境就掛掉」的問題不斷出現。
下一篇,我們來談解決這個問題的關鍵——容器化(Docker)。