cover

Log Management:不再 SSH 進機器看日誌

Metrics 告訴你「什麼地方出了問題」,Logs 告訴你「為什麼出了問題」。當服務部署在多台 Host 上,分散在各處的日誌讓排查變得痛苦——要 SSH 到每台機器、docker logs 每個容器、還要比對時間戳拼湊事件順序。集中式日誌管理把所有日誌收集到一個地方,提供全文搜尋、時間範圍篩選、和跨服務關聯。EFK(Elasticsearch + Fluentd + Kibana)或 ELK(Elasticsearch + Logstash + Kibana)是最常見的選擇。

架構概覽

flowchart LR
  App[應用程式日誌\nstdout / 檔案] -->|收集| Collector[Fluentd / Logstash\n日誌收集器]
  Sys[系統日誌\nsyslog / journal] -->|收集| Collector
  Collector -->|解析 & 轉發| ES[Elasticsearch\n索引與儲存]
  ES -->|搜尋 & 視覺化| Kibana[Kibana\n日誌查詢面板]
  ES -->|生命週期管理| ILM[ILM\n自動清理舊索引]

架構概覽

flowchart LR
  App1[API Service\nstdout/stderr] -->|docker log driver| Fluentd[Fluentd\n收集 + 解析 + 轉發]
  App2[Worker Service\nstdout/stderr] -->|docker log driver| Fluentd
  Nginx[Nginx\naccess/error log] -->|tail| Fluentd
  System[System Logs\nsyslog/journal] -->|input plugin| Fluentd

  Fluentd -->|output| ES[Elasticsearch\n索引 + 儲存\n:9200]
  ES --> Kibana[Kibana\n搜尋 + 視覺化\n:5601]

  Kibana --> DevOps[DevOps\n排查問題]
  ES -->|ILM| Cleanup[Index Lifecycle\n自動清理舊索引]

所有日誌源(容器 stdout、Nginx log、系統 log)由 Fluentd 統一收集、解析後送到 Elasticsearch 建立索引。DevOps 透過 Kibana 搜尋和分析日誌。Elasticsearch 的 ILM(Index Lifecycle Management)自動管理索引的生命週期。

核心概念

  1. 結構化日誌(Structured Logging):日誌應該是機器可讀的,而不只是給人看的。好的日誌格式是 JSON:{"timestamp":"2024-09-15T10:30:00Z","level":"error","service":"api","message":"DB connection failed","error":"connection refused","host":"db-01"}。壞的日誌格式是自由文字:ERROR 2024-09-15 DB connection failed!!!。結構化日誌可以在 Elasticsearch 裡精確搜尋(level:error AND service:api),非結構化日誌只能做全文搜尋,結果不精確。

  2. 日誌收集器(Fluentd vs Logstash):兩者都能收集、解析、轉發日誌。Fluentd 用 Ruby 寫、plugin 生態豐富、記憶體佔用較低(約 40MB),適合資源有限的環境。Logstash 用 Java 寫、功能強大但記憶體佔用高(約 500MB-1GB),適合需要複雜 pipeline 的場景。小型到中型部署建議用 Fluentd(或更輕量的 Fluent Bit,佔用約 15MB)。

  3. Index 設計:Elasticsearch 用 Index 來組織資料。建議按日期建立 Index(例如 logs-api-2024.09.15),搭配 Index Template 統一 mapping 和 settings。這樣做的好處是可以用 ILM 自動刪除過期的 Index(例如保留 30 天),不需要手動清理。如果所有日誌都寫在同一個 Index 裡,刪除舊資料就很麻煩。

  4. 日誌等級與取捨:不是所有日誌都需要保留。DEBUG 等級的日誌在 production 環境產生量極大,會快速填滿 Elasticsearch 的磁碟空間。建議 production 只收 INFO 以上(INFO/WARN/ERROR),DEBUG 只在排查問題時臨時開啟。Fluentd 可以設定 filter 在轉發前過濾掉不需要的日誌。

