Learning Platform
Глоссарий Troubleshooting
Урок 14.05 · 22 мин
Продвинутый
OOMKilledPendingEvictedkubectl-topmetrics-servertroubleshooting

Troubleshooting: OOMKilled, Pending, Evicted

В реальной работе resource-related инциденты происходят постоянно: Pod не стартует (Pending), Pod падает с loop (OOMKilled), Pod внезапно исчез (Evicted). На CKAD задачи на отладку — половина баллов. Этот урок про последовательность диагностики каждого случая: где смотреть, какие команды запускать, как читать вывод.


Disk emergency: что делать когда диск 100%

OOMKilled: полный flow

Когда Pod рестартует с OOMKilled, последовательность событий такая:

OOMKilled: путь от malloc до restart
app процессApplication делает обычный allocation: malloc в C, new Object() в Java, make([]byte, ...) в Go.
cgroup memory.maxkubelet через CRI настроил cgroup container с memory.max = limits.memory. Это hard cap на RSS + page cache (учитывается по-разному в зависимости от cgroup версии).
kernel oom_killKernel внутри этой cgroup выбирает victim (process с самым большим oom_score, обычно сам app). Отправляет SIGKILL — невозможно перехватить, никакого finally/defer.
container exit 137128 + signal 9 = 137. Стандартный exit code для OOMKilled. Контейнер завершён.
kubelet detectkubelet через CRI получает stop event, читает cgroup OOM counter, проставляет container Terminated с reason=OOMKilled. Это видно в kubectl describe pod как Last State.
restart по policyrestartPolicy=Always (default для Deployment) — kubelet рестартует с exponential backoff: 10s, 20s, 40s, ... до 5 min. При повторных OOMKilled — CrashLoopBackOff.

Диагностика:

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 — кеш заполняется и считается в лимит. Это редко, но встречается.

DANGER

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 — конкретная причина. Возможные сценарии:

Pending Pod: причины и проверки
Insufficient cpu/memoryНи на одном node нет места под requests Pod. Проверить: kubectl describe nodes — секции Allocatable и Allocated resources. Если все nodes заполнены — нужен новый node или уменьшение requests.
taints без tolerationВсе nodes имеют taint, который Pod не толерирует. kubectl get nodes -o jsonpath='{.items[*].spec.taints}'. Решение: добавить tolerations или убрать taint.
nodeSelector/affinity не matchитсяspec.nodeSelector или nodeAffinity указывает на labels, которых нет ни на одном node. kubectl get nodes --show-labels — посмотреть, что реально есть.
PVC pendingPod ждёт PVC, который сам Pending (нет PV, StorageClass не provisions). kubectl get pvc — статус. Pod не запустится пока volume не bound.
ResourceQuota exceededВ namespace ResourceQuota забит. Error from controller, но Pod в Pending. kubectl describe quota — посмотреть what's full.
PodSecurityStandard violationPod-spec нарушает enforced PSA уровень (restricted/baseline). kubectl describe pod покажет admission error.

Команды для диагностики

# Какие 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 — доступная память на node
  • nodefs.available — место на node filesystem (где kubelet хранит state)
  • nodefs.inodesFree — свободные inodes
  • imagefs.available — место для container images
  • pid.available — свободные process IDs

Soft vs hard eviction

Soft и hard eviction
soft evictionkubelet видит memory.available < threshold. Запускается grace period (соответствует pod terminationGracePeriodSeconds). За это время Pod получает SIGTERM, успевает закрыть соединения, сохранить state. После grace — SIGKILL.
hard evictionmemory.available уже критично низкий (default ~100Mi). Нет grace — kubelet делает SIGKILL сразу. Данные могут быть потеряны. Это защита самого node от падения.

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:

  1. BestEffort — первыми, среди них выбирается по memory usage (наибольший).
  2. Burstable превысившие memory request — следующими, по (usage - request) (наибольший первым).
  3. 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.

WARNING

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 и под секундомер.


Проверка знанийKnowledge check
Pod падает с CrashLoopBackOff. В describe Last State: Terminated, Reason: OOMKilled, Exit Code: 137. Что это значит и куда смотреть?
ОтветAnswer
Container превысил memory limit, kernel OOM killer убил процесс (SIGKILL → exit 128+9=137), kubelet рестартует, повторяется — CrashLoopBackOff. Диагностика: kubectl top pod чтобы увидеть реальное использование, kubectl get pod -o yaml чтобы посмотреть текущий limit. Решение: поднять limit, искать memory leak (pprof/heap dump), тюнить GC. SIGKILL не перехватывается — никакого graceful shutdown не было.
Проверка знанийKnowledge check
Pod в Pending status уже 5 минут. Как диагностировать?
ОтветAnswer
kubectl describe pod <name>, смотреть секцию Events в конце. Реальная причина в Reason=FailedScheduling и Message. Дальше по Message: 'Insufficient memory' → kubectl describe nodes; 'taints did not tolerate' → kubectl get nodes -o ...spec.taints; 'didn't match nodeSelector' → kubectl get nodes --show-labels; 'no PV available' → kubectl get pvc; 'exceeded quota' → kubectl describe quota.
Проверка знанийKnowledge check
В чём разница между OOMKilled и Evicted?
ОтветAnswer
OOMKilled — решение kernel на уровне cgroup конкретного container, когда процесс упёрся в его memory.max. Container exit 137, Pod помечается Terminated reason=OOMKilled, рестарт по policy. Evicted — решение kubelet на уровне node, когда у самого node заканчивается memory/disk. Pod помечается Failed reason=Evicted, controller пересоздаёт replica на другом node. OOM — про лимит одного container, Evict — про здоровье всего node. Eviction может произойти и для Pod, который сам в пределах своего лимита.
Проверка знанийKnowledge check
kubectl top pod выдаёт 'error: Metrics API not available'. В чём дело?
ОтветAnswer
В кластере не установлен metrics-server (это отдельный компонент, не входит в core Kubernetes). kubectl top опрашивает metrics.k8s.io/v1beta1 API, который реализует именно metrics-server через APIService aggregation. Решение: kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml (на kubeadm/самосборных кластерах ещё может понадобиться --kubelet-insecure-tls). Managed (EKS/GKE/AKS) часто имеет metrics-server из коробки.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Pod в CrashLoopBackOff. kubectl describe pod показывает Last State Terminated, Reason: OOMKilled, Exit Code 137. Что это значит и куда смотреть?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5