← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202619 phút đọc17 min read

Observability & Troubleshooting (Khả năng quan sát & Gỡ lỗi)Observability & Troubleshooting

Thuộc knowledge base theo Kubernetes Roadmap.

Tổng quan

Kubernetes là một vòng lặp điều khiển (control loop) liên tục hòa hợp desired state (những gì YAML của bạn khai báo) với actual state (những gì kubelet và scheduler quan sát được). Observability và troubleshooting trên Kubernetes là kỷ luật làm cho quá trình reconcile đó trở nên dễ đọc: hiểu vì sao một Pod bị Pending, vì sao nó cứ restart, và liệu workload của bạn có thực sự khỏe dưới góc nhìn của người dùng hay không — chứ không chỉ “đang chạy”. Nền tảng cung cấp cho bạn các primitive mà không stack VM thông thường nào có: probe để kubelet hành động dựa trên sức khỏe, Events kể lại mọi quyết định scheduling và lifecycle, hạch toán tài nguyên (resource accounting) theo từng container, và một bề mặt kubectl thống nhất cho logs, exec và debug trực tiếp.

Mô hình tư duy tiết kiệm thời gian nhất: gần như mọi vấn đề Kubernetes đều là một trong bốn nhóm — vấn đề scheduling (Pod không được đặt lên node — Pending, unschedulable), vấn đề image (không pull được image — ImagePullBackOff), vấn đề startup/runtime (process crash hoặc fail probe — CrashLoopBackOff, readiness failing), hoặc vấn đề tài nguyên (container bị throttle hoặc bị kill — OOMKilled, evicted). Mỗi nhóm có một “chữ ký” riêng trong kubectl describe, kubectl logs và luồng Events, và mỗi nhóm có một đường chẩn đoán quen thuộc. Học đọc các chữ ký đó biến “app chết rồi” từ hoảng loạn thành một cuộc triage năm phút.

Nằm trên phần troubleshooting thô là observability stack: metrics-server cho kubectl top và autoscaling, hệ sinh thái Prometheus (kube-state-metrics, node-exporter, Grafana, Prometheus Operator) cho giám sát toàn cluster, tổng hợp log ở cấp node (Fluent Bit → Loki/Elasticsearch), và distributed tracing qua OpenTelemetry cho cái nhìn ở cấp request. Kubernetes cố tình không đóng gói sẵn lưu trữ metrics hay log dài hạn — nó phát ra telemetry (stdout/stderr của container, metric cAdvisor, Events với TTL ngắn) và kỳ vọng bạn lắp thêm một stack để thu thập và lưu giữ. Hiểu sự phân chia đó — nền tảng cho gì so với những gì bạn phải tự thêm — là toàn bộ cuộc chơi.

Hợp đồng observability của một container trên Kubernetes

Một workload cư xử tốt làm ba việc khiến nó quan sát được: ghi log ra stdout/stderr (không bao giờ vào một file bên trong container mà node agent không đọc tới), expose các health endpoint để kubelet probe (liveness/readiness/startup), và khai báo resource requests và limits để scheduler và OOM killer ra quyết định đúng. Làm đúng ba điều này thì 80% câu hỏi “vì sao app của tôi hỏng trên Kubernetes?” tự trả lời chỉ bằng kubectl.

Kiến thức nền tảng

Health probes: liveness, readiness, startup

Kubelet chủ động probe các container để quyết định số phận của chúng. Có ba loại probe, mỗi loại trả lời một câu hỏi khác nhau, và nhầm lẫn giữa chúng gây ra nhiều sự cố tự chuốc lấy nhất trên Kubernetes.

ProbeCâu hỏiHành động khi failKhi nào chạy
LivenessProcess có còn sống không, hay bị treo/deadlock?Restart containerLiên tục, sau initialDelaySeconds / startup probe
ReadinessProcess có phục vụ traffic được ngay lúc này không?Gỡ Pod khỏi Endpoints của Service (không nhận traffic) — không restartLiên tục, suốt vòng đời Pod
StartupApp đã boot xong chưa?Restart nếu không bao giờ khởi động; chặn liveness/readiness cho đến khi passChỉ lúc khởi động, sau đó tắt

Các hệ quả bạn phải nằm lòng:

