Как читать события Kubernetes и быстро находить причину, почему pod не стартует
Каждый раз, когда новый pod в кластере не поднимается, я открываю одно и то же окно терминала и иду по одному и тому же маршруту. За восемь лет работы с k8s у меня накопился короткий чек-лист команд, который покрывает 90% случаев: образ не скачался, не хватило ресурсов, ошибка в манифесте, проблемы с правами, кривой liveness. Здесь я выложу его так, как сам бы хотел увидеть три года назад.
Версия — k8s 1.30, но всё применимо к 1.27+. Команды копируются и работают, никаких <your-cluster>-плейсхолдеров без объяснений.
Куда смотреть в первую очередь
Когда pod в статусе Pending, CrashLoopBackOff, ImagePullBackOff или просто бесконечно ContainerCreating, в 99% случаев причина уже написана в событиях. События — это объекты типа Event, которые kubelet, scheduler и controller-manager сами создают, когда что-то идёт не по плану.
Самый честный способ их посмотреть:
kubectl describe pod <pod-name> -n <namespace>В выводе пролистай вниз, в самом конце будет секция Events:. Это таблица с колонками Type, Reason, Age, From, Message. Именно последние две колонки тебе и нужны: они говорят, кто пожаловался и на что.
Альтернативный вариант — через get events
Если pod уже умер и пересоздаётся под новым именем, describe покажет события только текущей итерации. Чтобы увидеть всё подряд, отсортированно по времени:
kubectl get events -n <namespace> --sort-by=.lastTimestampЯ обычно фильтрую по типу Warning, чтобы не утонуть в шуме:
kubectl get events -n <namespace> \
--field-selector type=Warning \
--sort-by=.lastTimestampСноска: события по умолчанию хранятся около часа (--event-ttl=1h у kube-apiserver). Если pod упал ночью, а ты пришёл утром — событий уже не будет, придётся идти в логи.
Типичные сценарии и что они означают
ImagePullBackOff и ErrImagePull
Самая частая ошибка в новых проектах. Сообщение в Events выглядит так:
Failed to pull image "registry.example.com/api:v1.2.3": rpc error:
code = Unknown desc = failed to pull and unpack image: failed to resolve reference
"registry.example.com/api:v1.2.3": pulling from host registry.example.com failed
with status code [manifests v1.2.3]: 404 Not FoundВозможные причины: тэга реально нет в реджистри, опечатка в имени, нет imagePullSecrets для приватного реджистри, нода в private subnet и не видит реджистри.
Проверка вручную с самой ноды:
kubectl get nodes -o wide
ssh user@<node-ip>
sudo crictl pull registry.example.com/api:v1.2.3Если crictl тоже не тянет — значит проблема не в k8s, а в сети или авторизации.
OOMKilled
Pod в статусе CrashLoopBackOff, в describe в секции про последний exit видно Reason: OOMKilled и Exit Code: 137. Это значит, контейнер съел больше памяти, чем указано в resources.limits.memory, и kernel его убил.
Что делать: либо повысить лимит, либо чинить утечку в коде. Для Go-приложений я ставлю GOMEMLIMIT на 90% от лимита pod, чтобы GC начал работать раньше OOMKilled. Java и .NET аналогично — указывайте лимиты в JVM/CLR, не надейтесь на k8s.
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
env:
- name: GOMEMLIMIT
value: "460MiB"CreateContainerConfigError
Pod создан, но не стартует. В Events что-то про secret "db-creds" not found или configmap "app-config" not found. Значит, манифест ссылается на ресурс, которого нет в нэймспейсе.
kubectl get secrets,configmaps -n <namespace>В CI это часто случается, когда секрет деплоится отдельным джобом, а pod стартует раньше. Лечится зависимостями в пайплайне или ArgoCD sync waves.
FailedScheduling
Pod в Pending, в Events:
0/5 nodes are available: 3 Insufficient memory, 2 node(s) had untolerated taintscheduler честно пишет, по какой ноде что не сошлось. Считай: если у тебя три ноды без памяти и две с taint — добавляй ноды или меняй tolerations. Часто помогает посмотреть, кто съедает ресурсы:
kubectl top nodes
kubectl describe node <node-name> | grep -A 5 "Allocated resources"Liveness probe failed
Pod стартует, через минуту падает. В Events:
Liveness probe failed: HTTP probe failed with statuscode: 503Тут два варианта: приложение реально не отвечает, или liveness слишком жёсткий. Проверь сам:
kubectl exec -it <pod> -n <ns> -- sh
# внутри контейнера:
curl -v http://localhost:8080/healthzЕсли эндпоинт отвечает руками, но liveness падает — увеличь initialDelaySeconds, periodSeconds или используй startupProbe для медленных стартов. Подробнее про probes — это отдельная тема, но 80% проблем решается тем, что startupProbe вообще задаётся.
Когда событий нет, а pod всё равно сломан
Бывает, события закончились (TTL), либо pod умирает где-то совсем рано. Тогда идём в логи:
kubectl logs <pod> -n <ns>
kubectl logs <pod> -n <ns> --previous # логи прошлой жизни контейнера
kubectl logs <pod> -n <ns> -c <init-container> # для init-контейнеровДля логов прошлой жизни флаг --previous работает только если контейнер не пересоздавался полностью. Если pod был удалён и создан заново — previous уже не поможет, нужны централизованные логи (Loki, ELK, что у вас стоит).
Логи самого kubelet
Если совсем плохо и pod не запускается без событий, нужно лезть на ноду:
ssh user@<node-ip>
sudo journalctl -u kubelet -f
sudo journalctl -u containerd -fВ journalctl -u kubelet видно, например, как kubelet пытается смонтировать volume и валится с permission denied или mount failed. Из describe pod такие вещи иногда не видно сразу.
Полезные алиасы и команды на каждый день
В моём ~/.zshrc лежат вот эти алиасы — экономят минуты в день:
alias k='kubectl'
alias kgp='kubectl get pods -o wide'
alias kgpa='kubectl get pods -A -o wide'
alias kge='kubectl get events --sort-by=.lastTimestamp'
alias kdp='kubectl describe pod'
alias klf='kubectl logs -f --tail=100'
kbroken() {
kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded
}Функция kbroken показывает все pods, которые не Running и не Succeeded — то есть всех текущих больных. Удобно по утрам пробежать глазами по кластеру.
Маршрут диагностики, который я повторяю всегда
Когда мне в чат пишут «у нас pod не работает», я делаю по шагам:
kubectl get pod <pod> -n <ns>— текущее состояние, restart count, age.kubectl describe pod <pod> -n <ns>— секция Events, статус контейнеров, exit code последнего раза.kubectl logs <pod> -n <ns> --previous— логи прошлой попытки, если есть.kubectl get events -n <ns> --sort-by=.lastTimestamp— события всего нэймспейса за час.kubectl top pod <pod> -n <ns>— по памяти/CPU, если жив.- Если ничего —
journalctl -u kubeletна ноде.
На пунктах 1–3 закрывается процентов 70 кейсов, на 4–6 — ещё 25. Остаётся 5% случаев, когда у тебя сломан CNI, сетевые политики бьют конкретный pod или CSI не умеет смонтировать том. Это уже отдельный квест.
Что запомнить
Не угадывай причину. Открывай describe pod и читай Events — k8s сам пишет, что не так, нужно только не пропустить мимо глаз. Если события закончились — переходи на логи, логи прошлой жизни и логи kubelet. И держи под рукой kbroken-функцию: один взгляд утром экономит часы вечером.
Куда копать дальше: официальный kubectl cheat sheet, страница про debug pods. Там же есть про kubectl debug — отдельный инструмент для случаев, когда в контейнере вообще нет shell. Про debug я как-нибудь напишу отдельно.