cover

Metrics & Monitoring:用數據取代猜測

服務跑得好不好,不能等使用者回報才知道。Metrics(指標)是一組時間序列的數值,描述系統在每個時間點的狀態——CPU 使用率、記憶體、請求數、錯誤率、響應時間。Prometheus 負責收集和儲存這些指標,Grafana 負責視覺化。這兩者搭配起來,就是可觀測性的第一根支柱(metrics、logs、traces 三根支柱中最基礎的)。

架構概覽

flowchart LR
  App[應用程式\n/metrics] -->|scrape| Prom[Prometheus\n指標收集與儲存]
  Exp[Exporters\nNode / DB / Nginx] -->|scrape| Prom
  Prom -->|查詢| Grafana[Grafana\n儀表板視覺化]
  Prom -->|規則評估| AM[Alertmanager\n告警通知]
  AM -->|通知| Notify[Slack / Email]
  Grafana --> Dashboard[Dashboard\n即時監控面板]

架構概覽

flowchart TD
  App[Application\n/metrics endpoint] -->|scrape| Prometheus[Prometheus\n:9090]
  NodeExp[Node Exporter\nHost 層級指標] -->|scrape| Prometheus
  PgExp[pg_exporter\nPostgreSQL 指標] -->|scrape| Prometheus
  NginxExp[nginx_exporter\nNginx 指標] -->|scrape| Prometheus
  CAdvisor[cAdvisor\nContainer 指標] -->|scrape| Prometheus

  Prometheus -->|query| Grafana[Grafana\n:3000]
  Prometheus -->|evaluate| AlertManager[Alertmanager\n:9093]
  AlertManager -->|notify| Slack[Slack / Discord]
  AlertManager -->|notify| Email[Email]

  Grafana --> Dashboard[Dashboard\nHost / App / DB]

Prometheus 定期 scrape(拉取)各個 exporter 和應用服務的 /metrics endpoint。Grafana 連接 Prometheus 作為 data source 來建立 dashboard。Alertmanager 處理告警通知。

核心概念

  1. Pull vs Push 模型:Prometheus 採用 Pull 模型——Prometheus 主動去各個 target 抓取指標,而不是由 target 推送到 Prometheus。好處是 Prometheus 可以知道 target 是否存活(scrape 失敗 = target 可能掛了),也不需要在 target 端設定 Prometheus 的位址。缺點是短暫存活的 job(例如 batch script)來不及被 scrape,需要用 Pushgateway 來處理。

  2. Exporter 生態:Prometheus 不直接監控服務,而是透過 Exporter 轉換各種服務的指標為 Prometheus 格式。常用的 Exporter:node_exporter(Host 的 CPU/Memory/Disk/Network)、cadvisorDocker 容器的資源使用)、postgres_exporterPostgreSQL 的連線數/查詢數/cache hit ratio)、nginx-prometheus-exporterNginx 的請求數/連線數/upstream 狀態)。只要裝上對應的 Exporter,就能在 Grafana 看到指標,不需要修改應用程式碼。

  3. PromQL 查詢語言:Prometheus 用 PromQL 來查詢和聚合指標。常用的函數:rate()(計算增長率,適合 counter 類型的指標)、increase()(一段時間的增量)、histogram_quantile()(計算百分位數,例如 p99 響應時間)。PromQL 是建立 dashboard 和告警規則的基礎,值得花時間學會基本用法。

  4. Dashboard 設計原則:不要把所有指標都塞在一個 dashboard 裡。建議分層:Overview dashboard(整體健康狀態,一眼看出有沒有問題)、Service dashboard(單一服務的詳細指標)、Debug dashboard(排查特定問題用的深入指標)。每個 panel 要有明確的標題和單位,不要讓看 dashboard 的人還要猜這個數字代表什麼。

使用情境

  • 日常巡檢:每天早上打開 Grafana Overview dashboard,看 Host 的 CPU/Memory/Disk 是否正常、各服務的 error rate 是否為零、資料庫的連線數和 cache hit ratio 是否在合理範圍。10 秒鐘就能知道系統狀態。

  • 效能排查:使用者反映網頁變慢。打開 Grafana 看 API 的 p99 響應時間從 200ms 升到 3000ms、PostgreSQL 的 active connections 從 30 飆到 95(接近 max_connections)、cache hit ratio 從 99% 降到 80%。問題定位:連線數太多導致查詢排隊。解法:調整 PgBouncer pool size。

  • 容量規劃:每個月 review 一次 disk usage 趨勢。如果過去 3 個月磁碟使用量每月增長 10GB,現在剩餘 50GB,代表大約 5 個月後要擴容或清理。有了趨勢數據,就可以提前規劃,而不是等到磁碟滿了才緊急處理。

實作範例 / 設定範例