Mỗi probe hỗ trợ ba cơ chế: httpGet (2xx/3xx = pass), tcpSocket (mở được port không?), và exec (một lệnh với exit code 0 = pass). Probe grpc cũng được hỗ trợ native. Các tham số điều chỉnh: initialDelaySeconds, periodSeconds, timeoutSeconds, successThreshold, failureThreshold.

# Một container với cả ba probe được cấu hình đúng.
containers:
  - name: api
    image: myorg/api:1.4.2
    ports:
      - containerPort: 8080
    # Startup: cho tối đa 30 * 10s = 5 phút để boot, rồi bàn giao.
    startupProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      failureThreshold: 30
    # Liveness: process có bị treo? Endpoint rẻ, không phụ thuộc dependency.
    livenessProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3          # 3 * 10s = 30s fail trước khi restart
    # Readiness: tôi phục vụ được không? Có thể phản ánh sức khỏe dependency.
    readinessProbe:
      httpGet: { path: /ready, port: 8080 }
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3

metrics-server và kubectl top

metrics-server là một aggregator nhẹ, toàn cluster, cho các metric tài nguyên (CPU và memory) được scrape từ Summary API của mỗi kubelet (dựa trên cAdvisor). Nó không phải hệ thống monitoring — chỉ giữ mẫu mới nhất trong bộ nhớ, không có lịch sử — nhưng nó cấp nguồn cho hai thứ quan trọng: lệnh kubectl topHorizontal Pod Autoscaler (và VPA). Hầu hết cluster cần cài nó; các dịch vụ managed thường bao gồm sẵn.

kubectl top nodes                 # sử dụng CPU/memory mỗi node so với capacity
kubectl top pods -A               # sử dụng theo pod trên tất cả namespace
kubectl top pods --containers     # tách theo container trong pod
kubectl top pod mypod --sort-by=memory

Nếu kubectl top trả về “Metrics API not available”, nghĩa là metrics-server thiếu hoặc không khỏe (nguyên nhân rất phổ biến trên cluster kubeadm/tự quản là cần flag --kubelet-insecure-tls cho cert kubelet self-signed).

Monitoring stack (hệ sinh thái Prometheus)

Để giám sát thật sự — có lịch sử, alert được — chuẩn de-facto là stack Prometheus, thường cài dạng bundle qua Helm chart kube-prometheus-stack:

# ServiceMonitor: Operator scrape mọi Service khớp các label này,
# trên port tên "metrics", tại /metrics — không cần sửa config trung tâm.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api
  labels: { release: kube-prometheus-stack }
spec:
  selector:
    matchLabels: { app: api }
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics

Logging trên Kubernetes

Kubernetes không có lưu trữ log toàn cluster native. Mô hình là:

  1. Container ghi ra stdout/stderr. Container runtime bắt các luồng đó và kubelet ghi chúng vào file trên node (/var/log/pods/...), được kubelet rotate.
  2. kubectl logs đọc các file cục bộ trên node đó — nghĩa là log biến mất khi Pod bị xóa hoặc node chết, và mất qua các lần restart container trừ khi bạn yêu cầu instance trước đó.
  3. Để có log bền, tìm kiếm được, toàn cluster, bạn chạy một agent cấp node dạng DaemonSet tail file log của mọi container và ship chúng tới một backend.
kubectl logs mypod                       # log container hiện tại
kubectl logs mypod -c sidecar            # một container cụ thể trong pod nhiều container
kubectl logs mypod --previous            # log từ container TRƯỚC (đã crash) — sống còn cho CrashLoopBackOff
kubectl logs -f deploy/api               # theo dõi log một pod của Deployment
kubectl logs -l app=api --all-containers --tail=100   # trên mọi pod khớp một label

Agent cấp node và backend:

Anti-pattern là ghi log vào một file bên trong container: node agent tail stdout/stderr, nên một file log trên filesystem của container vô hình với collector và mất khi restart. Logging kiểu Twelve-Factor (stdout như một luồng sự kiện) tồn tại chính vì lý do này.

Kubernetes Events

Events là các bản ghi sống ngắn (TTL mặc định ~1 giờ) mà control plane phát ra để kể lại những gì đang xảy ra với các object: quyết định scheduling, pull image, fail probe, mount volume, OOM kill, eviction. Chúng là tín hiệu troubleshooting giàu nhất và bị dùng chưa hết nhất.

