← 返回上一頁
Kubernetes 教學文章

Kubernetes 容器日誌最佳實踐:別再 tail -f 硬查,搞懂 kubelet / kubectl logs / 集中式收集架構

最後更新:2026-04-30
本頁目錄

很多剛從傳統 VM 環境轉到 Kubernetes 的 SRE,第一個會延續下來的習慣就是:kubectl exec -it 進容器,然後 tail -f /var/log/app.log。在十幾個 Pod 的 dev 環境這樣搞還行,但 cluster 一旦長到一兩百個節點、每天上千次發版、Pod 隨時被驅逐重排,這套流程就會崩。

崩的點通常不是「指令不會打」,而是心智模型錯誤。容器日誌不是寫在容器裡某個固定路徑的「檔案」,它是 stdout/stderr 經由 runtime 寫到節點的一段 stream,會被 kubelet 輪轉、會被排程器隨 Pod 一起搬走,更重要的是——容器一重啟,原本進去 tail 的那個 shell session 就斷了,但日誌可能根本沒丟,只是搬到了另一個檔案路徑。

這篇把我這幾年在 production 上踩過的坑整理成一份操作筆記。從節點上日誌的實體儲存、kubelet/containerd-shim 的角色,到 kubectl logs 的所有實用 flag、無法走 API Server 時的節點層救援、再到生產等級的 Fluent Bit / Loki 集中收集架構。內容基於 containerd 1.7.x 與 Kubernetes 1.29,所有指令都實際在環境上跑過。

一、容器日誌的底層機制

要理解為什麼不該 exec 進容器看 log,得先知道容器日誌實際寫在哪、由誰寫、誰負責輪轉。這條鏈路看似簡單,但出問題時不知道每一段在哪裡,就只能瞎猜。

1.1 日誌檔在節點上的位置

containerd 環境下,所有 Pod 的 stdout/stderr 都會被寫到節點上的固定路徑:

/var/log/pods/<namespace>_<pod_name>_<pod_uid>/<container_name>/<restart_count>.log

實際操作上:

# 看節點上目前有哪些 Pod 的 log
ls -la /var/log/pods/

# 看某個 Pod 的所有 container 目錄
ls -la /var/log/pods/default_nginx-deployment-6d9f4c8b7-x2m9n_abc123/

# 看某個 container 的 log 檔
ls -la /var/log/pods/default_nginx-deployment-6d9f4c8b7-x2m9n_abc123/nginx/

# 直接 cat 檔案內容
cat /var/log/pods/default_nginx-deployment-6d9f4c8b7-x2m9n_abc123/nginx/0.log

檔名規則是 <restart_count>.log,從 0.log 開始,container 每重啟一次就會生出新的 1.log2.logkubectl logs --previous 看到的就是上一個編號的檔案。

另外還有 /var/log/containers/ 這個目錄,裡面是上面那堆 log 的 symlink,命名扁平一點,方便 log collector 用 glob 抓:

ls -la /var/log/containers/ | head -5
# nginx-deployment-xxx_default_nginx-abc.log -> /var/log/pods/default_nginx-.../nginx/0.log

