可觀測性 (Observability)
可觀測性 = 從系統的外部輸出(數字、文字、追蹤)推斷它的內部狀態。
它不防止故障,而是確保故障發生時你能快速看見、快速定位、快速確診。三支柱 Metrics / Logs / Traces 不是同一件事的三種說法,而是回答三個不同問題、互相補完的工具。面試時的殺手鐧不是「我會加監控」,而是「我設計了哪些指標衡量健康、出事時用什麼順序排查」。
為什麼需要可觀測性
「你不能修復你看不見的東西。」
系統上線後真正的挑戰才開始。某個週五晚上用戶回報「結帳失敗」,你打開 dashboard 只看到某服務回應時間從 200ms 跳到 8 秒 —— 是 DB 慢了?下游 API 掛了?新部署有 bug?流量暴增?你不知道,只能在黑暗中摸索。
可觀測性就是把這種「黑暗摸索」變成「有依據的推斷」。它建立在三個支柱上:
┌──────────── Observability ────────────┐
│ │
Metrics(指標) Logs(日誌) Traces(追蹤)
系統狀態如何? 這事件發生了什麼? 這請求走了哪條路?
數值 + 時序 文字事件 DAG(跨服務鏈路)
│ │ │
告警/趨勢 debug 確診 找跨服務瓶頸
同一個故障,Metrics 告訴你「有問題」,Traces 告訴你「問題在哪個服務的哪一步」,Logs 告訴你「那一步具體錯了什麼」。缺一個就會在某個環節卡住。
支柱一:Metrics(指標)
回答的問題:系統現在的狀態如何?趨勢是什麼?
Metrics 是數值型時序資料——每隔一段時間(例如 15 秒)記錄一個數值,長期形成曲線。你可以在上面做聚合、算百分位數、設閾值觸發告警。
三種 Metric 類型
| 類型 | 性質 | 重點 | 範例 |
|---|---|---|---|
| Counter 計數器 | 只會單調遞增 | 本身沒意義,增長速率才有意義(每秒請求數) | HTTP 請求總數、錯誤總數、發送訊息數 |
| Gauge 儀表 | 可上下浮動 | 代表某時間點的即時狀態 | 記憶體用量、連線池使用數、佇列深度 |
| Histogram 直方圖 | 把數值分桶統計分佈 | 算百分位數的基礎(P50/P99) | 延遲分佈,「P99 = 450ms」就是從這算出來的 |
平均延遲會被大量快請求拉低,掩蓋掉尾端慢請求。Histogram 把延遲丟進桶裡,取第 99 百分位的那個值——這才是反映「最差體驗」的真實數字。詳見 02-Distributed-Systems/05-Numbers-to-Know 對延遲數量級的整理。
你該監控哪些:四個黃金信號(Google SRE)
任何服務都該監控的最基本指標:
| 信號 | 問題 | 細節要點 |
|---|---|---|
| Latency 延遲 | 請求要多久? | 要分開成功 vs 失敗——快速失敗 ≠ 緩慢成功,意義完全不同 |
| Traffic 流量 | 每秒多少請求? | 衡量系統負載的基準(RPS) |
| Errors 錯誤率 | 失敗比例多少? | 區分顯性(HTTP 500)與隱性(回 200 但內容錯) |
| Saturation 飽和度 | 還剩多少餘裕? | CPU / 記憶體 / 磁碟,越接近上限系統越脆弱(接 12-Ops-and-Reliability/02-Overload-Protection) |
Prometheus + Grafana 的標準組合
各服務暴露 /metrics 端點
│ (Pull 模式,每 15s 拉一次)
▼
Prometheus(時序資料庫 TSDB)
│
▼
Grafana(視覺化 dashboard + 告警規則)
- Prometheus:最主流的開源 Metrics 系統,Pull 模式定期從各服務
/metrics拉資料存進 TSDB。 - Grafana:在 Prometheus 資料上做視覺化儀表板與告警。
Pull(Prometheus 主動拉):中心端掌握「該有哪些 target」,target 沒回應本身就是一個訊號(知道它掛了);易於本地測試(直接 curl /metrics)。
Push(服務主動推,如 StatsD):適合短命任務(batch job、Lambda 跑完就消失,來不及被拉)。Prometheus 用 Pushgateway 補這個缺口。
結論:長壽服務用 Pull,短命任務用 Push,兩者常並存。
告警原則:對症狀告警,不對原因告警
「CPU > 80% 就告警」「記憶體 > 70% 就告警」——但 CPU 高不一定代表用戶有感。這種告警太多 → alert fatigue → 工程師開始無視告警 → 真正的事故被淹沒。
正確做法是問「用戶有沒有感受到問題?」:
# ❌ 不好:對原因告警(CPU 高不代表用戶有問題)
- alert: HighCPU
expr: cpu_usage > 0.8
# ✅ 好:對症狀告警(用戶正在受影響)
- alert: HighErrorRate
expr: rate(http_requests_total{status_code=~"5.."}[5m]) > 0.01
annotations:
summary: "錯誤率超過 1%,用戶正在受到影響"
- alert: HighLatency
expr: histogram_quantile(0.99, http_request_duration_seconds) > 1.0
annotations:
summary: "P99 延遲超過 1 秒"
支柱二:Logs(日誌)
回答的問題:某個特定事件到底發生了什麼?
當告警觸發,你知道「錯誤率上升」,但不知道為什麼。這時需要 Logs——每個事件的詳細文字記錄,讓你追到具體失敗原因。
結構化日誌(Structured Logging)
純文字 "User 123 created order 456 for $100" 很難用程式解析、過濾、聚合。改用 JSON 結構化日誌,每個欄位都能查詢。
// logger.info("order_created", user_id=123, order_id=456, amount=100, duration_ms=45)
{
"event": "order_created",
"user_id": 123,
"order_id": 456,
"amount": 100,
"payment_method": "credit_card",
"duration_ms": 45,
"timestamp": "2024-01-15T10:23:45Z",
"service": "order-service",
"trace_id": "abc-123", // 串起 Trace 的關鍵欄位
"level": "info"
}
trace_id / correlation_id
這是讓 Logs 與 Traces 互通的橋樑:在 Trace 上找到慢請求 → 拿它的 trace_id → 直接撈出該請求在所有服務的 Log。沒這個欄位,三支柱就斷掉了。
Log Level:用嚴重程度快速過濾
| Level | 含義 | 範例 |
|---|---|---|
| DEBUG | 開發詳細資訊 | 生產環境通常關閉 |
| INFO | 正常業務事件 | 訂單建立、用戶登入 |
| WARN | 值得注意但不緊急 | 重試成功、快取 miss 率偏高 |
| ERROR | 某操作失敗,但系統還在運作 | 單筆寫入失敗 |
| FATAL | 系統無法繼續,需立即處理 | 啟動時連不上必要依賴 |
集中式日誌(Centralized Logging)
Log 分散在各台機器 → 出事要一台台 SSH 進去查,效率極差(在 Serverless / 容器環境根本沒法 SSH)。集中式把所有服務的 Log 匯到同一處。
| 方案 | 組成 | 特性 |
|---|---|---|
| ELK Stack | Elasticsearch(存+搜)+ Logstash/Filebeat(收集)+ Kibana(查詢介面) | 功能完整、全文索引,但成本高(見 08-Search-and-AI/03-Elasticsearch) |
| Grafana Loki | 更輕量,只對標籤建索引、不對 Log 內文建全文索引 | 儲存成本低,與 Grafana 整合好 |
取樣(Sampling):高流量服務的成本控制
每秒幾十萬條 Log,全存成本爆炸。策略:
- INFO 取樣:只保留 10–20%。
- ERROR / WARN 永遠 100% 保留——出事時不能漏掉關鍵線索。
支柱三:Traces(分散式追蹤)
回答的問題:一個請求在整個系統裡走了哪些路徑?每一步花多久?
微服務裡一個請求可能經過十幾個服務。Metrics 說「P99 上升了」、Logs 說「各服務各自發生了什麼」,但沒法把它們串成一條完整鏈路。Traces 解決的正是這個。
Trace 與 Span
- Trace:一個請求的完整生命週期。
- Span:一個操作單元(呼叫一個服務、跑一次 DB 查詢、發一個 HTTP 請求)。一個 Trace 由多個 Span 組成一棵樹(DAG)。
Trace ID: abc-123 總耗時 250ms
│
├── Span: API Gateway [0ms ──────────────── 250ms]
│ ├── Span: Auth Service [5ms ─ 15ms]
│ └── Span: Order Service [20ms ───────── 240ms]
│ ├── Span: DB Query SELECT [25ms ──── 180ms] ← 瓶頸!佔 155ms
│ └── Span: Notification Svc [185ms ─ 200ms]
一眼看出:整個請求 250ms,其中 DB 查詢就佔 155ms——這就是要優化的地方。
Trace Context 的傳遞
要讓 Trace 跨服務工作,每個請求都要帶著 Trace ID 往下傳:服務 A 呼叫 B 時在 HTTP header 附上 Trace ID,B 看到後在同一個 Trace 下建立新 Span。
# OpenTelemetry:現在的業界標準
context = extract(request.headers) # 從上游 header 取出 trace context
with tracer.start_as_current_span("create_order", context=context) as span:
span.set_attribute("user_id", request.user_id)
headers = {}
inject(headers) # 呼叫下游時自動把 context 注入 header
response = downstream_service.call(headers=headers)
OTel 定義統一的 API + 資料格式,讓你的程式碼與後端解耦——後端可自由換 Jaeger / Zipkin / Grafana Tempo / Datadog,不用改埋點。面試提 distributed tracing 直接說 OTel 是安全牌。
Trace 資料量大,通常只追蹤一部分請求。兩種取樣時機:
Head-based sampling(請求一進來就決定追不追,如 1%):簡單、可預測成本,但可能漏掉那筆出錯的請求。
Tail-based sampling(請求跑完才決定):可以「只保留出錯或特別慢的 trace」,資訊價值高,但要先暫存所有 span,架構較複雜。生產系統常用 tail-based 保住有問題的鏈路。
三者如何互補:實際排查流程
| 維度 | Metrics | Logs | Traces |
|---|---|---|---|
| 回答的問題 | 系統狀態怎樣? | 這事件發生了什麼? | 這請求走了什麼路徑? |
| 資料形式 | 數值 + 時序 | 文字事件 | 有向無環圖(DAG) |
| 適合做 | 告警、趨勢分析 | debug 特定錯誤 | 找跨服務的延遲瓶頸 |
| 儲存成本 | 低 | 高 | 中 |
| 查詢速度 | 快 | 較慢 | 中 |
實際排查永遠是串起來用的:
1. Metrics 告警 "P99 延遲從 200ms 升到 2 秒"
↓
2. 查 Traces 定位 "這些慢請求都卡在 Order Service 的 DB Query"
↓
3. 查 Logs 確診 "Order Service 大量 'slow query: 1.8s' 的 warning log"
↓
結論:有個 SQL 查詢沒用到索引(見 04-Database-Indexing/01-Indexing-Overview)
口訣: Metrics 告警 → Traces 定位 → Logs 確診
SLI / SLO / SLA:把可靠性量化
可觀測性的最終目的是衡量並保證可靠性。三個常被混淆的概念:
| 概念 | 是什麼 | 範例 | 給誰看 |
|---|---|---|---|
| SLI(Service Level Indicator) | 衡量品質的具體指標(一個 Metric) | 「成功請求比例」「P99 延遲」 | 工程內部 |
| SLO(Service Level Objective) | 你對 SLI 設的目標值(自我承諾) | 「成功率 ≥ 99.9%」「P99 ≤ 500ms」 | 工程內部 |
| SLA(Service Level Agreement) | 對外部客戶的合約,通常比 SLO 寬鬆 | 對客戶承諾 99.5%(留緩衝) | 客戶 / 法務 |
SLI = 0.9995 > SLO = 99.9% > SLA = 99.5%
(實際表現) (內部目標) (對外承諾,留緩衝)
→ 一切正常
SLA 比 SLO 寬鬆是刻意的:內部 SLO 99.9% 先觸發預警,讓你在違反對外 SLA 99.5% 之前就有時間修。
Error Budget(錯誤預算)
SLO 給了你一個可以「消耗」的預算。SLO = 99.9% → 一個月允許停機:
0.1% × 30天 × 24小時 × 60分鐘 ≈ 43 分鐘 / 月
這把「可靠性 vs 開發速度」的取捨變成清楚的數字決策:
| 錯誤預算狀態 | 決策 |
|---|---|
| 還充足 | 可以繼續快速發布新功能 |
| 快耗盡 | 放慢發布節奏,優先修可靠性問題 |
100% SLO 意味著你永遠不能做任何可能影響穩定性的改動(包括發新功能)。99.9% 給你每月 43 分鐘預算去快速迭代。設太高 → 工程師全花在可靠性、犧牲功能;設太低 → 用戶體驗差。通常從 99.9% 起步,依業務敏感度調整。
面試話術
不要等被問。畫架構圖的同時就說明維運面——這展現你思考的是「真實上線後」而非「紙上設計」。
一段標準的可觀測性宣告(可直接背):
「這個服務我會暴露 Prometheus metrics,監控四個黃金信號:延遲(P99)、流量(RPS)、錯誤率(5xx 比例)、飽和度(CPU/記憶體)。所有服務的 Log 用結構化 JSON 集中到 ELK(或 Loki),每條帶
trace_id。跨服務請求用 OpenTelemetry 做 distributed tracing 送到 Jaeger。SLO 設成功率 99.9%、P99 ≤ 500ms,告警對症狀而非對原因。」
常見情境的具體答法:
| 系統 | 可觀測性設計 |
|---|---|
| URL Shortener | 每次訪問記一個 Counter,Grafana 看趨勢;訪問量遠超平均時告警(可能被病毒式傳播,需擴容) |
| 通知系統 | 每階段記時間戳,用 Trace 串端到端;P99 端到端 > 30s 告警;分渠道監控發送成功率,任一 < 95% 告警 |
| 電商訂單 | 用 Gauge 記每分鐘 GMV;短時間異常下跌 > 20% 立即告警——比等用戶回報「無法結帳」快得多 |
常見 Deep Dive
三者回答不同問題,使用有順序。Metrics 是告警的基礎,第一時間讓你知道「有問題」,但不告訴你為什麼——它是觀察系統狀態的儀表板。問題被發現後,Traces 幫你縮小範圍:哪個服務、哪個操作慢。找到瓶頸後,Logs 給細節:那個操作具體出了什麼錯。完整流程:Metrics 告警 → Traces 定位 → Logs 確診。
兩個原則。(1) 從用戶角度出發,不是從系統能力:先問「用戶能接受的最差體驗是什麼」,翻譯成可測量的 SLI,再設稍嚴格一點的 SLO,讓團隊在違反 SLA 前就有預警。(2) SLO 不該是 100%:100% 意味著永遠不能做任何改動。99.9% 給每月 43 分鐘錯誤預算去快速迭代。通常從 99.9% 起步,依業務敏感度調整。
四招並用:① 取樣——INFO 只留 10–20%,ERROR/WARN 留 100%;② 動態 Log level——平時只開 WARN 以上,出問題時把某服務動態調成 DEBUG,查完調回;③ 設 TTL——Log 不必永久保留,30–90 天通常夠,舊的自動刪;④ 結構化 + 選擇性索引——只對 service/user_id/trace_id/level 等關鍵欄位建索引,不對全文建索引,查詢快又省成本(Loki 就是這思路)。
純看 HTTP 狀態碼會漏。要靠業務語意指標:例如電商用 GMV/訂單成功率這種業務 Gauge,當「技術上沒報錯但業務數字異常下跌」時告警;通知系統監控「實際送達率」而非「API 回 200 率」。本質是:對用戶真正在意的結果建 metric,而不只是對 HTTP 層建 metric。
Metrics 的每個 label 值組合都是一條獨立時序。把 user_id、request_id 這種高基數值塞進 Prometheus label,會讓時序數量爆炸、TSDB 記憶體炸掉。原則:高基數資訊放 Logs/Traces(它們本來就逐事件記錄),Metrics 的 label 只放低基數維度(method、endpoint、status_code、region)。
埋點(instrument)必須在開發時就一起做:metric 的命名規範、trace context 的傳遞、log 的結構化欄位,全是設計的一部分。等出事才想加,往往因為缺 trace_id、缺關鍵 metric 而排查無門。
自我測驗重點
| 問題 | 重點 |
|---|---|
| 三支柱各回答什麼問題 | Metrics=狀態 / Logs=事件細節 / Traces=跨服務路徑 |
| 排查順序 | Metrics 告警 → Traces 定位 → Logs 確診 |
| 三種 Metric 類型 | Counter(遞增)/ Gauge(浮動)/ Histogram(分佈、算 P99) |
| 四個黃金信號 | Latency / Traffic / Errors / Saturation |
| 告警原則 | 對症狀告警(用戶有感),不對原因(CPU 高)→ 避免告警疲勞 |
| 結構化日誌 | JSON 而非純文字、帶 trace_id、ERROR 永遠 100% 保留 |
| Trace vs Span | Trace=整個請求生命週期;Span=一個操作單元;OTel 為標準 |
| SLI vs SLO vs SLA | 指標 / 內部目標 / 對外合約;SLA < SLO 留緩衝 |
| Error Budget | 99.9% → 每月約 43 分鐘;預算決定發布快慢 |
| 高基數標籤 | 別把 user_id 放 Metrics label,放 Logs/Traces |
Related Notes
- 01-Networking/07-Fault-Handling — 故障處理(重試、超時、熔斷)需要可觀測性來偵測與驗證,兩者一體兩面
- 12-Ops-and-Reliability/02-Overload-Protection — 飽和度(Saturation)信號是觸發 load shedding / 限流的依據
- 12-Ops-and-Reliability/03-Reliable-Delivery — DLQ 深度、重試次數都是要監控的 metric;trace_id 串起整條投遞鏈路
- 12-Ops-and-Reliability/01-Dealing-With-Contention — 鎖等待、慢查詢是延遲飆高的常見根因,靠 Traces + Logs 定位
- 02-Distributed-Systems/05-Numbers-to-Know — 延遲數量級是設定 SLO 與判讀 P99 的基準
- 08-Search-and-AI/03-Elasticsearch — ELK Stack 的 E,集中式日誌的儲存與搜尋引擎
- 09-Messaging-Coord/01-Data-Pipeline — 可觀測性資料本身常透過 pipeline 收集;管線每階段的時延也要監控
- 11-Infrastructure/04-Serverless — 函式短暫無法 SSH,更依賴結構化日誌 + distributed tracing
- 04-Database-Indexing/01-Indexing-Overview — 「慢查詢沒用到索引」是 Logs/Traces 排查最常見的結論