kubectl get events --sort-by=.lastTimestamp          # theo thời gian, namespace hiện tại
kubectl get events -A --sort-by=.lastTimestamp       # tất cả namespace
kubectl describe pod mypod                            # Events của Pod NÀY ở cuối — bắt đầu ở đây
kubectl get events --field-selector involvedObject.name=mypod,type=Warning

Vì Events hết hạn, truy một vấn đề chập chờn nghĩa là ship chúng tới backend logging (Fluent Bit/các kube-events exporter làm được) để giữ lịch sử.

Distributed tracing (con trỏ)

Metrics nói một service chậm; trace nói ở đâu trong một request nhiều service thời gian bị mất. Trên Kubernetes chuẩn là OpenTelemetry (OTel): instrument app bằng OTel SDK (hoặc auto-instrumentation), chạy OTel Collector dạng DaemonSet/Deployment để nhận, batch, sample và export span, và lưu chúng trong Jaeger hoặc Grafana Tempo. Chi tiết cốt lõi là context propagation — header W3C traceparent phải sống sót qua mọi hop (kể cả qua Service, ingress, và message queue) hoặc trace sẽ bị vỡ mảnh. Tracing được trình bày sâu trong trang observability của DevOps; trên Kubernetes điểm mấu chốt là triển khai Collector và đấu nối sidecar/DaemonSet đúng cách.

Khái niệm chính

Quy trình troubleshooting

Một thứ tự triage lặp lại được thắng việc đoán mò. Bắt đầu rộng, rồi thu hẹp:

# 1. Trạng thái tổng quan? Tìm cái không-Running / không-Ready / số lần restart.
kubectl get pods -o wide
kubectl get pods -A | grep -vE 'Running|Completed'

# 2. Vì sao? describe hiện Events, kết quả probe, trạng thái cuối, exit code, lý do.
kubectl describe pod <pod>

# 3. App nói gì? Logs — và log TRƯỚC nếu nó crash.
kubectl logs <pod>
kubectl logs <pod> --previous

# 4. Thọc vào một container đang chạy (chỉ khi nó còn sống).
kubectl exec -it <pod> -- sh
kubectl exec <pod> -- env            # kiểm tra config/env var, DNS, secret đã mount

# 5. Nếu container không có shell / đang crash, gắn một ephemeral debug container.
kubectl debug -it <pod> --image=busybox:1.36 --target=<container>

# 6. Bối cảnh cấp cluster.
kubectl get events --sort-by=.lastTimestamp
kubectl top pod <pod> --containers   # nó có gần limit memory/CPU không?

kubectl debug và ephemeral container đáng được nhấn mạnh: các image được hardening hiện đại thường là distroless (không shell, không curl, không package manager), nên kubectl exec ... -- sh thất bại. Ephemeral container cho phép gắn một debug container đầy đủ công cụ mà chia sẻ namespace của Pod đích (process, network) mà không restart hay sửa Pod — bạn có thể soi network và file của process đang crash một cách trực tiếp. kubectl debug node/<node> cho bạn một container privileged trên một node để debug cấp node.

Các chữ ký lỗi thường gặp

Triệu chứngNguyên nhân khả dĩCách xác nhậnHướng khắc phục
CrashLoopBackOffApp exit/crash lúc khởi động; liveness probe fail; config sai/thiếu env; command saikubectl logs --previous; describe hiện exit code & số lần restartĐọc log trước; sửa crash, config, hoặc liveness probe quá gắt. BackOff chỉ là độ trễ restart mũ của kubelet, không phải nguyên nhân gốc.
ImagePullBackOff / ErrImagePullSai tên/tag image; registry private không có imagePullSecret; rate-limit/auth registry; sai kiến trúcdescribe hiện lỗi pull nguyên văn trong EventsSửa tag; thêm/gắn imagePullSecret; kiểm tra auth registry và network của node.
Pending / UnschedulableKhông node nào đủ CPU/mem (requests quá cao); taint không có toleration; nodeSelector/affinity không thỏa; không có PV cho PVC; node pressuredescribe pod → Events: “0/5 nodes are available: Insufficient cpu…”Giảm requests, thêm node/capacity autoscaler, sửa taint/affinity, hoặc provision PVC.
OOMKilledContainer vượt limit memorydescribe → Last State: Terminated, Reason: OOMKilled, exit 137; kubectl top gần limitTăng memory limit, sửa rò rỉ (leak), hoặc giảm memory workload. Lưu ý limit ≠ request.
Readiness fail (Pod sống, không traffic)/ready trả non-2xx; dependency chết; sai port/path; timeout quá gắtdescribe hiện “Readiness probe failed”; Pod Running nhưng 0/1 READYSửa readiness endpoint, dependency, port, hoặc threshold. Pod không nhận traffic Service cho tới khi pass.
CreateContainerConfigErrorConfigMap/Secret/key được tham chiếu không tồn tạiEvents trong describe nêu tên object thiếuTạo ConfigMap/Secret hoặc sửa tham chiếu.
EvictedNode bị memory/disk pressure; Pod vượt ephemeral-storage; QoS class thấpdescribe node hiện các điều kiện pressureĐặt requests (nâng QoS), dọn disk, thêm capacity.
Node NotReadykubelet chết, sự cố network/CNI, disk/memory pressurekubectl describe node; kubectl get nodesĐiều tra kubelet/CNI trên node; cordon/drain nếu do phần cứng.