Prometheus + Grafana + Exporters 部署

# docker-compose.monitoring.yml
version: "3.8"
 
services:
  prometheus:
    image: prom/prometheus:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./alert-rules.yml:/etc/prometheus/alert-rules.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--storage.tsdb.retention.size=10GB'
 
  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
      GF_USERS_ALLOW_SIGN_UP: "false"
    volumes:
      - grafana-data:/var/lib/grafana
 
  node-exporter:
    image: prom/node-exporter:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
 
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
 
volumes:
  prometheus-data:
  grafana-data:

prometheus.yml 設定

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s
 
rule_files:
  - "alert-rules.yml"
 
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']
 
scrape_configs:
  # Prometheus 自身指標
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
 
  # Host 指標
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']
 
  # Container 指標
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']
 
  # PostgreSQL 指標
  - job_name: 'postgresql'
    static_configs:
      - targets: ['postgres-exporter:9187']
 
  # Nginx 指標
  - job_name: 'nginx'
    static_configs:
      - targets: ['nginx-exporter:9113']
 
  # Application 指標(如果應用有 /metrics endpoint)
  - job_name: 'api'
    metrics_path: /metrics
    static_configs:
      - targets: ['api:8000']

告警規則範例

# alert-rules.yml
groups:
  - name: host
    rules:
      - alert: HighCPU
        expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU usage > 85% for 5 minutes"
 
      - alert: DiskSpaceLow
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 20
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disk space < 20% on {{ $labels.instance }}"
 
      - alert: HighMemory
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Memory usage > 90%"
 
  - name: postgresql
    rules:
      - alert: PostgreSQLDown
        expr: pg_up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "PostgreSQL is down"
 
      - alert: PostgreSQLHighConnections
        expr: sum(pg_stat_activity_count) > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "PostgreSQL connections > 80"

Metrics + Logs:完整的系統監控

Metrics 和 Logs 是系統監控的兩根支柱,各有所長:

面向Metrics(Prometheus)Logs(EFK)
回答的問題「發生了什麼?嚴重嗎?」「為什麼發生?細節是什麼?」
資料形式數值(CPU 85%、QPS 1200)文字(error trace、request body)
適合場景即時告警、趨勢分析、容量規劃錯誤排查、安全稽核、事件回溯
保留成本低(時間序列壓縮效率高)高(全文索引佔空間)

典型的監控排查流程:

Grafana 看到 API error rate 飆高(Metrics)
→ 確認是哪個 endpoint、哪個時間段
→ 切到 Kibana 搜尋同時段的 error log(Logs)
→ 找到具體的 error message 和 stack trace
→ 定位問題根因並修復

建議的建置順序:先把 Prometheus + Grafana 架好(本篇),確認 Metrics 收集正常;再架 EFK Stack,把日誌也集中收集;最後設定 Alertmanager 把告警串到 Slack/Discord。三者合起來就是完整的系統監控方案。

常見問題與風險

  • Prometheus 磁碟空間耗盡:Prometheus 的 TSDB 隨時間增長,如果 scrape 的 target 很多、指標很密,資料量增長很快。Prometheus 掛了就沒有監控,變成「監控系統本身沒有被監控」。避免方式:設定 --storage.tsdb.retention.time=30d--storage.tsdb.retention.size=10GB 限制儲存上限。

  • Scrape target 掛了沒發現:Prometheus 配了 scrape job,但 target 的 exporter 沒裝好或掛了,Prometheus 只會顯示 up == 0,如果沒有對應的告警規則,就不會有人知道。避免方式:為每個 job 設定 up == 0 的告警規則。

  • Dashboard 太多太雜:每個人都建自己的 dashboard,最後 Grafana 裡有 50 個 dashboard 但沒有人維護。避免方式:建立 dashboard 命名規範和分層結構、定期清理不再使用的 dashboard、用 provisioning 把核心 dashboard 寫成 JSON 檔案進 Git。

  • 告警疲勞:告警規則設得太敏感(例如 CPU > 50% 就告警),導致每天收到幾十封告警通知,團隊開始忽略所有告警。避免方式:告警閾值要合理(Warning 85%、Critical 95%)、設定 for 持續時間避免瞬間尖峰觸發告警、分級處理(Critical 立即通知、Warning 只記錄)。

優點

  • Pull 模型讓 target 不需要知道 Prometheus 的存在,部署簡單
  • Exporter 生態豐富,幾乎所有常見服務都有現成的 Exporter
  • PromQL 強大且靈活,能做複雜的聚合和計算

缺點 / 限制

  • Prometheus 單節點有容量上限,超大規模需要 Thanos 或 Cortex
  • Pull 模型不適合短暫存活的 job(需要 Pushgateway)
  • Grafana 的 dashboard JSON 不太好 review 和版本控管

延伸閱讀