
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)自動管理索引的生命週期。
核心概念
-
結構化日誌(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),非結構化日誌只能做全文搜尋,結果不精確。 -
日誌收集器(Fluentd vs Logstash):兩者都能收集、解析、轉發日誌。Fluentd 用 Ruby 寫、plugin 生態豐富、記憶體佔用較低(約 40MB),適合資源有限的環境。Logstash 用 Java 寫、功能強大但記憶體佔用高(約 500MB-1GB),適合需要複雜 pipeline 的場景。小型到中型部署建議用 Fluentd(或更輕量的 Fluent Bit,佔用約 15MB)。
-
Index 設計:Elasticsearch 用 Index 來組織資料。建議按日期建立 Index(例如
logs-api-2024.09.15),搭配 Index Template 統一 mapping 和 settings。這樣做的好處是可以用 ILM 自動刪除過期的 Index(例如保留 30 天),不需要手動清理。如果所有日誌都寫在同一個 Index 裡,刪除舊資料就很麻煩。 -
日誌等級與取捨:不是所有日誌都需要保留。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 -5Step 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
- 打開 Kibana(
http://your-host:5601) - 進入 Stack Management → Index Patterns
- 建立 Index Pattern:
logs-* - 選擇
@timestamp作為 Time field - 進入 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)