Bảng tra exit code: 0 = thoát sạch; 1/2 = lỗi app; 137 = SIGKILL (thường là OOMKilled hoặc bị liveness kill); 143 = SIGTERM (tắt êm); 139 = SIGSEGV.

QoS class và OOM killer

Limit memory của một container được kernel cgroup cưỡng chế: vượt qua thì process bị OOMKilled (137). Request ảnh hưởng scheduling và QoS class của Pod, cái quyết định thứ tự eviction khi node bị pressure:

Đây là lý do “không limit = nhanh” là một cái bẫy: Pod BestEffort là nạn nhân đầu tiên khi một node quá tải. Đặt requests hợp lý vừa giữ scheduler trung thực vừa bảo vệ Pod của bạn khi pressure.

Đọc kubectl describe như phim X-quang

Ba phần chẩn đoán nhất của kubectl describe pod: State / Last State (trạng thái hiện tại, và cách instance container trước chết — exit code và lý do, ví dụ OOMKilled), Conditions (Ready, PodScheduled, ContainersReady — một False ở đây khoanh vùng lỗi), và danh sách Events ở cuối (câu chuyện theo thời gian — lỗi pull, fail probe, thông báo scheduling). Chín mươi phần trăm triage tuyến đầu nằm ở đây trước khi bạn nhìn tới log ứng dụng.

Best Practices

  1. Định nghĩa liveness, readiness và startup probe một cách có chủ đích — chúng không thay thế được cho nhau. Readiness chặn traffic; liveness restart; startup mua thời gian boot. Nhầm lẫn chúng (ví dụ liveness probe kiểm tra một database) biến các cú chập chờn dependency nhất thời thành bão restart.
  2. Giữ liveness probe rẻ và không phụ thuộc dependency. Liveness chỉ nên trả lời “process này có bị treo không?” — đừng gọi service phía sau từ nó, kẻo một outage dependency trở thành vòng lặp restart lan truyền trên cả fleet.
  3. Để readiness phản ánh khả năng phục vụ, bao gồm warmup và dependency. Một Pod chưa ready đơn giản bị gỡ khỏi load balancer, đúng là hành vi nhẹ nhàng bạn muốn khi warmup hoặc khi dependency suy giảm.
  4. Dùng startup probe cho app boot chậm thay vì initialDelaySeconds khổng lồ cho liveness. Nó tách sự khoan dung lúc boot khỏi việc phát hiện treo ở trạng thái ổn định, để bạn kiên nhẫn lúc khởi động và nhạy bén về sau.
  5. Luôn đặt resource requests và (memory) limits. Requests điều khiển scheduling và QoS; một memory limit ngăn một Pod rò rỉ kéo sập cả node. Không có requests nghĩa là BestEffort — bị evict đầu tiên khi pressure.
  6. Log ra stdout/stderr dưới dạng JSON có cấu trúc, không bao giờ vào file trong container. Node agent chỉ thấy stdout/stderr; log dạng file vô hình với collector và mất khi restart. Bao gồm một trace_id để liên kết chéo tín hiệu.
  7. Cài metrics-server và một monitoring stack thật. metrics-server bật kubectl top và autoscaling; kube-prometheus-stack cho metric có lịch sử, alert được. Cả hai đều không tùy chọn với một cluster production.
  8. Alert theo trạng thái object Kubernetes qua kube-state-metrics. Alert “replica available < desired”, “restart tăng vọt”, “Pod kẹt Pending”, và node pressure — triệu chứng của sức khỏe workload, không chỉ CPU thô.
  9. Quản lý config scrape bằng CRD của Prometheus Operator. ServiceMonitor/PodMonitor/PrometheusRule khiến config monitoring declarative, version-controlled và review được thay vì một file trung tâm dễ vỡ.
  10. Ship Events và logs khỏi node trước khi chúng hết hạn. Events có TTL ~1h và log container chết cùng Pod; forward cả hai tới một backend bền (Loki/Elastic) để post-mortem có lịch sử.
  11. Học thứ tự triage describe → logs --previous → exec/debug. Bắt đầu với describe (Events, exit code, conditions), đọc log trước khi crash loop, và chỉ sau đó mới thọc vào trong. Thứ tự này giải quyết hầu hết sự cố nhanh.
  12. Thành thạo ephemeral debug container cho image distroless. Image được hardening không có shell; kubectl debug --target gắn một container đầy đủ công cụ chia sẻ namespace của Pod mà không restart nó — cách duy nhất để soi một Pod distroless đang crash một cách trực tiếp.
  13. Đọc exit code như bằng chứng hạng nhất. 137 = OOMKilled hoặc liveness kill, 143 = SIGTERM êm, 1/2 = lỗi app. Code cộng lý do “Last State” thường nêu tên nhóm lỗi ngay lập tức.
  14. Thiết lập graceful shutdown (terminationGracePeriodSeconds + xử lý SIGTERM) và readiness khi shutdown. Trong một rolling update, một app phớt lờ SIGTERM làm rớt các request đang xử lý; fail readiness trước sẽ drain nó khỏi Service trước khi bị kill.
  15. Instrument request bằng OpenTelemetry và propagate traceparent. Metrics nói một service chậm; trace nói ở đâu. Một header propagation bị rớt âm thầm làm vỡ mảnh trace — kiểm tra nó sống sót qua mọi hop, kể cả ingress và queue.
  16. Chỉnh đúng kích cỡ probe và threshold; test các failure mode. timeoutSeconds/failureThreshold quá gắt gây flapping và restart sai. Chạy game-day: kill một dependency, làm cạn memory, và kiểm tra probe, alert và dashboard cư xử đúng ý.

