kubectl logs là lệnh ai cũng gõ trước tiên khi có sự cố. Bài này đo xem nó thực sự đọc cái gì — và cái gì nó không đọc.

Đường đi của một dòng log và kết quả đo sau khi xoay vòng

Đường đi

ứng dụng ghi stdout
   -> containerd ghi ra tệp
      /var/log/pods/<ns>_<pod>_<uid>/<container>/0.log
         -> kubelet đọc lại khi có ai gọi kubectl logs

Không có cơ sở dữ liệu, không có bộ đệm. Chỉ là một tệp văn bản trên đĩa của node, mỗi dòng kèm dấu thời gian và nhãn stdout/stderr.

Điều đó giải thích một chuyện hay bị hỏi: log không đi qua control plane. kubectl logs mở một luồng tới kubelet của đúng node đó. Node chết là không đọc được nữa, dù pod vẫn còn trong etcd.

Đo: 400.000 dòng, trả về 0 dòng

Tôi cho một pod in 400.000 dòng liên tục rồi ngủ, và đợi.

Trên node:

        0 byte   0.log
55.392.273 byte  0.log.20260831-160306

kubectl logs:

0 dòng

Không lỗi. Không cảnh báo. Không có gì cho biết 53 MB nhật ký đang nằm ngay đó.

Tôi tưởng mình gõ nhầm tên pod. Một pod khác in ba dòng thì kubectl logs trả về đúng ba dòng, nên lệnh không hỏng.

Lần đo thứ hai, rõ hơn

Lần này pod in 300.000 dòng nhanh, rồi in tiếp mỗi hai giây một dòng.

40.940.121 byte  0.log.20260831-160526   (300.000 dòng "cu")
     1.678 byte  0.log                   (36 dòng "moi")

kubectl logs -> 36 dòng, dòng đầu là "moi 1"

Đủ rõ: kubectl logs chỉ đọc tệp hiện tại. Mọi thứ đã xoay vòng coi như mất, kể cả khi tệp còn nguyên trên đĩa.

Hệ quả thực tế đáng sợ hơn nó nghe: một pod ồn ào tự xoá nhật ký của chính nó. Đúng lúc sự cố xảy ra, ứng dụng thường in nhiều nhất — stack trace, retry, warning — và chính lượng đó đẩy phần đầu của sự cố ra khỏi tầm với của kubectl logs. Bạn mở log lên và chỉ thấy triệu chứng cuối cùng.

Cách chữa duy nhất là gom log ra ngoài. kubectl logs là công cụ xem nhanh, không phải nơi lưu trữ.

Trong lúc chưa có bộ gom, còn một đường: đọc thẳng tệp đã xoay vòng trên node.

kubectl debug node/<node> -it --image=busybox -- \
  ls -la /host/var/log/pods/<ns>_<pod>_<uid>/<container>/

Xấu, nhưng đó là chỗ duy nhất còn dữ liệu.

containerLogMaxSize không phải trần cứng

Mặc định của kubelet là containerLogMaxSize: 10MicontainerLogMaxFiles: 5. Tôi trông đợi tệp đã xoay vòng nặng khoảng 10 MB.

Đo được 55 MB40 MB.

Lý do: kubelet kiểm tra định kỳ rồi mới xoay vòng, chứ không chặn lúc ghi. Giữa hai lần kiểm tra, container ghi được bao nhiêu thì ghi. Container ghi nhanh thì vượt trần vài lần là chuyện thường.

Nghĩa là phép tính dung lượng đĩa quen thuộc — 10Mi × 5 tệp × số containerthiếu vài lần trong tình huống xấu nhất. Node hết ephemeral-storage vì log là sự cố có thật, và nó xảy ra đúng lúc mọi thứ đang hỏng nên mọi ứng dụng đều đang in nhiều.

--previous và giới hạn của nó

kubectl logs <pod> --previous

Đọc log của lần chạy trước của container — cứu tinh khi pod CrashLoopBackOff, vì log của lần chạy hiện tại thường trống rỗng.

Giới hạn: chỉ giữ một lần trước. Container khởi động lại hai lần là lần đầu tiên mất hẳn, mà lần đầu tiên mới là lần chứa nguyên nhân gốc.

Vài lệnh hữu dụng ít người dùng:

kubectl logs <pod> --since=15m           # theo thời gian
kubectl logs <pod> --timestamps          # thêm dấu thời gian
kubectl logs -l app=web --max-log-requests=20   # gom nhiều pod
kubectl logs <pod> -c <container> -f     # theo dõi một container cụ thể

-l rất tiện lúc điều tra, nhưng nhớ là nó chỉ theo dõi pod đang tồn tại lúc gõ lệnh — pod mới sinh ra sau đó không tự vào luồng.

Sự kiện: nửa còn lại, và nó hết hạn

kubectl logs cho biết ứng dụng nói gì. kubectl get events cho biết Kubernetes nói gì — vì sao pod không xếp được lịch, vì sao image kéo hỏng, vì sao probe trượt.

kubectl get events -n <ns> --sort-by=.lastTimestamp
kubectl describe pod <pod> | sed -n '/Events:/,$p'

Bẫy lớn nhất: sự kiện chỉ giữ một giờ (--event-ttl mặc định của API server). Sự cố lúc 2 giờ sáng, 9 giờ sáng mở lên xem thì không còn gì. Và khác với log, sự kiện đã hết hạn thì không còn ở đâu trên đĩa cả.

Nên khi bắt được sự cố, việc đầu tiên là chụp lại:

kubectl get events -A -o yaml > su-kien-$(date +%s).yaml

Gõ trước, đọc sau. Một phút chần chừ có thể là một giờ mò mẫm.

Thử ba mươi giây

Xem pod nào đang xoay vòng log nhanh nhất — tức là pod đang mất log của chính nó:

kubectl get --raw "/api/v1/nodes/<node>/proxy/stats/summary" \
  | python3 -c '
import sys, json
d = json.load(sys.stdin)
r = []
for p in d.get("pods", []):
    for c in p.get("containers", []):
        b = (c.get("logs") or {}).get("usedBytes")
        if b: r.append((b, p["podRef"]["namespace"], p["podRef"]["name"], c["name"]))
for b, ns, n, c in sorted(r, reverse=True)[:10]:
    print(f"{b/1e6:8.1f} MB  {ns}/{n}  {c}")
'

Đây là tổng cả tệp đã xoay vòng, không phải riêng tệp hiện tại — tôi đã đoán nhầm và phải chạy lại mới thấy: pod noi ở trên có 0.log rỗng mà vẫn báo 55,4 MB.

Điều đó lại làm con số này hữu dụng hơn. Pod nào vượt xa 10Mi là pod đã xoay vòng ít nhất một lần, tức là đã có một khối log biến mất khỏi tầm với của kubectl logs. Sắp xếp giảm dần rồi gom log của mấy pod đầu bảng trước.

Phần sau: chỉ số — metrics-server đo gì, Prometheus đo gì, và vì sao hai con số không bao giờ khớp nhau.