Troubleshooting: OOMKilled, Pending, Evicted
В реальной работе resource-related инциденты происходят постоянно: Pod не стартует (Pending), Pod падает с loop (OOMKilled), Pod внезапно исчез (Evicted). На CKAD задачи на отладку — половина баллов. Этот урок про последовательность диагностики каждого случая: где смотреть, какие команды запускать, как читать вывод.
Disk emergency: что делать когда диск 100%
OOMKilled: полный flow
Когда Pod рестартует с OOMKilled, последовательность событий такая:
Диагностика:
kubectl describe pod <name>
# ... в выводе:
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# Started: ...
# Finished: ...
# Restart Count: 5
Подробнее (включая memory limit):
kubectl get pod <name> -o jsonpath='{.spec.containers[*].resources}'
# {"limits":{"memory":"256Mi"},"requests":{"memory":"128Mi"}}
И реальный usage:
kubectl top pod <name>
# NAME CPU(cores) MEMORY(bytes)
# app 250m 245Mi # видно, что upor в 256Mi
Стратегии лечения
1. Поднять memory limit. Самое быстрое. Работает если приложение объективно требует больше памяти (тяжёлые операции, кеш).
2. Найти memory leak. Если usage растёт со временем без причины — это leak. Профилировщики:
- Go:
net/http/pprof,go tool pprof http://pod:6060/debug/pprof/heap - Java:
jcmd <pid> GC.heap_dump, потом анализ в Eclipse MAT - Python:
tracemalloc,objgraph,memory_profiler - Node.js:
node --inspect, heap snapshot в Chrome DevTools
3. Тюнинг runtime. Java особенно: -XX:MaxRAMPercentage=75 чтобы JVM сам подсчитал heap из cgroup. Без этого JVM на старых версиях видит host memory, не container, и стартует с heap, который не помещается.
4. Проверить, не считается ли page cache в RSS. На cgroup v2 (modern kernels, 1.25+) memory.current включает file cache, и Pod, читающий много с диска, может OOMKilled даже без leak — кеш заполняется и считается в лимит. Это редко, но встречается.
SIGKILL не перехватывается. Это означает: НЕТ graceful shutdown, НЕТ финализаторов в Java, НЕТ defer в Go, НЕТ flush буферов БД. Если приложение пишет batched data — данные теряются. Поэтому OOMKilled в production — это всегда событие, требующее анализа, не просто «рестартанулся».
Pending Pod: почему не стартует
Pod в state Pending — scheduler ещё не назначил node, либо kubelet не начал запуск. Pending штатно длится секунды; если дольше — что-то не так.
Поток диагностики:
kubectl describe pod <name>
# ... в самом низу:
# Events:
# Type Reason Age From Message
# Warning FailedScheduling 10s default-scheduler 0/3 nodes are available:
# 3 Insufficient memory.
Reason FailedScheduling и в Message — конкретная причина. Возможные сценарии:
Команды для диагностики
# Какие resources на nodes
kubectl describe node <node-name>
# Allocatable:
# cpu: 4
# memory: 7960Mi
# Allocated resources:
# Resource Requests Limits
# cpu 3800m (95%) 8 (200%)
# memory 7500Mi (94%) ...
Если Allocated requests близко к Allocatable — node заполнен, scheduler не найдёт места.
# Все pending Pods и их события
kubectl get pods --field-selector=status.phase=Pending -A
kubectl describe pod <pending-pod> | tail -20
# Проверить taints
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
# Quota в namespace
kubectl describe quota -n <ns>
Node-level eviction
Eviction — это отдельный механизм, не связанный с OOMKilled или ResourceQuota. Kubelet мониторит сигналы на node и сам решает выселять Pods, когда ресурсы у самого node заканчиваются.
Eviction signals
memory.available— доступная память на nodenodefs.available— место на node filesystem (где kubelet хранит state)nodefs.inodesFree— свободные inodesimagefs.available— место для container imagespid.available— свободные process IDs
Soft vs hard eviction
Defaults (могут отличаться, проверить через kubectl describe node или kubelet config):
hard:
memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
imagefs.available: 15%
soft:
(по умолчанию пусто, настраивается админом)
Кто evict-ится
Eviction order — по QoS и usage:
- BestEffort — первыми, среди них выбирается по memory usage (наибольший).
- Burstable превысившие memory request — следующими, по
(usage - request)(наибольший первым). - Guaranteed и Burstable в пределах request — последними, только если выше не хватило.
Pod помечается Failed, reason Evicted. Через kubectl describe pod:
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory.
Container app was using 245Mi, request is 128Mi
Controller (Deployment/RS) пересоздаёт replica — но уже на другом node, не на проблемном. Это и есть штатное self-healing поведение Kubernetes.
Evicted Pods НЕ удаляются автоматически — они остаются в Failed state для observability. Если в namespace накопились сотни Evicted Pods, kubectl get pods становится медленным. Уборка: kubectl delete pods —field-selector=status.phase=Failed.
kubectl top: реальное потребление
kubectl top показывает текущее CPU/memory usage. Требует установленного metrics-server (его нет из коробки в kubeadm и vanilla Kubernetes; есть в managed: EKS, GKE, AKS — но иногда отключён).
kubectl top nodes
# NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
# node-1 2500m 62% 6Gi 75%
kubectl top pod -n <ns>
# NAME CPU(cores) MEMORY(bytes)
# app-1 250m 245Mi
# app-2 1m 50Mi
kubectl top pod -n <ns> --containers
# показывает по containers внутри Pod
Если kubectl top возвращает error: Metrics API not available — metrics-server не установлен.
Установка (для CKAD-кластера и для тестов):
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
В kubeadm-кластерах часто нужен флаг --kubelet-insecure-tls в deployment metrics-server (kubelet самоподписан).
Killer момент: типовой CKAD scenario «диагностируйте OOMKilled»
Условие: «В namespace prod Deployment api рестартует. Диагностируйте и почините.»
Шаги:
# 1. Посмотреть Pods
kubectl get pods -n prod
# api-xyz 0/1 CrashLoopBackOff 5 3m
# 2. Описать Pod
kubectl describe pod -n prod api-xyz
# ... Last State: Terminated, Reason: OOMKilled, Exit Code: 137
# Limits: memory: 128Mi
# 3. Реальное потребление
kubectl top pod -n prod api-xyz --containers
# api 250m 245Mi
# 4. Вывод: limit 128Mi, реально нужно ~250Mi. Поднять до 512Mi (с запасом).
# 5. Патч (для Deployment)
kubectl set resources deployment/api -n prod \
--limits=memory=512Mi --requests=memory=256Mi
# 6. Дождаться нового pod, проверить
kubectl get pod -n prod -w
kubectl describe pod -n prod <new-pod> | grep Last
На реальном CKAD — то же самое, но в TUI и под секундомер.