Tài liệu tham khảo

Part of the Kubernetes Roadmap knowledge base.

Overview

Kubernetes is a control loop that constantly reconciles desired state (what your YAML says) against actual state (what the kubelet and scheduler observe). Observability and troubleshooting on Kubernetes is the discipline of making that reconciliation legible: understanding why a Pod is Pending, why it keeps restarting, and whether your workloads are actually healthy from a user’s perspective — not just “running”. The platform gives you primitives that no ordinary VM stack has: probes that let the kubelet act on health, Events that narrate every scheduling and lifecycle decision, per-container resource accounting, and a uniform kubectl surface for logs, exec, and live debugging.

The mental model that saves the most time: almost every Kubernetes problem is either a scheduling problem (the Pod can’t be placed — Pending, unschedulable), an image problem (the container image can’t be pulled — ImagePullBackOff), a startup/runtime problem (the process crashes or fails its probes — CrashLoopBackOff, readiness failing), or a resource problem (the container is throttled or killed — OOMKilled, evicted). Each class has a distinct signature in kubectl describe, kubectl logs, and the Events stream, and each has a well-worn diagnostic path. Learning to read those signatures turns “the app is down” from a panic into a five-minute triage.

On top of raw troubleshooting sits the observability stack: metrics-server for kubectl top and autoscaling, the Prometheus ecosystem (kube-state-metrics, node-exporter, Grafana, the Prometheus Operator) for cluster-wide monitoring, node-level log aggregation (Fluent Bit → Loki/Elasticsearch), and distributed tracing via OpenTelemetry for request-level insight. Kubernetes deliberately ships no long-term metrics or log storage in the box — it emits telemetry (container stdout/stderr, cAdvisor metrics, Events with a short TTL) and expects you to bolt on a stack to collect and retain it. Understanding that division — what the platform gives you versus what you must add — is the whole game.

The observability contract of a container on Kubernetes

A well-behaved workload does three things that make it observable: it writes logs to stdout/stderr (never to a file inside the container, which the node agent can’t reach), it exposes health endpoints the kubelet can probe (liveness/readiness/startup), and it declares resource requests and limits so the scheduler and the OOM killer make correct decisions. Get these three right and 80% of “why is my app broken on Kubernetes?” answers itself from kubectl alone.