使用情境

  • 跨服務錯誤排查:使用者回報「訂單建立失敗」。在 Kibana 搜尋 order_id:12345,找到 API 服務的日誌顯示 payment service returned 500,再搜尋 payment 服務同時間的日誌,發現 connection to payment gateway timed out。10 分鐘內定位問題,而不是 SSH 到兩台機器比對日誌。

  • 安全事件調查:發現有異常的 API 呼叫模式(大量 401 錯誤)。在 Kibana 搜尋 status:401,按 client_ip 聚合,找到來自同一個 IP 的大量失敗登入嘗試。確認是暴力破解攻擊後,加入防火牆黑名單。

  • 合規與稽核:某些產業要求保留所有操作日誌一定年限。把關鍵操作(使用者登入/登出、資料修改、權限變更)寫成結構化日誌,在 Elasticsearch 設定 ILM 保留 1 年,搭配快照備份到 MinIO 長期歸檔。

實作範例 / 設定範例

EFK Stack 部署

# docker-compose.logging.yml
version: "3.8"
 
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    ports:
      - "127.0.0.1:9200:9200"
 
  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    restart: unless-stopped
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
 
  fluentd:
    image: fluent/fluentd:v1.16-1
    restart: unless-stopped
    volumes:
      - ./fluentd/fluent.conf:/fluentd/etc/fluent.conf
      - /var/log:/var/log:ro
    ports:
      - "24224:24224"
      - "24224:24224/udp"
    depends_on:
      - elasticsearch
 
volumes:
  es-data:

Fluentd 設定

<!-- fluentd/fluent.conf -->
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>
 
<!-- Docker 容器日誌(透過 fluentd log driver 送進來) -->
<filter docker.**>
  @type parser
  key_name log
  reserve_data true
  <parse>
    @type json
    time_key timestamp
    time_format %Y-%m-%dT%H:%M:%S%z
  </parse>
</filter>
 
<!-- 過濾掉 DEBUG 等級 -->
<filter **>
  @type grep
  <exclude>
    key level
    pattern /^debug$/i
  </exclude>
</filter>
 
<!-- 輸出到 Elasticsearch -->
<match **>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix logs
  logstash_dateformat %Y.%m.%d
  include_tag_key true
  type_name _doc
  <buffer>
    @type file
    path /fluentd/log/buffer
    flush_interval 5s
    chunk_limit_size 8MB
    total_limit_size 512MB
  </buffer>
</match>

Docker 容器對接 Fluentd

# 在應用的 docker-compose.yml 中設定 log driver
services:
  api:
    image: registry.example.com/myapp/api:v1.2.0
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.api"
        fluentd-async: "true"
 
  worker:
    image: registry.example.com/myapp/worker:v1.2.0
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.worker"
        fluentd-async: "true"

Elasticsearch ILM Policy

# 設定 ILM:30 天後刪除
curl -X PUT "localhost:9200/_ilm/policy/logs-cleanup" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "5GB",
            "max_age": "1d"
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}'

建置步驟:從零開始建立 Log Collection

以下是在一台已安裝 Docker + Portainer 的 Host 上,從零開始建立集中式日誌收集的步驟。

Step 1:部署 EFK Stack

把上方的 docker-compose.logging.yml 準備好,在 Portainer 建立一個名為 logging 的 Stack,或直接用 CLI 啟動:

# 建立設定檔目錄
mkdir -p /opt/logging/fluentd
 
# 把 fluent.conf 放到 /opt/logging/fluentd/fluent.conf
# 把 docker-compose.logging.yml 放到 /opt/logging/docker-compose.yml
 
cd /opt/logging
docker compose up -d

等 Elasticsearch 啟動完成(通常需要 30-60 秒),確認健康狀態:

# 確認 Elasticsearch 啟動成功
curl -s http://localhost:9200/_cluster/health | python3 -m json.tool
# status 應為 "green" 或 "yellow"(單節點為 yellow 是正常的)
 
# 確認 Kibana 可以訪問
curl -s -o /dev/null -w "%{http_code}" http://localhost:5601
# 應返回 200 或 302
 
# 確認 Fluentd 正在監聽
docker compose logs fluentd | tail -5

Step 2:把現有容器對接到 Fluentd

修改各服務的 docker-compose.yml,加入 fluentd log driver(參考上方的範例)。或者如果想全域套用,修改 daemon.json

{
  "log-driver": "fluentd",
  "log-opts": {
    "fluentd-address": "localhost:24224",
    "fluentd-async": "true",
    "tag": "docker.{{.Name}}"
  }
}

