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.
| Probe | Câu hỏi | Hành động khi fail | Khi nào chạy |
|---|---|---|---|
| Liveness | Process có còn sống không, hay bị treo/deadlock? | Restart container | Liên tục, sau initialDelaySeconds / startup probe |
| Readiness | Process 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 restart | Liên tục, suốt vòng đời Pod |
| Startup | App đã boot xong chưa? | Restart nếu không bao giờ khởi động; chặn liveness/readiness cho đến khi pass | Chỉ lúc khởi động, sau đó tắt |
Các hệ quả bạn phải nằm lòng:
- Một fail liveness là cái búa: nó kill và restart container. Nếu liveness probe kiểm tra thứ phụ thuộc vào một dependency chậm (một database), một cú chập chờn nhất thời của DB kích hoạt bão restart — bạn biến một sự cố nhỏ thành một outage. Liveness chỉ nên test bản thân process (nó có phản hồi được chút nào không?), không bao giờ test các dependency phía sau.
- Một fail readiness thì nhẹ nhàng: Pod vẫn chạy nhưng bị gỡ khỏi vòng quay load-balancer của Service, nên không traffic người dùng nào tới nó. Đây chính là điều bạn muốn khi một dependency chậm hoặc khi đang warmup — Pod chờ, không bị kill, và quay lại khi khỏe. Readiness có thể kiểm tra dependency (tôi có nên nhận traffic khi DB không tới được không?).
- Một startup probe tồn tại cho các app boot chậm (JVM cũ, app nạp model lớn). Không có nó, bạn buộc phải đặt
initialDelaySecondskhổng lồ cho liveness, điều này cũng làm chậm việc phát hiện một cú treo thật. Startup probe cho phép nói “cho app này tối đa 5 phút để boot, nhưng khi đã boot xong thì check liveness mỗi 10 giây” — tách sự khoan dung lúc boot khỏi độ nhạy ở trạng thái ổn định.
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 top và Horizontal 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:
- Prometheus — metrics server theo mô hình pull, scrape các endpoint
/metricsđược phát hiện qua Kubernetes service discovery. Lưu time series cục bộ; remote-write sang Thanos/Mimir/Cortex để lưu trữ dài hạn, HA. - kube-state-metrics (KSM) — expose trạng thái của các object Kubernetes thành metric:
kube_pod_status_phase,kube_deployment_status_replicas_available,kube_pod_container_status_restarts_total, v.v. Đây là cách bạn alert “một Deployment có ít replica ready hơn mong muốn” hay “một Pod restart 5 lần trong 10 phút”. KSM mô tả object; nó khác với metrics-server (đo mức sử dụng tài nguyên). - node-exporter — một DaemonSet expose metric cấp host (CPU, memory, disk, filesystem, network) mỗi node.
- cAdvisor — tích hợp sẵn trong kubelet; expose metric tài nguyên theo container (
container_cpu_usage_seconds_total,container_memory_working_set_bytes). Prometheus scrape nó qua kubelet. - Grafana — dashboard trên Prometheus (và Loki/Tempo). kube-prometheus-stack đóng gói sẵn các dashboard cluster/node/workload đã tuyển chọn.
- Prometheus Operator — một controller quản lý Prometheus, Alertmanager và cấu hình scrape qua CRD:
ServiceMonitorvàPodMonitor(khai báo scrape cái gì qua label selector, thay vì sửa một file config tĩnh khổng lồ),PrometheusRule(rule alert/recording), vàAlertmanager(routing). Điều này biến config monitoring thành các object Kubernetes version-controlled.
# 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à:
- 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. 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 đó.- Để 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:
- Fluent Bit — bộ xử lý/forward log nhẹ viết bằng C, lựa chọn DaemonSet thông thường; footprint thấp.
- Fluentd — collector Ruby nặng hơn, giàu plugin (CNCF, cùng nhà).
- Vector — một lựa chọn thay thế hiện đại, hiệu năng cao.
- Backend: Loki (index theo label, rẻ, ghép với Grafana và LogQL), stack ELK/Elastic hoặc OpenSearch (full-text search, mạnh nhưng nặng hơn), hoặc dịch vụ managed (CloudWatch, Cloud Logging).
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ứng | Nguyên nhân khả dĩ | Cách xác nhận | Hướng khắc phục |
|---|---|---|---|
| CrashLoopBackOff | App exit/crash lúc khởi động; liveness probe fail; config sai/thiếu env; command sai | kubectl 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 / ErrImagePull | Sai tên/tag image; registry private không có imagePullSecret; rate-limit/auth registry; sai kiến trúc | describe hiện lỗi pull nguyên văn trong Events | Sửa tag; thêm/gắn imagePullSecret; kiểm tra auth registry và network của node. |
| Pending / Unschedulable | Khô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 pressure | describe 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. |
| OOMKilled | Container vượt limit memory | describe → Last State: Terminated, Reason: OOMKilled, exit 137; kubectl top gần limit | Tă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ắt | describe hiện “Readiness probe failed”; Pod Running nhưng 0/1 READY | Sửa readiness endpoint, dependency, port, hoặc threshold. Pod không nhận traffic Service cho tới khi pass. |
| CreateContainerConfigError | ConfigMap/Secret/key được tham chiếu không tồn tại | Events trong describe nêu tên object thiếu | Tạo ConfigMap/Secret hoặc sửa tham chiếu. |
| Evicted | Node bị memory/disk pressure; Pod vượt ephemeral-storage; QoS class thấp | describe node hiện các điều kiện pressure | Đặt requests (nâng QoS), dọn disk, thêm capacity. |
| Node NotReady | kubelet chết, sự cố network/CNI, disk/memory pressure | kubectl 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:
- Guaranteed — mọi container có requests == limits cho cả CPU lẫn memory. Bị evict cuối cùng.
- Burstable — có ít nhất một request được đặt, nhưng không bằng limits. Bị evict sau BestEffort.
- BestEffort — hoàn toàn không có requests/limits. Bị evict đầu tiên khi 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
- Đị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.
- 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.
- Để 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.
- Dùng startup probe cho app boot chậm thay vì
initialDelaySecondskhổ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. - 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.
- 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. - Cài metrics-server và một monitoring stack thật. metrics-server bật
kubectl topvà 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. - 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ô.
- Quản lý config scrape bằng CRD của Prometheus Operator.
ServiceMonitor/PodMonitor/PrometheusRulekhiến config monitoring declarative, version-controlled và review được thay vì một file trung tâm dễ vỡ. - 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ử.
- Học thứ tự triage
describe → logs --previous → exec/debug. Bắt đầu vớidescribe(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. - Thành thạo ephemeral debug container cho image distroless. Image được hardening không có shell;
kubectl debug --targetgắ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. - Đọ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.
- 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. - 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. - Chỉnh đúng kích cỡ probe và threshold; test các failure mode.
timeoutSeconds/failureThresholdquá 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
- roadmap.sh — Kubernetes Roadmap
- Kubernetes — Configure Liveness, Readiness and Startup Probes
- Kubernetes — Pod Lifecycle (probes, restart policy, conditions)
- Kubernetes — Troubleshooting Applications
- Kubernetes — Debug Running Pods & Ephemeral Containers
- Kubernetes — Debug a Pod với
kubectl debug - Kubernetes — Logging Architecture
- Kubernetes — Resource Management & QoS Classes
- Kubernetes — Tools for Monitoring Resources (Metrics Pipelines)
- metrics-server (GitHub)
- Kubernetes —
kubectl topreference - Prometheus documentation · kube-state-metrics · node_exporter
- Prometheus Operator documentation · kube-prometheus-stack Helm chart
- Grafana documentation · Grafana Loki
- Fluent Bit for Kubernetes · Fluentd
- OpenTelemetry — Kubernetes · Jaeger · Grafana Tempo
- Google SRE Book — Monitoring Distributed Systems
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.
| Probe | Question | Action on failure | When it runs |
|---|---|---|---|
| Liveness | Is the process alive, or wedged/deadlocked? | Restarts the container | Continuously, after initialDelaySeconds / startup probe |
| Readiness | Can the process serve traffic right now? | Removes the Pod from Service Endpoints (no traffic) — does not restart | Continuously, for the Pod’s whole life |
| Startup | Has the app finished booting? | Restarts if it never starts; gates liveness/readiness until it passes | Only during startup, then disabled |
Effects you must internalize:
- A liveness failure is a hammer: it kills and restarts the container. If your liveness probe checks something that depends on a slow dependency (a database), a transient DB blip triggers a restart storm — you’ve turned a minor issue into an outage. Liveness should test only the process itself (can it respond at all?), never downstream dependencies.
- A readiness failure is graceful: the Pod stays running but is pulled out of the Service load-balancer rotation, so no user traffic reaches it. This is exactly what you want during a slow dependency or a warmup — the Pod waits, doesn’t get killed, and rejoins when healthy. Readiness may check dependencies (should I receive traffic if my DB is unreachable?).
- A startup probe exists for slow-booting apps (legacy JVMs, apps loading large models). Without it, you’re forced to set a huge
initialDelaySecondson liveness, which also delays detecting a real hang. The startup probe lets you say “give this app up to 5 minutes to boot, but once booted, check liveness every 10 seconds” — decoupling boot tolerance from steady-state sensitivity.
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:
- Prometheus — pull-based metrics server that scrapes
/metricsendpoints discovered via Kubernetes service discovery. Stores time series locally; remote-writes to Thanos/Mimir/Cortex for long-term, HA storage. - kube-state-metrics (KSM) — exposes the state of Kubernetes objects as metrics:
kube_pod_status_phase,kube_deployment_status_replicas_available,kube_pod_container_status_restarts_total, etc. This is how you alert on “a Deployment has fewer ready replicas than desired” or “a Pod restarted 5 times in 10 minutes”. KSM describes objects; it is distinct from metrics-server (which measures resource usage). - node-exporter — a DaemonSet exposing host-level metrics (CPU, memory, disk, filesystem, network) per node.
- cAdvisor — built into the kubelet; exposes per-container resource metrics (
container_cpu_usage_seconds_total,container_memory_working_set_bytes). Prometheus scrapes it via the kubelet. - Grafana — dashboards over Prometheus (and Loki/Tempo). The kube-prometheus-stack ships curated cluster/node/workload dashboards out of the box.
- Prometheus Operator — a controller that manages Prometheus, Alertmanager, and scrape config through CRDs:
ServiceMonitorandPodMonitor(declare what to scrape via label selectors, instead of editing a giant static config),PrometheusRule(alerting/recording rules), andAlertmanager(routing). This turns monitoring config into version-controlled Kubernetes objects.
# 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:
- 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. kubectl logsreads 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.- 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:
- Fluent Bit — lightweight C log processor/forwarder, the usual DaemonSet choice; low footprint.
- Fluentd — heavier, plugin-rich Ruby collector (CNCF, same family).
- Vector — a modern high-performance alternative.
- Backends: Loki (label-indexed, cheap, pairs with Grafana and LogQL), the ELK/Elastic or OpenSearch stack (full-text search, powerful but heavier), or a managed service (CloudWatch, Cloud Logging).
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
| Symptom | Likely causes | How to confirm | Fix direction |
|---|---|---|---|
| CrashLoopBackOff | App exits/crashes on startup; failing liveness probe; bad config/missing env; bad command | kubectl logs --previous; describe shows exit code & restart count | Read 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 / ErrImagePull | Wrong image name/tag; private registry without imagePullSecret; registry rate-limit/auth; wrong arch | describe Events show the pull error verbatim | Fix tag; add/attach imagePullSecret; check registry auth and node network. |
| Pending / Unschedulable | No node has enough CPU/mem (requests too high); taints without tolerations; nodeSelector/affinity unsatisfiable; no PV for a PVC; node pressure | describe pod → Events: “0/5 nodes are available: Insufficient cpu…” | Lower requests, add nodes/autoscaler capacity, fix taints/affinity, or provision the PVC. |
| OOMKilled | Container exceeded its memory limit | describe → Last State: Terminated, Reason: OOMKilled, exit 137; kubectl top near limit | Raise 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 timeout | describe shows “Readiness probe failed”; Pod is Running but 0/1 READY | Fix the readiness endpoint, dependency, port, or thresholds. Pod won’t receive Service traffic until it passes. |
| CreateContainerConfigError | Referenced ConfigMap/Secret/key doesn’t exist | describe Events name the missing object | Create the ConfigMap/Secret or fix the reference. |
| Evicted | Node under memory/disk pressure; Pod exceeded ephemeral-storage; low QoS class | describe node shows pressure conditions | Set requests (raise QoS), clean disk, add capacity. |
| Node NotReady | kubelet down, network/CNI issue, disk/memory pressure | kubectl describe node; kubectl get nodes | Investigate 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:
- Guaranteed — every container has requests == limits for both CPU and memory. Last to be evicted.
- Burstable — at least one request set, but not equal to limits. Evicted after BestEffort.
- BestEffort — no requests/limits at all. First to be evicted under 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
- 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.
- 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.
- 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.
- 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. - 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.
- 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_idfor cross-signal correlation. - Install metrics-server and a real monitoring stack. metrics-server enables
kubectl topand autoscaling; kube-prometheus-stack gives historical, alertable metrics. Neither is optional for a production cluster. - 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.
- Manage scrape config with the Prometheus Operator’s CRDs.
ServiceMonitor/PodMonitor/PrometheusRulemake monitoring config declarative, version-controlled, and reviewable instead of a fragile central file. - 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.
- Learn the
describe → logs --previous → exec/debugtriage order. Start withdescribe(Events, exit codes, conditions), read the previous logs for crash loops, and only then poke inside. This order resolves most incidents fast. - Master ephemeral debug containers for distroless images. Hardened images have no shell;
kubectl debug --targetattaches a fully-tooled container sharing the Pod’s namespaces without restarting it — the only way to inspect a crashing distroless Pod live. - 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.
- 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. - 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. - Right-size probes and thresholds; test failure modes. Too-tight
timeoutSeconds/failureThresholdcause flapping and false restarts. Run game-days: kill a dependency, exhaust memory, and verify your probes, alerts, and dashboards behave as intended.
References
- roadmap.sh — Kubernetes Roadmap
- Kubernetes — Configure Liveness, Readiness and Startup Probes
- Kubernetes — Pod Lifecycle (probes, restart policy, conditions)
- Kubernetes — Troubleshooting Applications
- Kubernetes — Debug Running Pods & Ephemeral Containers
- Kubernetes — Debug a Pod with
kubectl debug - Kubernetes — Logging Architecture
- Kubernetes — Resource Management & QoS Classes
- Kubernetes — Tools for Monitoring Resources (Metrics Pipelines)
- metrics-server (GitHub)
- Kubernetes —
kubectl topreference - Prometheus documentation · kube-state-metrics · node_exporter
- Prometheus Operator documentation · kube-prometheus-stack Helm chart
- Grafana documentation · Grafana Loki
- Fluent Bit for Kubernetes · Fluentd
- OpenTelemetry — Kubernetes · Jaeger · Grafana Tempo
- Google SRE Book — Monitoring Distributed Systems