Fundamentals

Health probes: liveness, readiness, startup

The kubelet actively probes containers to decide their fate. There are three probe types, each answering a different question, and confusing them causes the most self-inflicted outages on Kubernetes.

ProbeQuestionAction on failureWhen it runs
LivenessIs the process alive, or wedged/deadlocked?Restarts the containerContinuously, after initialDelaySeconds / startup probe
ReadinessCan the process serve traffic right now?Removes the Pod from Service Endpoints (no traffic) — does not restartContinuously, for the Pod’s whole life
StartupHas the app finished booting?Restarts if it never starts; gates liveness/readiness until it passesOnly during startup, then disabled

Effects you must internalize:

Each probe supports three mechanisms: httpGet (2xx/3xx = pass), tcpSocket (can we open the port?), and exec (a command with exit code 0 = pass). grpc probes are also supported natively. Tunables: initialDelaySeconds, periodSeconds, timeoutSeconds, successThreshold, failureThreshold.

# A container with all three probes done correctly.
containers:
  - name: api
    image: myorg/api:1.4.2
    ports:
      - containerPort: 8080
    # Startup: allow up to 30 * 10s = 5 min to boot, then hand off.
    startupProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      failureThreshold: 30
    # Liveness: is the process wedged? Cheap, dependency-free endpoint.
    livenessProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3          # 3 * 10s = 30s of failure before restart
    # Readiness: can I serve? May reflect dependency health.
    readinessProbe:
      httpGet: { path: /ready, port: 8080 }
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3

metrics-server and kubectl top

metrics-server is a lightweight, cluster-wide aggregator of resource metrics (CPU and memory) scraped from each kubelet’s Summary API (backed by cAdvisor). It is not a monitoring system — it keeps only the latest sample in memory, no history — but it powers two critical things: the kubectl top command and the Horizontal Pod Autoscaler (and VPA). Most clusters need it installed; managed offerings often include it.

kubectl top nodes                 # CPU/memory usage per node vs capacity
kubectl top pods -A               # usage per pod across all namespaces
kubectl top pods --containers     # break down by container within pods
kubectl top pod mypod --sort-by=memory

If kubectl top returns “Metrics API not available”, metrics-server is missing or unhealthy (a very common cause on kubeadm/self-managed clusters is the --kubelet-insecure-tls flag being needed for self-signed kubelet certs).

The monitoring stack (Prometheus ecosystem)

For real, historical, alertable monitoring, the de-facto standard is the Prometheus stack, usually installed as a bundle via the kube-prometheus-stack Helm chart:

# ServiceMonitor: the Operator scrapes any Service matching these labels,
# on the port named "metrics", at /metrics — no central config edits.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api
  labels: { release: kube-prometheus-stack }
spec:
  selector:
    matchLabels: { app: api }
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics

Logging on Kubernetes

Kubernetes has no native cluster-wide log storage. The model is:

  1. Containers write to stdout/stderr. The container runtime captures those streams and the kubelet writes them to files on the node (/var/log/pods/...), rotated by the kubelet.
  2. kubectl logs reads those node-local files — which means logs vanish when the Pod is deleted or the node dies, and are lost across container restarts unless you ask for the previous instance.
  3. For durable, searchable, cluster-wide logs you run a node-level agent as a DaemonSet that tails every container’s log files and ships them to a backend.
kubectl logs mypod                       # current container logs
kubectl logs mypod -c sidecar            # a specific container in a multi-container pod
kubectl logs mypod --previous            # logs from the PREVIOUS (crashed) container — vital for CrashLoopBackOff
kubectl logs -f deploy/api               # follow logs of one pod of a Deployment
kubectl logs -l app=api --all-containers --tail=100   # across all pods matching a label

Node-level agents and backends:

The anti-pattern is writing logs to a file inside the container: the node agent tails stdout/stderr, so a log file on the container filesystem is invisible to the collector and lost on restart. Twelve-Factor logging (stdout as an event stream) exists precisely for this.

Kubernetes Events

Events are short-lived (default TTL ~1 hour) records the control plane emits to narrate what’s happening to objects: scheduling decisions, image pulls, probe failures, volume mounts, OOM kills, evictions. They are the single richest, most under-used troubleshooting signal.

