kubectl describe pod часто вспоминают слишком поздно. Уже сходили в логи, пролистали Helm values, спросили в чате «кто деплоил?», на всякий случай дропнули Pod — и только потом внизу describe нашли ту самую строку: образ не скачался, памяти не хватило, Secret не примонтировался или readiness-проба честно возвращала 500. Эта команда не заменяет логи, метрики и трейсинг. Она про другое: показать, что Kubernetes пытался сделать с Pod’ом и на каком шаге всё развалилось. Если читать describe не как длинную простыню полей, а как историю жизни Pod’а, он экономит не минуты, а часы.
Если вам нужна не вся теория, а быстрая шпаргалка для инцидента — в конце статьи есть компактная схема: что смотреть в kubectl describe pod при Pending, CrashLoopBackOff, ImagePullBackOff, OOMKilled, FailedMount и других типовых состояниях. Можно сразу перейти к ней, сохранить и использовать как чек-лист. А если хочется понять не только «куда смотреть», но и почему Kubernetes ведёт себя именно так — дальше разберём describe вместе по шагам.
Есть команды, которые выглядят слишком простыми, чтобы относиться к ним серьёзно. kubectl get pods показывает короткий статус. kubectl logs показывает, что говорит приложение. kubectl exec позволяет залезть внутрь контейнера, если он вообще жив. А kubectl describe pod стоит где-то между ними: вроде бы не логи, не YAML, не метрики, а просто «описание».
Именно поэтому её часто недооценивают.
В реальном дебаге всё обычно начинается с чего-то такого:
kubectl get pods -n payments
NAME READY STATUS RESTARTS AGEapi-7c9d7c9c6b-kx2sq 0/1 CrashLoopBackOff 8 12mworker-5d7b9f66fb-jm4cp 0/1 Pending 0 9mgateway-58d79c8f45-pz9nb 1/1 Running 0 31m
На этом месте легко поймать первую ловушку: принять колонку STATUS за диагноз. Видим Pending — и думаем, что контейнер просто долго стартует, хотя очень часто он вообще ещё не запускался: Pod мог не пройти шедулинг из-за requests, taints, node affinity, quota или PVC. Видим CrashLoopBackOff — и звучит так, будто «Kubernetes опять что-то сломал», хотя чаще приложение само завершается, а kubelet просто перезапускает его и делает паузы между попытками. Видим Running — и становится спокойнее, но зря: контейнер может быть запущен, при этом не быть ready, не получать трафик, падать по probe, висеть в deadlock или отвечать только на один технический endpoint.
Есть ещё одна тонкость, которую лучше понять пораньше: STATUS в kubectl get pods — это удобная человекочитаемая колонка, а не всегда строгая «фаза Pod’а». Формально у Pod есть фазы вроде Pending, Running, Succeeded, Failed. Но в таблице kubectl get pods Kubernetes часто показывает не фазу, а более полезную причину из состояния контейнеров: CrashLoopBackOff, ImagePullBackOff, ErrImagePull, CreateContainerConfigError. Для быстрого взгляда это удобно. Для дебага — коварно: кажется, что диагноз уже есть, хотя на самом деле перед нами только табличка на двери. describe нужен как раз здесь — когда симптом уже виден, но ещё непонятно, где именно сломался механизм.
kubectl describe pod api-7c9d7c9c6b-kx2sq -n payments
Вывод describe может немного отличаться от кластера к кластеру: версия Kubernetes, container runtime, облако, включённые feature gates, admission webhooks, платформенные настройки — всё это оставляет свои следы. Но принцип чтения не меняется. Pod в Kubernetes не просто «запускается», как обычный процесс на сервере. Сначала его создаёт контроллер, потом scheduler подбирает ноду, kubelet на этой ноде получает задачу, runtime пытается скачать образ, подключаются volumes, собираются переменные окружения, отрабатывают init-контейнеры, потом стартуют основные контейнеры, включаются probes, и только после этого Pod становится ready — или не становится. Почти на каждом шаге Kubernetes что-то фиксирует: где поставил, что не скачал, что не смонтировал, какой контейнер упал, какая проверка не прошла. kubectl describe pod как раз собирает эти следы в одном месте. Проблема в том, что внешне это выглядит как большая простыня текста, и читать её сверху вниз, особенно поначалу, не очень приятно и не всегда полезно.
Я обычно читаю describe по маршруту:
1. Это точно нужный Pod и нужный namespace?2. Кто им управляет?3. Назначен ли он на Node?4. Что с init containers?5. Что с основными контейнерами: State, Last State, Ready, Restart Count, Image, Requests/Limits, probes?6. Что говорят Conditions?7. Что с volumes, mounts, env, service account, tolerations?8. Что написано в Events?
Если Pod не назначен на Node — логи приложения не нужны. Приложение ещё не запускалось.
Если образ не скачался — логи приложения тоже не нужны. Контейнера нет.
Если контейнер перезапускается — первым делом смотрим Last State и kubectl logs --previous.
Если Pod Running, но Ready=False — смотрим readiness probe, labels, Service selector и endpoints.
Если есть FailedMount — не надо начинать с сетевого плагина. Возможно, Secret просто не существует в этом namespace.
В этом и ценность describe: он не даёт утонуть в Kubernetes целиком. Он заставляет идти по этапам.
Сначала паспорт Pod’а: кто создал, куда попал, на чём живёт
Верхняя часть describe обычно кажется скучной. Но это как раз тот случай, где скучные строки экономят неприятные полчаса.
Name: api-7c9d7c9c6b-kx2sqNamespace: paymentsPriority: 0Service Account: apiNode: worker-03/10.0.4.23Start Time: Fri, 01 May 2026 10:14:22 +0000Labels: app=api pod-template-hash=7c9d7c9c6bAnnotations: kubectl.kubernetes.io/restartedAt: 2026-05-01T10:12:03ZStatus: RunningIP: 10.42.3.18Controlled By: ReplicaSet/api-7c9d7c9c6b
Первый вопрос всегда простой, почти скучный: это вообще тот Pod? В больших кластерах легко смотреть не туда — особенно когда рядом живут dev, stage, prod, preview-неймспейсы, canary-релизы, несколько командных окружений и пачка приложений с похожими именами. Поэтому Namespace — не формальность, а страховка от красивого, уверенного, но совершенно бесполезного дебага не той среды.
Controlled By показывает, кто владеет Pod’ом. Если там ReplicaSet, значит Pod приехал из Deployment. Если StatefulSet — из StatefulSet. Если Job — из Job. Если владелец странный или его вообще нет, это уже сигнал: надо понять, кто создал этот объект и почему он живёт сам по себе. Это важно, потому что Pod почти никогда не стоит «чинить руками». Pod — расходник. Его можно удалить, и Kubernetes создаст новый. Но если проблема лежит в deployment template, Helm-чарте, Kustomize-оверлее, ConfigMap, Secret, admission webhook’е или контроллере, новый Pod родится с тем же дефектом. Получится не фикс, а просто повтор запуска с той же ошибкой.
Строка Node отвечает на вопрос, который нужно задавать раньше логов: Pod вообще назначен на ноду?
Если видим:
Node: <none>
или Node отсутствует, контейнеры ещё не запускались. Значит, kubectl logs не даст полезного ответа. Нужно идти в Events, потому что scheduler почти наверняка уже объяснил, почему Pod некуда поставить.
Если Node есть, это не значит, что контейнер стартовал. Это значит только, что scheduler выбрал место, а kubelet на этой Node теперь пытается выполнить работу.
Service Account часто не замечают до того момента, пока всё не упрётся в доступы. Поставили не тот сервис-аккаунт — и внезапно у Pod’а уже не те RBAC-права, не тот projected token, не тот IAM в облаке, нет доступа к Vault, S3, Pub/Sub или Kubernetes API. Снаружи это может выглядеть как проблема приложения: «раньше ходило, теперь не ходит, в коде ничего не меняли». А на деле одна строка Service Account в describe иногда важнее всего стектрейса.
Labels полезны не только для красоты. Через них Pod попадает под Service, NetworkPolicy, PodDisruptionBudget, мониторинг, логирование, policy engine, service mesh. Один неправильный label может сделать сервис невидимым для трафика или, наоборот, подвести Pod под правило, на которое вы не рассчитывали.
Классика:
Labels: app.kubernetes.io/name: payment-api app.kubernetes.io/instance: payments
А в Service:
selector: app: payment-api
Оба человека в споре могут быть уверены, что «лейбл же есть». Но Kubernetes не угадывает намерения. Selector должен совпасть с labels буквально.
Annotations — тоже не просто служебный мусор, который можно пролистать. В них часто остаются следы того, кто и как трогал Pod: kubectl rollout restart, checksum конфигов, инжект сайдкара, настройки service mesh, Prometheus scraping, security profiles, перезапуски от reloader’ов. Иногда именно annotation спокойно объясняет то, из-за чего команда уже десять минут спорит в чате: почему Pod внезапно пересоздался, откуда взялся лишний сайдкар или почему итоговый Pod не похож на манифест, который лежит в репозитории.
Ещё ниже в выводе могут быть Node-Selectors, Tolerations, иногда важные настройки scheduling. Их легко пролистать, пока Pod уже стоит на Node. Но если Pod в Pending, эти поля становятся первыми подозреваемыми.
Node-Selectors: workload=paymentsTolerations: dedicated=payments:NoSchedule
Если Pod должен попадать только на определённый node pool, именно здесь можно увидеть, что он вообще просит. А в Events будет видно, почему scheduler не смог выполнить эту просьбу.
В нормальной диагностике верхний блок читается быстро, но не на автопилоте. После него в голове должна сложиться короткая картинка: это тот самый Pod, в том самом namespace, им рулит ожидаемый контроллер, он уже назначен на Node или ещё висит без неё, работает под нужным service account, а его labels, annotations и scheduling hints выглядят так, как вы ожидали. Если на этом этапе что-то не сходится, можно не лезть глубже в контейнеры, probes и логи. Вы нашли не баг приложения и не «приколы Kubernetes», а ошибку маршрута: смотрели не туда, не тот объект, не тот владелец, не те метки или не те правила планирования.
Контейнеры: место, где Running перестаёт успокаивать
Самая важная часть вывода — блоки Init Containers и Containers.
Начнём с основных контейнеров. Типичный фрагмент выглядит так:
Containers: api: Container ID: containerd://... Image: registry.example.com/payments/api:1.42.0 Image ID: registry.example.com/payments/api@sha256:... Port: 8080/TCP State: Running Started: Fri, 01 May 2026 10:14:29 +0000 Ready: False Restart Count: 8 Limits: memory: 512Mi Requests: cpu: 250m memory: 256Mi Liveness: http-get http://:8080/healthz delay=10s timeout=1s period=10s Readiness: http-get http://:8080/ready delay=5s timeout=1s period=5s
Новички часто цепляются за State: увидели Running — и всё, пошли копать Service, Ingress, DNS, сетку, что угодно вокруг. Но в той же карточке рядом может спокойно лежать Ready: False и Restart Count: 8, а это уже совсем другой сюжет. `
State: Running` говорит только о том, что процесс контейнера сейчас запущен. Не о том, что он здоров. Не о том, что он готов принимать трафик. И уж точно не о том, что минуту назад он не падал.
Ready: False означает, что контейнер не считается готовым. Если Pod не ready, Service не должен отправлять туда обычный трафик. Для Kubernetes это не «почти работает», а вполне конкретный сигнал: экземпляр существует, но в балансировку его пока не пускать.
Restart Count показывает, сколько раз контейнер уже перезапускался. Само число без контекста не всегда страшное. Если Pod живёт 90 дней и у него 1 restart после maintenance — никто не должен бегать по потолку. Но если Pod создан 12 минут назад и у него 8 рестартов, это уже не случайность, а поведение.
Самый полезный сосед State — это Last State.
State: Waiting Reason: CrashLoopBackOffLast State: Terminated Reason: Error Exit Code: 1 Started: Fri, 01 May 2026 10:20:11 +0000 Finished: Fri, 01 May 2026 10:20:12 +0000Restart Count: 8
Здесь почти вся история уже есть. Контейнер стартует, живёт около секунды, завершается с кодом 1, kubelet перезапускает его, а потом делает паузы между попытками.
Следующий шаг — не удалить Pod и не спорить с Kubernetes. Следующий шаг:
kubectl logs api-7c9d7c9c6b-kx2sq -n payments -c api --previous
Ключ --previous важен. Если контейнер уже перезапустился, обычный kubectl logs может показать новый запуск, где приложение ещё не успело упасть. А --previous достаёт логи прошлого умершего контейнера.
Другой пример:
Last State: Terminated Reason: OOMKilled Exit Code: 137Restart Count: 4Limits: memory: 256Mi
Вот тут тоже не надо начинать с легенды про нестабильный Kubernetes. Если Reason: OOMKilled, контейнер убит из-за памяти. Дальше уже идут нормальные инженерные вопросы: лимит слишком низкий, приложение реально стало есть больше, появилась утечка, вырос batch, JVM heap настроен без учёта container limit, Python загрузил весь файл в память, sidecar съел часть памяти, или Pod попал на Node, где и без него было тяжело.
describe не ответит на все эти вопросы. Но он мгновенно убирает половину ложных версий.
Блок Image и Image ID тоже недооценивают. Тег может выглядеть правильно, а digest оказаться не тем. Или наоборот: все говорят, что деплоили 1.42.0, но в Pod стоит другой image. В production лучше деплоить immutable tags или digest’ы, но реальность обычно разнообразнее документации.
Image: registry.example.com/payments/api:latestImage ID: registry.example.com/payments/api@sha256:6f2c...
Если команда использует latest, mutable tags или пересобирает один и тот же tag, Image ID часто честнее, чем разговоры в чате. Тег говорит, как образ назвали. Digest говорит, что реально запущено.
Рядом с образом обычно видны Requests и Limits.
Requests: cpu: 100m memory: 128MiLimits: cpu: 500m memory: 256Mi
Requests — это то, с чем Pod приходит к scheduler’у: «мне нужно столько CPU и памяти, найди мне подходящую ноду». Если таких ресурсов на подходящих нодах нет, Pod может так и остаться в Pending. А Limits — это уже правила жизни после старта. Memory limit может закончиться OOMKilled, CPU limit — троттлингом, когда приложение вроде бы работает, но его регулярно «придерживают» по процессору. describe не покажет красивый график троттлинга, зато быстро даст понять, в каких условиях контейнер вообще запустили и не душим ли мы его собственными лимитами.
Ещё ниже может встретиться QoS Class:
QoS Class: Burstable
В обычном дебаге это не первая строка, за которую стоит хвататься. Чаще вы сначала пойдёте в Events, Last State, пробы, лимиты и логи. Но если Pod эвиктится, на ноде давление по памяти или она вообще ведёт себя странно, QoS-класс уже становится важным контекстом. Он помогает понять, насколько «защищённым» Kubernetes считает этот Pod при нехватке ресурсов. Нет requests и limits — одна история. Они аккуратно выставлены и совпадают для всех контейнеров — другая. Memory limit маленький, а приложение распухает на старте — третья, и обычно не самая весёлая.
Отдельная зона риска — пробы.
Liveness: http-get http://:8080/healthz delay=10s timeout=1s period=10s #success=1 #failure=3Readiness: http-get http://:8080/ready delay=5s timeout=1s period=5s #success=1 #failure=3
В describe видно не то, что вы помните из Helm values, и не то, что неделю назад было в PR. Там видно то, что реально доехало до Pod’а. Это важная разница. Readiness-проба отвечает на простой вопрос: можно ли уже пускать сюда трафик? Liveness-проба — на другой: не завис ли процесс так, что его пора прибить и запустить заново? Startup-проба нужна для третьей ситуации: приложение долго поднимается, прогревает кеши, открывает соединения, читает конфиги, и до конца этого старта его не надо трогать liveness-проверкой.
Проблемы начинаются, когда все эти три смысла складывают в один /healthz и ставят timeoutSeconds: 1. На бумаге выглядит аккуратно: есть healthcheck, Kubernetes всё проверяет, надёжность растёт. В жизни получается веселее. Сервис чуть дольше прогрелся — liveness убила его на старте. База моргнула — readiness выкинула все Pod’ы из балансировки. GC занял больше секунды — kubelet решил, что процесс умер. Снаружи это выглядит как «Kubernetes опять зачем-то рестартит приложение». Но чаще Kubernetes просто делает ровно то, что мы сами ему сказали делать.
Более здоровый пример:
startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 5 failureThreshold: 24readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 5 timeoutSeconds: 2livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 timeoutSeconds: 2
Это не универсальный рецепт. Числа зависят от приложения. Но разделение ролей важно: startup даёт стартовать, readiness управляет трафиком, liveness перезапускает действительно зависший процесс.
Особенно аккуратно надо относиться к readiness. У неё часто возникает соблазн стать «самой честной проверкой всего мира»: проверить базу, Kafka, Redis, внешний API, миграции, очередь, лицензию и настроение соседнего сервиса. Иногда это оправдано. Но если readiness слишком глубоко завязана на зависимости, краткий сбой одной зависимости может одновременно вывести из трафика все реплики и усилить аварию.
Readiness — это не исповедь приложения. Это контракт с балансировкой: «можно ли прямо сейчас давать сюда пользовательский трафик?»
В multi-container Pod важно смотреть не только на основной контейнер. Если в kubectl get pods видно:
NAME READY STATUS RESTARTS AGEapi-7c9d7c9c6b-kx2sq 1/2 Running 0 5m
это значит, что один из двух контейнеров не ready. Возможно, приложение живо, но sidecar не готов. Возможно, service mesh proxy не поднялся. Возможно, лог-агент падает. А возможно и наоборот: sidecar работает, а основной процесс умер.
describe покажет состояние каждого контейнера отдельно. А логи в таком Pod надо читать явно:
kubectl logs api-7c9d7c9c6b-kx2sq -n payments -c apikubectl logs api-7c9d7c9c6b-kx2sq -n payments -c istio-proxy
Иначе можно полчаса читать не тот процесс и делать очень уверенные неправильные выводы.
Теперь про init containers. Это один из самых частых источников путаницы: «Pod не стартует, у приложения нет логов, Kubernetes что-то ждёт».
Фрагмент может выглядеть так:
Init Containers: migrate: Image: registry.example.com/payments/migrations:1.42.0 State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Exit Code: 1 Restart Count: 5
В такой ситуации основной контейнер api ещё не запускался. Смотреть его логи бессмысленно. Нужно смотреть init container:
kubectl logs api-7c9d7c9c6b-kx2sq -n payments -c migrate --previous
Init containers хороши для короткой подготовки: дождаться зависимости, положить файл, сделать простую проверку, подготовить volume. Нормальный такой предзапусковой «разогрев». Но они быстро превращаются в источник боли, когда в них кладут тяжёлые миграции, долгие операции с данными или логику, которая может начать конфликтовать между несколькими репликами. Например, если init container гоняет миграцию схемы, а Deployment одновременно поднимает несколько Pod’ов, легко получить гонки, блокировки и странные падения на старте. В describe это будет выглядеть как проблема конкретного Pod’а, хотя на самом деле сломалась стратегия релиза.
Иногда там же можно увидеть и ephemeral containers — временные дебаг-контейнеры, добавленные через kubectl debug. Это уже не часть обычного старта приложения, а скорее след расследования: кто-то заходил внутрь Pod’а и пытался разобраться на месте. Само по себе это не криминал, даже в production, но хороший повод понять, кто его добавил, зачем, и не остался ли после отладки лишний доступ.
Conditions, volumes и Events: Kubernetes обычно уже оставил записку
После контейнеров стоит посмотреть Conditions.
Conditions: Type Status PodReadyToStartContainers True Initialized True Ready False ContainersReady False PodScheduled True
В разных версиях Kubernetes и при разных настройках набор conditions может отличаться. Например, PodReadyToStartContainers можно встретить не везде. Но смысл блока один: он показывает стадии жизни Pod’а.
PodScheduled: False — scheduler не назначил Pod на Node. Контейнеры ещё не тема.
Initialized: False — проблема может быть в init containers.
ContainersReady: False — один или несколько контейнеров не ready.
Ready: False — Pod в целом не готов для обычного трафика.
Conditions сухие. Они не рассказывают красивую историю. Но они показывают, на какой станции поезд остановился. Это полезно, когда в голове уже смешались scheduler, kubelet, probes, Service и приложение.
Дальше идут volumes и mounts. Многие пролистывают их автоматически, а потом полчаса ищут проблему в коде.
Volumes: config: Type: ConfigMap (a volume populated by a ConfigMap) Name: api-config Optional: false secrets: Type: Secret (a volume populated by a Secret) SecretName: api-secrets Optional: false
Если Secret или ConfigMap отсутствует, kubelet обычно напишет об этом в Events:
Warning FailedMount 32s (x8 over 2m) kubelet MountVolume.SetUp failed for volume "config": configmap "api-config" not found
Это не проблема приложения. Оно может даже не стартовать, потому что окружение не собрано. Нужно проверить, есть ли ConfigMap или Secret в том же namespace, правильно ли называется объект, не сломался ли Helm template, не отвалился ли External Secrets Operator, не поменяли ли имя в values.
Secret в prod не помогает Pod’у в stage. ConfigMap в соседнем namespace тоже не помогает. Kubernetes в этом смысле очень прямолинеен.
Похожая история с environment variables. Если контейнер собирает env из Secret или ConfigMap, describe может показать, откуда берётся значение:
Environment: DATABASE_URL: <set to the key 'database-url' in secret 'api-secrets'> Optional: false
Сам секрет, конечно, не будет распечатан в открытом виде. И это хорошо. Но describe покажет ссылку: какой объект, какой key, optional или нет. Если key переименовали, Secret пересоздали не в том namespace или забыли применить ExternalSecret, ошибка часто будет видна не в логах приложения, а в Events и конфигурации контейнера.
С PVC история похожая, но последствия другие:
Warning FailedScheduling 1m default-scheduler 0/6 nodes are available: pod has unbound immediate PersistentVolumeClaims.
Это уже не про контейнеры, не про образ и не про readiness. Нужно смотреть PVC, StorageClass, provisioner, access modes, zone constraints. Один и тот же Pending может означать нехватку CPU, неподходящий taint или проблему с диском. Поэтому статус без Events почти бесполезен.
Events — это нижняя часть вывода, до которой многие доходят последними. А зря. Если в describe нужно выработать одну привычку, то вот она: не закрывать вывод, пока не прочитал Events.
Kubernetes редко пишет там литературно, но часто пишет достаточно честно.
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 3m22s default-scheduler 0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had untolerated taint {dedicated: gpu}. Normal NotTriggerScaleUp 3m18s cluster-autoscaler pod didn't trigger scale-up: 2 node(s) had untolerated taint
Pod не «завис в Pending» сам по себе. Его просто некуда поставить: scheduler посмотрел на кластер и не нашёл подходящую ноду. Где-то не хватает CPU, где-то висит taint, который этот Pod не умеет терпеть через toleration. И если сейчас удалить Pod, проблема не решится — новый с тем же template упрётся ровно в ту же стену. Поэтому, это не лечится рестартом, это проблема шедулинга.
Другой пример:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Pulling 2m10s kubelet Pulling image "registry.example.com/payments/api:1.42.0" Warning Failed 2m8s kubelet Failed to pull image "registry.example.com/payments/api:1.42.0": pull access denied Warning Failed 2m8s kubelet Error: ErrImagePull Normal BackOff 58s kubelet Back-off pulling image "registry.example.com/payments/api:1.42.0" Warning Failed 58s kubelet Error: ImagePullBackOff
Это не проблема приложения. Приложение ещё не запускалось. Логов контейнера нет, потому что контейнера нет. Нужно проверять tag, digest, registry credentials, imagePullSecrets, права service account, доступ Node к registry, сетевые ограничения или временную недоступность registry.
Ещё пример:
Warning Unhealthy 45s (x6 over 80s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 500
Контейнер запущен, но readiness probe получает 500. Pod может быть Running, но не попадать в endpoints Service. Для пользователя это выглядит как «деплой прошёл, а трафика нет». Для Kubernetes всё логично: экземпляр не готов, поэтому отправлять туда запросы нельзя.
С liveness ещё опаснее:
Warning Unhealthy 2m10s (x3 over 2m30s) kubelet Liveness probe failed: Get "http://10.42.3.18:8080/healthz": context deadline exceededNormal Killing 2m10s kubelet Container api failed liveness probe, will be restarted
Kubernetes не от скуки перезапускает Pod. Он перезапускает контейнер, потому что liveness probe сказала что процесс нездоров. Если timeout слишком агрессивный, endpoint тяжёлый, приложение прогревается дольше, Node перегружена или CPU троттлинг делает ответы медленными, liveness может превратиться в автоматический убийца процессов.
Events можно смотреть и отдельной командой. В шумном namespace полезно фильтровать по конкретному Pod:
kubectl get events -n <namespace> \ --field-selector involvedObject.kind=Pod,involvedObject.name=<pod> \ --sort-by=.lastTimestamp
Или шире по namespace:
kubectl get events -n <namespace> --sort-by=.lastTimestamp
Но у Events есть ограничение: они не вечные. В кластере может быть настроено ограниченное хранение, события могут агрегироваться, старые записи могут исчезнуть. Если Pod болел ночью, а вы пришли утром, часть истории уже могла пропасть. Поэтому для production нужны централизованные события, логи kubelet/control plane там, где они доступны, и нормальная observability.
Как читать типовые поломки без гадания
Самая большая польза describe в том, что он расклеивает похожие симптомы.
Pending выглядит одинаково в kubectl get pods, но внутри может быть несколько разных миров.
FailedScheduling: 0/6 nodes are available: 6 Insufficient memory.
Это про requests, capacity, autoscaler, overcommit, quota.
FailedScheduling: 0/6 nodes are available: pod has unbound immediate PersistentVolumeClaims.
Это про PVC, StorageClass, provisioner, zone, access mode.
FailedScheduling: 0/6 nodes are available: 3 node(s) had untolerated taint {workload: infra}.
Это про tolerations и правильный node pool.
Один Pending, три разных причины, три разных направления диагностики. Если идти только по статусу, вы будете чинить не тот слой.
CrashLoopBackOff тоже не диагноз, а поведение. Контейнер часто падает, kubelet делает backoff.
Если видим:
Last State: Reason: Error Exit Code: 1
идём в previous logs.
Если:
Last State: Reason: OOMKilled Exit Code: 137
смотрим память, limits, heap, метрики, размер входных данных, sidecars.
Если:
State: Waiting: Reason: CreateContainerConfigError
часто проблема в конфигурации контейнера: отсутствует Secret, ConfigMap, неправильная ссылка на key, ошибка сборки env. Тут Events обычно скажут больше, чем логи приложения, потому что приложение ещё не стартовало.
ImagePullBackOff почти всегда надо начинать с Events, Image и image pull settings. Разница между «тега не существует» и «нет прав на registry» принципиальная. По статусу она не видна, по Events обычно видна.
Running, но трафика нет — отдельный любимый жанр. Здесь нужно смотреть не только Pod.
В describe pod проверяем:
Ready: FalseReadiness: http-get http://:8080/ready ...
Потом смотрим события probe. Потом labels Pod’а. Потом Service selector:
kubectl describe service <service> -n <namespace>
И endpoints:
kubectl get endpoints <service> -n <namespace>
или в новых кластерах:
kubectl get endpointslice -n <namespace> \ -l kubernetes.io/service-name=<service>
Если Pod ready, но Service его не выбирает, проблема не в контейнере. Если Pod не ready, Service правильно не отправляет туда трафик. Если endpoints есть, но снаружи всё равно 503 — дальше уже Ingress, Gateway, mesh, network policy или приложение.
FailedMount почти всегда ведёт к volumes, Secret, ConfigMap, PVC или CSI driver. Не надо начинать с рестарта Deployment. Если kubelet не может смонтировать volume, новый Pod с тем же spec будет страдать так же.
Evicted — отдельный случай. В describe можно увидеть причину eviction, например давление по memory или ephemeral storage на Node. Это уже не просто «контейнер упал». Это Node сказала: ресурсов плохо, кого-то надо выселить. Тут полезно смотреть requests/limits, ephemeral storage, QoS, состояние Node и соседние workload’ы.
Есть ещё неприятная категория проблем, где Pod выглядит вроде нормально, но его изменила платформа. Например, mutating webhook добавил sidecar, security context, volume, env или resource limit. describe в таком случае покажет уже итоговую картину. Он может не ответить, кто именно это добавил, но даст первый сигнал: «в Pod есть то, чего мы не писали руками».
Дальше уже нужно идти к источнику: namespace labels, admission webhooks, Helm chart, Kustomize overlay, policy engine, service mesh injector, platform controller.
В инциденте такой короткий маршрут часто полезнее длинного знания Kubernetes. Он не решает проблему за вас, но не даёт начинать с неправильного слоя.
Есть ещё несколько привычек, которые сильно снижают количество ложных следов.
Первая: всегда указывать namespace.
kubectl describe pod <pod> -n <namespace>
Да, можно настроить context., но в инциденте лучше быть скучным и явным.
Вторая: в multi-container Pod всегда указывать контейнер для логов.
kubectl logs <pod> -n <namespace> -c <container>
Третья: если контейнер перезапускался, почти всегда пробовать --previous.
kubectl logs <pod> -n <namespace> -c <container> --previous
Четвёртая: не парсить kubectl describe скриптами. Его вывод предназначен для человека и может меняться. Для автоматизации используйте kubectl get pod -o yaml, -o json, JSONPath, jq, API или нормальные SDK.
Пятая: помнить, что describe показывает итоговый Pod, но не всегда показывает источник значения. Если вы видите странный env, probe, resource limit, sidecar или security context, они могли прийти из Helm values, Kustomize overlay, mutating webhook, policy engine, platform defaults или service mesh injection. describe показывает, что получилось. Чтобы понять, почему получилось именно так, иногда надо идти выше по цепочке.
Шестая: не лечить всё удалением Pod’а.
Удаление иногда помогает, если была временная проблема Node, registry, DNS или runtime. Но если причина в template, ConfigMap, Secret, requests, probes, PVC, policy или image, новый Pod повторит тот же сценарий. Удаление Pod’а без понимания причины — это не remediation. Это перезапуск надежды.
Хороший итог после describe — не «Pod в CrashLoopBackOff». Это просто повтор статуса, а не диагноз. Лучше сформулировать так, чтобы сразу было понятно, куда копать: «контейнер стартует, живёт примерно секунду и падает с exit code 1 — надо смотреть previous logs»; «Pod не назначен на Node, scheduler пишет Insufficient memory»; «образ не скачивается, pull access denied — проверяем imagePullSecret и права в registry»; «Pod в Running, но Ready=False: readiness получает 500, поэтому Service правильно не льёт туда трафик»; «основной контейнер даже не стартовал, потому что падает init container migrate». Вот в этот момент и начинается нормальная диагностика. Вы уже не пересказываете ярлык из kubectl get pods, а описываете механизм поломки — коротко, по делу и так, чтобы следующая команда была почти очевидна.
kubectl describe pod ценен не тем, что показывает много информации. Много информации есть везде: в логах, Grafana, traces, дашбордах, алертах, CI, Helm templates. Его ценность в том, что он связывает симптомы с жизненным циклом Pod’а.
Был ли Pod создан нужным контроллером. Назначил ли его scheduler. Успел ли kubelet скачать образ. Собрались ли volumes и env. Завершились ли init containers. Запустился ли основной контейнер. Чем закончился прошлый запуск. Почему Pod не ready. Что Kubernetes уже записал в Events.
Для junior-инженера это способ не потеряться. Для middle — быстрее отделить инфраструктурную проблему от прикладной. Для senior — проверить, не уехала ли команда в сложные версии, когда кластер уже написал простую причину внизу вывода
describe не чинит Pod’ы. Он чинит ход мысли. А в эксплуатации это часто делает разницу.
ссылка на оригинал статьи https://habr.com/ru/articles/1031454/