注意:全域設定 fluentd log driver 後,如果 Fluentd 掛了,所有容器的日誌都會受影響。建議先在個別服務的 docker-compose 裡設定,確認穩定後再考慮全域套用。

Step 3:設定 Kibana Index Pattern

  1. 打開 Kibana(http://your-host:5601
  2. 進入 Stack Management → Index Patterns
  3. 建立 Index Pattern:logs-*
  4. 選擇 @timestamp 作為 Time field
  5. 進入 Discover 頁面,應該能看到各容器送過來的日誌

Step 4:建立 Log → Monitoring 的連動

日誌收集到 Elasticsearch 後,要和 Prometheus 監控 串連:

# 在 alert-rules.yml 加入日誌相關的告警
groups:
  - name: logging
    rules:
      # Fluentd 掛了就收不到日誌
      - alert: FluentdDown
        expr: up{job="fluentd"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Fluentd is down - log collection stopped"
 
      # Elasticsearch 磁碟使用率
      - alert: ElasticsearchDiskHigh
        expr: elasticsearch_filesystem_data_available_bytes / elasticsearch_filesystem_data_size_bytes < 0.2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Elasticsearch disk usage > 80%"

在 Prometheus 的 scrape_configs 加入 Fluentd 和 Elasticsearch 的 exporter:

  # Fluentd 指標(需要安裝 fluent-plugin-prometheus)
  - job_name: 'fluentd'
    static_configs:
      - targets: ['fluentd:24231']
 
  # Elasticsearch 指標
  - job_name: 'elasticsearch'
    static_configs:
      - targets: ['elasticsearch-exporter:9114']

Step 5:設定 ILM 自動清理

執行上方的 ILM Policy 設定指令,確保舊的日誌 Index 會被自動清理,不會把磁碟塞滿。

驗證清單

完成以上步驟後,用這個清單確認一切正常:

- [ ] Elasticsearch cluster health 為 green/yellow
- [ ] Kibana 可以正常訪問
- [ ] Fluentd 收到容器日誌(Kibana Discover 有資料)
- [ ] 日誌格式為 JSON(結構化日誌)
- [ ] ILM Policy 已設定(日誌不會無限增長)
- [ ] Prometheus 有監控 Fluentd 和 Elasticsearch
- [ ] 告警規則已設定(Fluentd down / ES disk high)

常見問題與風險

  • Elasticsearch 記憶體不足:ES 的 JVM heap 設太小,搜尋大量日誌時 OOM。反之設太大,OS cache 不夠用,效能也差。避免方式:JVM heap 設為 RAM 的 50%(但不超過 32GB),剩下留給 OS cache。單節點建議至少 4GB RAM(heap 2GB)。

  • 日誌量暴增:某個服務進入錯誤迴圈,每秒產生上千行 error log,Elasticsearch 磁碟迅速被塞滿。避免方式:在 Fluentd 設定 rate limiting、在應用端設定 log sampling(每 100 次相同錯誤只記錄 1 次)、設定 ILM 自動清理。

  • Fluentd 掛了日誌丟失:如果 Fluentd 掛了,Docker 的 fluentd log driver 會 buffer 一段時間,超過 buffer 就丟棄。避免方式:設定 fluentd-async: true 讓容器不因 Fluentd 掛掉而阻塞。Fluentd 用 file buffer 持久化,重啟後繼續送。

  • 搜尋太慢:Index 沒有做好 mapping,所有欄位都是 text 類型(支援全文搜尋但佔空間),導致搜尋效能差。避免方式:用 Index Template 定義明確的 mapping,keyword 欄位用 keyword 類型,時間用 date 類型,只有真正需要全文搜尋的欄位用 text

優點

  • 集中管理所有日誌,排查問題不用 SSH 到各台機器
  • Kibana 提供強大的搜尋和視覺化能力
  • ILM 自動管理日誌保留期限

缺點 / 限制

  • EFK/ELK Stack 資源消耗大(建議至少 8GB RAM)
  • Elasticsearch 的運維複雜度高,cluster 管理有一定門檻
  • 小團隊可以考慮 Loki(輕量替代方案,但搜尋能力不如 ES)

延伸閱讀