可觀測性 (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」就是從這算出來的
P99 必須用 Histogram,不能用「平均值」

平均延遲會被大量快請求拉低,掩蓋掉尾端慢請求。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 + 告警規則)

告警原則:對症狀告警,不對原因告警

對「原因」告警會製造告警疲勞

「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)

不要用純文字 Log

純文字 "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"
}
每條 Log 都帶上 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,全存成本爆炸。策略:


支柱三:Traces(分散式追蹤)

回答的問題:一個請求在整個系統裡走了哪些路徑?每一步花多久?

微服務裡一個請求可能經過十幾個服務。Metrics 說「P99 上升了」、Logs 說「各服務各自發生了什麼」,但沒法把它們串成一條完整鏈路。Traces 解決的正是這個。

Trace 與 Span

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)
OpenTelemetry (OTel) 是「標準」不是「產品」

OTel 定義統一的 API + 資料格式,讓你的程式碼與後端解耦——後端可自由換 Jaeger / Zipkin / Grafana Tempo / Datadog,不用改埋點。面試提 distributed tracing 直接說 OTel 是安全牌。


三者如何互補:實際排查流程

維度 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 開發速度」的取捨變成清楚的數字決策:

錯誤預算狀態 決策
還充足 可以繼續快速發布新功能
快耗盡 放慢發布節奏,優先修可靠性問題
SLO 絕對不該設成 100%

100% SLO 意味著你永遠不能做任何可能影響穩定性的改動(包括發新功能)。99.9% 給你每月 43 分鐘預算去快速迭代。設太高 → 工程師全花在可靠性、犧牲功能;設太低 → 用戶體驗差。通常從 99.9% 起步,依業務敏感度調整。


面試話術

設計任何生產系統時,主動講「你怎麼知道它運作正常」

不要等被問。畫架構圖的同時就說明維運面——這展現你思考的是「真實上線後」而非「紙上設計」。

一段標準的可觀測性宣告(可直接背):

「這個服務我會暴露 Prometheus metrics,監控四個黃金信號:延遲(P99)、流量(RPS)、錯誤率(5xx 比例)、飽和度(CPU/記憶體)。所有服務的 Log 用結構化 JSON 集中到 ELK(或 Loki),每條帶 trace_id。跨服務請求用 OpenTelemetry 做 distributed tracing 送到 JaegerSLO 設成功率 99.9%、P99 ≤ 500ms,告警對症狀而非對原因。」

常見情境的具體答法:

系統 可觀測性設計
URL Shortener 每次訪問記一個 Counter,Grafana 看趨勢;訪問量遠超平均時告警(可能被病毒式傳播,需擴容)
通知系統 每階段記時間戳,用 Trace 串端到端;P99 端到端 > 30s 告警;分渠道監控發送成功率,任一 < 95% 告警
電商訂單 Gauge 記每分鐘 GMV;短時間異常下跌 > 20% 立即告警——比等用戶回報「無法結帳」快得多

常見 Deep Dive

可觀測性不是「事後加上去」的

埋點(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