kubectl get events --sort-by=.lastTimestamp          # chronological, current namespace
kubectl get events -A --sort-by=.lastTimestamp       # all namespaces
kubectl describe pod mypod                            # Events for THIS pod at the bottom — start here
kubectl get events --field-selector involvedObject.name=mypod,type=Warning

Because Events expire, chasing an intermittent issue means shipping them to your logging backend (Fluent Bit/kube-events exporters can do this) so you retain the history.

Distributed tracing (pointer)

Metrics tell you a service is slow; traces tell you where in a multi-service request the time went. On Kubernetes the standard is OpenTelemetry (OTel): instrument apps with OTel SDKs (or auto-instrumentation), run the OTel Collector as a DaemonSet/Deployment to receive, batch, sample and export spans, and store them in Jaeger or Grafana Tempo. The load-bearing detail is context propagation — the W3C traceparent header must survive every hop (including through Services, ingress, and message queues) or the trace fragments. Tracing is covered in depth in the DevOps observability page; on Kubernetes the key is deploying the Collector and wiring the sidecar/DaemonSet correctly.

Key Concepts

The troubleshooting workflow

A repeatable triage order beats guessing. Start wide, then narrow:

# 1. What's the high-level state? Look for non-Running / non-Ready / restart counts.
kubectl get pods -o wide
kubectl get pods -A | grep -vE 'Running|Completed'

# 2. Why? describe shows Events, probe results, the last state, exit codes, and reasons.
kubectl describe pod <pod>

# 3. What did the app say? Logs — and PREVIOUS logs if it crashed.
kubectl logs <pod>
kubectl logs <pod> --previous

# 4. Poke inside a running container (only if it stays up).
kubectl exec -it <pod> -- sh
kubectl exec <pod> -- env            # verify config/env vars, DNS, mounted secrets

# 5. If the container has no shell / is crashing, attach an ephemeral debug container.
kubectl debug -it <pod> --image=busybox:1.36 --target=<container>

# 6. Cluster-level context.
kubectl get events --sort-by=.lastTimestamp
kubectl top pod <pod> --containers   # is it near its memory/CPU limit?

kubectl debug and ephemeral containers deserve emphasis: modern hardened images are often distroless (no shell, no curl, no package manager), so kubectl exec ... -- sh fails. Ephemeral containers let you attach a fully-tooled debug container that shares the target Pod’s namespaces (process, network) without restarting or modifying the Pod — you can inspect the crashing process’s network and files live. kubectl debug node/<node> gives you a privileged container on a node for node-level debugging.

Common failure signatures

SymptomLikely causesHow to confirmFix direction
CrashLoopBackOffApp exits/crashes on startup; failing liveness probe; bad config/missing env; bad commandkubectl logs --previous; describe shows exit code & restart countRead the previous logs; fix the crash, config, or an over-aggressive liveness probe. BackOff is just the kubelet’s exponential restart delay, not the root cause.
ImagePullBackOff / ErrImagePullWrong image name/tag; private registry without imagePullSecret; registry rate-limit/auth; wrong archdescribe Events show the pull error verbatimFix tag; add/attach imagePullSecret; check registry auth and node network.
Pending / UnschedulableNo node has enough CPU/mem (requests too high); taints without tolerations; nodeSelector/affinity unsatisfiable; no PV for a PVC; node pressuredescribe pod → Events: “0/5 nodes are available: Insufficient cpu…”Lower requests, add nodes/autoscaler capacity, fix taints/affinity, or provision the PVC.
OOMKilledContainer exceeded its memory limitdescribe → Last State: Terminated, Reason: OOMKilled, exit 137; kubectl top near limitRaise the memory limit, fix the leak, or lower workload memory. Note limit ≠ request.
Readiness failing (Pod up, no traffic)/ready returns non-2xx; dependency down; wrong port/path; too-tight timeoutdescribe shows “Readiness probe failed”; Pod is Running but 0/1 READYFix the readiness endpoint, dependency, port, or thresholds. Pod won’t receive Service traffic until it passes.
CreateContainerConfigErrorReferenced ConfigMap/Secret/key doesn’t existdescribe Events name the missing objectCreate the ConfigMap/Secret or fix the reference.
EvictedNode under memory/disk pressure; Pod exceeded ephemeral-storage; low QoS classdescribe node shows pressure conditionsSet requests (raise QoS), clean disk, add capacity.
Node NotReadykubelet down, network/CNI issue, disk/memory pressurekubectl describe node; kubectl get nodesInvestigate kubelet/CNI on the node; cordon/drain if hardware.