Fluent Bit、Promtail 這類 collector 預設都是吃 /var/log/containers/*.log,原因就是這個 symlink 命名直接含了 namespace、pod、container,解析起來很乾淨。

1.2 kubelet 怎麼處理 stdout/stderr

kubelet 不會自己 read container stdout,它委託給 CRI runtime(containerd / cri-o)。流程是:

  1. container 主進程 write(1, ...) 寫到 stdout
  2. containerd 透過 shim 抓取這個 fd
  3. 寫進節點上的 0.log,CRI 規範用的是 JSON Lines 格式,每行帶 timestamp、stream(stdout/stderr)、原始訊息
  4. kubelet 監控檔案大小,達閾值就觸發輪轉

這裡有個關鍵:只有 stdout/stderr 才會被收。如果應用程式自己寫到 /var/log/app.log,那只有寫進 container rootfs,宿主上根本看不到,kubectl logs 也撈不到。所以容器化的 12-factor 原則第一條就是 logs 要當 event stream 寫到 stdout。

kubelet 的輪轉設定看 /var/lib/kubelet/config.yaml

cat /var/lib/kubelet/config.yaml | grep -i container
# containerLogMaxSize: 10Mi
# containerLogMaxFiles: 5

預設值很保守:單檔 10MB、保留 5 份,上限 50MB。日誌量大的服務(API Gateway、access log)這個預設絕對不夠用,後面會講怎麼調。

1.3 containerd-shim 在 log 路徑上的角色

containerd-shim-runc-v2 是每個 container 的「父進程」,存在的理由有三個:

  • 解耦 containerd daemon 與 container 生命週期:containerd 升級重啟時,shim 還在跑,container 不會被殺。
  • 接管 stdin/stdout/stderr:container 退出後,shim 還能把最後那批 buffer flush 出來。
  • 處理 reaper 角色:container 主進程退了,shim 會收到 SIGCHLD,再回報給 containerd。

實際看一下節點上的 shim:

# 列出所有 shim 進程,每個 container 一個
ps aux | grep containerd-shim | head -3

# 用 ctr 列出 k8s.io namespace 的 container(這是 kubelet 創的)
sudo ctr -n k8s.io containers list

# 直接從 shim 撈 task 的 stdout(containerd 原生方式)
sudo ctr -n k8s.io tasks logs <container-id>

kubectl logs 整路鏈不通的時候(API Server 掛、kubelet 掛),ctr -n k8s.io tasks logs 是最後一道救命指令。

1.4 日誌輪轉的預設行為與調整

預設 10Mi × 5 對於高流量 service 來說,可能 5 分鐘就翻完一輪,舊的 ERROR 訊息就丟了。生產環境我通常調成這樣:

# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
containerLogMaxSize: 100Mi
containerLogMaxFiles: 10

調完要 systemctl restart kubelet 才生效。要注意的是這個是 kubelet 級的全域設定,不能 per-Pod 設定。如果單一服務日誌量爆炸,正解是調 application log level 或乾脆走 sidecar 寫到別的 PV,不是無腳鎖大這個值。

下面這張圖把整條資料流串起來:

圖 1

理解這條鏈路之後,就會發現「進容器 tail -f」這個動作完全跳過了上面所有基礎設施——你看到的日誌是寫到 stdout 之前那一瞬間的字串,container 一被驅逐就什麼都沒了,運維上完全沒有意義。

二、kubectl logs 的正確姿勢

kubectl logs 本質是在問 API Server:「我要看這個 container 的 log」。請求路徑是 kubectl → API Server → kubelet → CRI → 讀檔。所以 API Server 或 kubelet 任一段壞掉都會看不到,這個後面會講替代方案。

2.1 基本用法(current / previous / 多 container)

最常用的幾個 pattern:

# 看當前 container 的所有 log
kubectl logs nginx-deployment-6d9f4c8b7-x2m9n

# 看上一次重啟前的 log(debug crash loop 必備)
kubectl logs nginx-deployment-6d9f4c8b7-x2m9n --previous

# 持續跟蹤(取代 tail -f)
kubectl logs -f nginx-deployment-6d9f4c8b7-x2m9n

# 多 container Pod 必須指定 -c
kubectl logs <pod> -c <container>
kubectl logs <pod> --all-containers=true

# 用 label selector 看多個 Pod
kubectl logs -l app=nginx --tail=100

--previous(簡寫 -p)是排查 CrashLoopBackOff 的核心。新 container 起不來、執行 kubectl logs 看到的是「正在啟動」的訊息,但你想知道的是上一次為什麼掛——那次的 stdout 在 1.log,要靠 --previous 才能拿到。

2.2 --since / --tail / --follow 的使用時機

Flag 用途 典型場景
--tail=N 只看最後 N 行 快速確認服務是否在輸出
--since=DURATION 看最近多久內的日誌(5m, 1h, 24h 排查近期異常
--since-time=RFC3339 看某個時間點之後的日誌 對應到 incident 時間軸
-f, --follow streaming 即時觀察新部署
-p, --previous 看上次重啟前 crash 排障
--timestamps 每行加上 timestamp 對齊跨服務 timeline
--all-containers 一次抓 Pod 內全部 container sidecar 模式 debug
-c, --container 指定 container 多 container Pod

實戰組合:

# 最近 1 小時的 ERROR 級訊息
kubectl logs nginx-xxx --since=1h | grep -E "ERROR|FATAL"

# 從某個事故時間點開始拉
kubectl logs nginx-xxx --since-time="2026-04-27T03:00:00Z"

# 只盯尾巴 100 行並持續追
kubectl logs -f --tail=100 nginx-xxx

# 帶 timestamp 方便事後對照
kubectl logs nginx-xxx --timestamps --since=10m

注意 --tail 預設值的行為:不指定就是輸出全部、指定就只輸出最後 N 行。如果 log 量很大,沒指定 tail 直接拉全量會撐爆 API Server response(過去看過拉 GB 級 log 把 kubelet record buffer 弄爆的案例)。任何 production cluster 上的 kubectl logs,預設都該帶 --tail--since

2.3 多 Pod 即時看:stern 與替代方案

kubectl logs -l app=xxx -f 確實能跟蹤多個 Pod,但 1.x 版的這個功能很簡陋:不會幫你著色、不會標 Pod 名、Pod 重排還會斷掉。生產上更實用的是 stern

# 跟蹤 production namespace 下所有 nginx Pod 最近 10 分鐘的 log
stern -n production app=nginx --since=10m

# 只顯示包含 ERROR 的行
stern -n production app=nginx --filter=ERROR

# 帶 timestamp、自動著色
stern -n production app=nginx --timestamp

stern 會自動偵測新 Pod 加入、舊 Pod 消失,每個 Pod 用不同顏色。我幾乎所有 cluster 都會預先安裝這個工具。

替代方案還有 kubetail(純 bash 腳本)和 k9s(互動式 TUI,內建多 Pod log 切換)。我自己日常 ops 操作用 k9s 比較多,CI 環境用 stern。

三、節點層直接看(kubectl logs 救不回來時)

API Server 掛、kubelet 掛、network plugin 掛——這些情境下 kubectl logs 全部失效。但 log 檔還在節點上,只要能 SSH 進去就有救。

3.1 /var/log/pods 直接讀

最直接的方式是 SSH 上節點,從 /var/log/pods 撈:

# SSH 進節點
ssh node-01.prod

# 找你要的 Pod 目錄
ls /var/log/pods/ | grep nginx-deployment

# 直接讀
sudo tail -f /var/log/pods/default_nginx-deployment-xxx_abc123/nginx/0.log

# 看上次重啟前的
sudo cat /var/log/pods/default_nginx-deployment-xxx_abc123/nginx/1.log

CRI 寫進去的格式是 JSON Lines,每行長這樣:

{"log":"2026-04-27T10:00:00Z hello world\n","stream":"stdout","time":"2026-04-27T10:00:00.123Z"}

如果只想看訊息本體:

sudo cat /var/log/pods/.../0.log | jq -r '.log'

3.2 crictl logs 備援

crictl 是 CRI 的官方 CLI,等同於節點上的本地 kubectl logs完全不經過 API Server

# 列出節點上所有 container
sudo crictl ps

# 用 container ID 看 log
sudo crictl logs <container-id>

# 跟蹤
sudo crictl logs -f <container-id>

# 看最後 100 行
sudo crictl logs --tail=100 <container-id>

這是 cluster control plane 整個炸掉時的最後手段。我曾經在 etcd quorum 失效那次,靠 crictl logs 確認到底是哪些 component 還活著、哪些已經 panic。

另外節點層還有兩個重要的 systemd unit log:

# kubelet 自己的日誌,排查 Pod 為什麼起不來時必看
sudo journalctl -u kubelet --since "30 minutes ago" --no-pager

# 過濾 ERROR
sudo journalctl -u kubelet --since "1 hour ago" | grep -i error

# containerd 自己的日誌
sudo journalctl -u containerd --since "1 hour ago" | grep -E "error|warn"

journalctl -u kubelet 是診斷「Pod stuck in Pending」「volume mount failed」「image pull error」這類問題的第一站,會比看 Event 詳細很多。

四、分散式日誌收集架構

前面所有指令都是「我登進去看」的 reactive 操作。production 上規模一拉大,這些都不夠。Pod 平均壽命可能只有幾小時、節點隨 autoscaler 上下、log 在節點本機 10Mi 就被輪掉——你要查昨天某筆訂單為什麼失敗時,那個 Pod 連屍體都沒了。

集中式日誌收集就是把所有節點的 /var/log/containers/*.log 持續往中央倉庫推,存幾天到幾個月,並提供查詢介面。

4.1 為何要集中式收集

集中收集解決的問題:

  • Pod ephemeral:Pod 被殺、被 reschedule、節點被 scale down,本機 log 都會丟。
  • 跨 Pod 查詢:一筆 trace 可能跨十幾個 service,光看單個 Pod 的 log 拼不出完整脈絡。
  • 長期保留與合規:金融、醫療場景常要求 log 保留 90 天到 1 年,本機輪轉根本不夠。
  • 告警與分析:基於 log 的錯誤率告警、業務指標 query,必須有結構化儲存。

4.2 主流方案對比:EFK / PLG / Vector

方案 收集 儲存 查詢 資源消耗 適用
EFK Fluentd / Fluent Bit Elasticsearch Kibana 高(ES 吃 RAM) 已有 ELK 生態、需要全文檢索
PLG (Loki Stack) Promtail / Fluent Bit Loki Grafana 低(label index) 已用 Grafana、cost 敏感
Vector + ClickHouse Vector ClickHouse Grafana / Superset 大規模、需要 SQL 分析
Datadog / 雲廠商 SaaS Agent 託管 託管 UI 看計價方案 不想自己運維、人力成本高

我的個人經驗:

  • 小到中型 cluster(<50 nodes):直接上 PLG。Loki 用 S3 後端,cost 低、運維輕,跟 Grafana 整合度最高。Loki 3.0 之後查詢效能也夠用了。
  • 已有 ELK 生態或需要複雜全文檢索:EFK。Fluent Bit 比 Fluentd 輕量,當 collector 是首選。
  • 大規模、需要長期保留並做分析:Vector + ClickHouse。Vector 是 Rust 寫的,吞吐量比 Fluent Bit 還高,ClickHouse 撐得住 PB 級。
  • 預算夠且團隊小:直接 Datadog 或 GCP Cloud Logging。算總成本未必比自建貴。

雲廠商的 log 服務也別忽略:AWS 有 CloudWatch Logs Insights、GCP 有 Cloud Logging、Azure 有 Log Analytics、阿里雲有 SLS。如果整個 cluster 已經跑在某個雲上,用該雲的原生 log 服務通常 IAM、網路設定都最簡單,cost 對中小規模也算合理。重度用 Kubernetes 才需要考慮自建。

4.3 Sidecar vs DaemonSet 收集模式

兩種主流收集模式:

DaemonSet 模式(推薦):每個節點跑一個 collector Pod,掛載 /var/log/containers/,抓所有 container 的 stdout:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: kube-system
spec:
  selector:
    matchLabels:
      k8s-app: fluent-bit
  template:
    metadata:
      labels:
        k8s-app: fluent-bit
    spec:
      containers:
      - name: fluent-bit
        image: fluent/fluent-bit:3.0.0
        ports:
        - name: http
          containerPort: 2020
        volumeMounts:
        - name: varlog
          mountPath: /var/log
          readOnly: true
        - name: fluent-bit-config
          mountPath: /fluent-bit/etc
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: fluent-bit-config
        configMap:
          name: fluent-bit-config

Sidecar 模式:每個 application Pod 帶一個 log collector container。適合無法寫到 stdout 的 legacy app(例如 Java app 寫到 file,或者一個 Pod 想把 access log / app log 分開索引):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-sidecar
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:latest
        volumeMounts:
        - name: log-volume
          mountPath: /var/log/myapp
      - name: log-collector
        image: fluent/fluent-bit:3.0.0
        volumeMounts:
        - name: log-volume
          mountPath: /var/log/myapp
        - name: fluent-bit-config
          mountPath: /fluent-bit/etc
      volumes:
      - name: log-volume
        emptyDir: {}
      - name: fluent-bit-config
        configMap:
          name: fluent-bit-config

DaemonSet 模式的 N 倍效率優勢明顯(一個 collector 服務整個節點),但 sidecar 在「需要 per-Pod 不同 parser、不同 destination」時是唯一選擇。99% 場景用 DaemonSet 就夠。

下面這張圖是常見的 PLG 架構:

Loki + Promtail 集中式日誌架構:多 Node DaemonSet 收集到 Loki,Grafana 用 LogQL 查詢

Fluent Bit 的核心設定就是 INPUT / FILTER / OUTPUT 三段式。INPUT 從 /var/log/containers/*.log 讀檔,FILTER 補上 Kubernetes metadata(namespace、labels、annotations),OUTPUT 推到 ES 或 Loki:

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            cri
    Tag               kube.*
    Refresh_Interval  5

[FILTER]
    Name                kubernetes
    Match               kube.*
    Kube_URL            https://kubernetes.default.svc:443
    Merge_Log           On
    K8S-Logging.Parser  On
    K8S-Logging.Exclude On

[OUTPUT]
    Name   loki
    Match  kube.*
    Host   loki.monitoring.svc
    Port   3100
    Labels job=fluentbit, namespace=$kubernetes['namespace_name']

K8S-Logging.ParserK8S-Logging.Exclude 是兩個很實用的開關:在 Pod 上加 annotation fluentbit.io/parser: json 就能 per-Pod 指定 parser、加 fluentbit.io/exclude: "true" 就能跳過收集。這對於「某些超高頻 Pod 不要收進來避免炸儲存」的場景特別有用。

五、典型踩雷與排障

整理幾個我親自踩過的雷:

雷 1:CrashLoopBackOff 看不到原因。新起的 container 一上來就 crash,kubectl logs 看到的是「ContainerCreating」。解法是 kubectl logs --previous 看上次的;如果連 --previous 都沒有(首次啟動就 crash),用 kubectl describe pod 看 Event,或者去節點上 journalctl -u kubelet 看 image pull / volume mount 階段有沒有失敗。

雷 2:log 看到一半突然斷掉。十之八九是日誌輪轉。kubectl logs 預設只讀「當前」那個 log 檔,輪轉後從 0.log1.log-f 可能會跟丟。production 上把 containerLogMaxSize 調大、collector 設定 Read_from_Head On 都能緩解。

雷 3:Pod 已被刪除,要查當時的 log。如果有集中收集就在 Loki/Kibana 撈;沒有的話,趁 Pod 還在 Terminating 狀態時 SSH 上節點 cat /var/log/pods/.../0.log,因為 Pod 一旦完全消失,kubelet 的 GC 會把目錄清掉。

雷 4:kubectl logs 跟 stdout 不一致。可能原因有三個:(1)log 輪轉導致 kubectl logs 拿的是當前檔,但你實際想要的在 1.log;(2)container 被重啟過,--previous 才是你要的;(3)應用程式 stderr 沒重定向,導致部分輸出沒被 CRI 抓到。

雷 5:Fluent Bit 收不到日誌。先檢查 collector pod 自己的狀態:

kubectl get pod -n kube-system -l k8s-app=fluent-bit
kubectl logs -n kube-system -l k8s-app=fluent-bit --tail=100
kubectl describe configmap fluent-bit-config -n kube-system

最常見原因是:Parser 跟實際 log 格式對不上(CRI 從某個版本改成 plain-text 不再是 JSON,要用 Parser cri 而不是 Parser docker);hostPath mount 路徑寫錯;ServiceAccount RBAC 不夠(Kubernetes Filter 要 list/watch pods 權限)。

雷 6:log 把節點磁碟塞爆。某個 container 突然 INFO 級狂噴,半小時把節點 /var/log 撐爆,kubelet 標記節點 DiskPressure 開始驅逐 Pod。緊急處置:

# 找出最大的 log
sudo find /var/log/pods -name "*.log" -size +100M -exec ls -lh {} \;

# 暫時清掉舊輪轉檔
sudo find /var/log/pods -name "*.log.*" -mtime +1 -delete

# 看哪個目錄最大
du -sh /var/log/pods/* | sort -h | tail -10

長期解法:應用層 log level 調回 INFO 以上、kubelet 設定合理輪轉、加 PrometheusRule 告警。

六、生產環境建議

把上面所有點濃縮成一份 checklist:

應用層

  • 所有 log 寫到 stdout/stderr,不要寫到 container 內檔案。
  • 用 JSON 結構化輸出,至少包含 timestamp / level / service / request_id / message
  • log level 用對:ERROR 真的影響功能、WARN 潛在問題、INFO 重要事件、DEBUG 預設關閉。
  • request_idtrace_id 串連跨服務 log,搭配 OpenTelemetry 更好。

節點層

  • kubelet 的 containerLogMaxSize / containerLogMaxFiles 調到符合服務量級。
  • 節點 /var/log 分區獨立,並對使用率設告警(>80% 觸發)。
  • 安裝 stern 或 k9s 給值班工程師。

集中收集層

  • 一律 DaemonSet 模式 collector,sidecar 只給特殊 case。
  • collector 本身要設 resource limit,防止它自己 OOM 把節點搞掛。
  • Loki / ES 後端走物件儲存(S3、GCS)做冷熱分層。
  • 熱資料 7-30 天、溫資料 30-90 天歸檔、冷資料 90 天以上轉 Glacier 之類。

告警層

  • 基於 log 的錯誤率告警(Loki recording rules 或 ES Watcher)。
  • 節點 /var/log 使用率告警。
  • collector 自身的 lag 告警(input rate vs output rate)。
# 例:節點 /var/log 使用率超過 80%
- alert: LogStorageUsageHigh
  expr: |
    (node_filesystem_avail_bytes{mountpoint="/var/log"} /
     node_filesystem_size_bytes{mountpoint="/var/log"}) < 0.2
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "Log storage usage above 80% on {{ $labels.instance }}"

操作習慣

  • 排查問題的第一步永遠是先去集中 log 平台搜尋(Grafana / Kibana),不是 SSH 進節點。
  • kubectl logs 一律帶 --tail--since,不要無腦拉全量。
  • 多 container Pod 養成 -c 習慣,不要靠錯誤訊息提醒。
  • crash 排查必看 --previous,搭配 kubectl describe pod 看 Event。
  • 節點層救援工具放最後:crictl logs > journalctl -u kubelet > cat /var/log/pods

七、小結

容器日誌的處理是 K8s 運維基本功裡看似簡單、但細節極多的一塊。能力分三層:

  • 第一層(基礎):熟練 kubectl logs 各種 flag、知道 --previous、知道 -c、知道 --since--tail 的差別。
  • 第二層(架構):理解 /var/log/pods 的儲存結構、kubelet 與 containerd-shim 的角色、輪轉設定的影響、節點層用 crictljournalctl 救援。
  • 第三層(平台):建立集中式日誌收集架構(PLG 或 EFK),有 retention 策略、有結構化欄位、有基於 log 的告警,整合進 SLO 體系。

所有「進容器 tail -f」的習慣都應該被替換掉。它在規模一上來就會崩、debug 會失準、Pod 被驅逐後完全沒救。建立心智模型——log 是一條從 stdout → containerd → 節點檔案 → collector → 中央倉庫的 stream——之後,你會發現 90% 的 log 排障問題其實在問「這條鏈的哪一段斷了」,知道每段在哪、怎麼查,就比在 shell 裡 grep 強太多。

最後一句話:production 上的 log 不該需要 SSH 才能看。 如果今天你還是非 SSH 不可,那不是排障難度的問題,是平台還沒做好。

參考資料

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料