Exit code cheatsheet: 0 = clean exit; 1/2 = app error; 137 = SIGKILL (usually OOMKilled or failed liveness kill); 143 = SIGTERM (graceful shutdown); 139 = SIGSEGV.

QoS classes and the OOM killer

A container’s memory limit is enforced by the kernel cgroup: exceed it and the process is OOMKilled (137). The request influences scheduling and the Pod’s QoS class, which decides eviction order under node pressure:

This is why “no limits = fast” is a trap: BestEffort Pods are the first casualties when a node runs hot. Setting sensible requests both keeps the scheduler honest and protects your Pod during pressure.

Reading kubectl describe like an X-ray

The three most diagnostic sections of kubectl describe pod: State / Last State (current status, and how the previous container instance died — exit code and reason, e.g. OOMKilled), Conditions (Ready, PodScheduled, ContainersReady — a False here localizes the failure), and the Events list at the bottom (the chronological narrative — pull errors, probe failures, scheduling messages). Ninety percent of first-line triage is here before you ever look at application logs.

Best Practices

  1. Define liveness, readiness, and startup probes deliberately — they are not interchangeable. Readiness gates traffic; liveness restarts; startup buys boot time. Mixing them up (e.g. a liveness probe that checks a database) turns transient dependency blips into restart storms.
  2. Keep liveness probes cheap and dependency-free. Liveness should answer “is this process wedged?” — never call downstream services from it, or a dependency outage becomes a cascading restart loop across your fleet.
  3. Let readiness reflect the ability to serve, including warmup and dependencies. A Pod that isn’t ready is simply removed from the load balancer, exactly the graceful behavior you want during warmup or a degraded dependency.
  4. Use a startup probe for slow-booting apps instead of a huge liveness initialDelaySeconds. It decouples boot tolerance from steady-state hang detection, so you can be patient at startup and sensitive afterward.
  5. Always set resource requests and (memory) limits. Requests drive scheduling and QoS; a memory limit prevents one leaky Pod from taking down a node. No requests means BestEffort — first to be evicted under pressure.
  6. Log to stdout/stderr as structured JSON, never to a file in the container. The node agent only sees stdout/stderr; file logs are invisible to the collector and lost on restart. Include a trace_id for cross-signal correlation.
  7. Install metrics-server and a real monitoring stack. metrics-server enables kubectl top and autoscaling; kube-prometheus-stack gives historical, alertable metrics. Neither is optional for a production cluster.
  8. Alert on Kubernetes-object state via kube-state-metrics. Alert on “replicas available < desired”, “restarts spiking”, “Pods stuck Pending”, and node pressure — symptoms of workload health, not just raw CPU.
  9. Manage scrape config with the Prometheus Operator’s CRDs. ServiceMonitor/PodMonitor/PrometheusRule make monitoring config declarative, version-controlled, and reviewable instead of a fragile central file.
  10. Ship Events and logs off-node before they expire. Events have a ~1h TTL and container logs die with the Pod; forward both to a durable backend (Loki/Elastic) so post-mortems have the history.
  11. Learn the describe → logs --previous → exec/debug triage order. Start with describe (Events, exit codes, conditions), read the previous logs for crash loops, and only then poke inside. This order resolves most incidents fast.
  12. Master ephemeral debug containers for distroless images. Hardened images have no shell; kubectl debug --target attaches a fully-tooled container sharing the Pod’s namespaces without restarting it — the only way to inspect a crashing distroless Pod live.
  13. Read exit codes as first-class evidence. 137 = OOMKilled or liveness kill, 143 = graceful SIGTERM, 1/2 = app error. The code plus the “Last State” reason usually names the failure class immediately.
  14. Set graceful shutdown (terminationGracePeriodSeconds + SIGTERM handling) and readiness on shutdown. During a rolling update, an app that ignores SIGTERM drops in-flight requests; failing readiness first drains it from the Service before the kill.
  15. Instrument requests with OpenTelemetry and propagate traceparent. Metrics say a service is slow; traces say where. A dropped propagation header silently fragments traces — verify it survives every hop, including ingress and queues.
  16. Right-size probes and thresholds; test failure modes. Too-tight timeoutSeconds/failureThreshold cause flapping and false restarts. Run game-days: kill a dependency, exhaust memory, and verify your probes, alerts, and dashboards behave as intended.

References