Kubernetes для самых маленьких. Часть 31: Как искать неисправности в кластере?
Все части серии «Kubernetes для самых маленьких»За тридцать частей кластер оброс компонентами: поды и контроллеры из части 6 и части 7, планировщик из части 13, сеть из Calico, kube-proxy и CoreDNS (части 14, 15, 16), тома на NFS и SMB, политики безопасности из прошлой части. У каждого из них есть свой способ сломаться, и каждый сообщает об этом в своём месте. Эта часть открывает блок о наблюдаемости и посвящена ручной диагностике: в каком порядке смотреть, какими командами, где лежат события, логи и состояние контейнера, как попасть в под без shell и на узел без SSH, и что означают статусы, которые чаще всего видны в kubectl get pods. Метрики, сбор логов и трассы — в следующих двух частях; здесь всё делается одним kubectl и, в крайнем случае, crictl на узле.
Неймспейс demo остаётся таким, каким его оставила прошлая часть: под уровнем restricted Pod Security Admission, hello работает от UID 10001 без capabilities. Это повлияет на отладку, и мы увидим как.
Сверху вниз
Kubernetes — это цепочка контроллеров, где каждый следующий реагирует на объект, созданный предыдущим (часть 3). Когда что-то не работает, сломалось одно звено, а симптом виден в конце цепочки: «сайт не открывается». Поэтому искать причину надо в обратном порядке — от объекта, с которым работает человек, к узлу, где живёт процесс:
уровень что смотреть чем
─────────── ───────────────────────────────────────── ─────────────────────────────────────
объект Deployment, Service, Ingress, PVC: kubectl get / describe
status, conditions, сколько готово
│
▼
события что контроллеры, планировщик и kubelet kubectl events --for, describe
сказали об объекте за последний час
│
▼
под фаза, conditions, узел, IP, kubectl get pod -o wide / -o yaml
состояние каждого контейнера
│
▼
контейнер логи текущего и прошлого запуска, kubectl logs, exec, debug
код выхода, что внутри
│
▼
узел kubelet, containerd, CNI, диск, память, kubectl describe node, debug node,
сертификаты crictl, journalctl
На каждом уровне вопрос один: дошло ли дело до следующего звена. Deployment создал ReplicaSet? ReplicaSet создал поды? Поды назначены на узел? kubelet запустил контейнер? Контейнер прошёл readiness? Первое звено, ответившее «нет», и есть место поломки, а его события обычно называют причину прямым текстом. Перескакивать к логам приложения, пока не ясно, запущено ли оно вообще, — частая потеря времени.
События
Event — обычный объект API в группе events.k8s.io/v1 (и в старом core v1, который kubectl describe до сих пор читает): короткое сообщение, привязанное к другому объекту. Пишут их компоненты, которые что-то делают с объектами: планировщик (Scheduled, FailedScheduling), kubelet (Pulling, Pulled, Created, Started, Killing, BackOff, Unhealthy, FailedMount), контроллеры (ScalingReplicaSet, SuccessfulCreate), драйверы CSI через свои sidecar-контейнеры (ProvisioningSucceeded), cert-manager и любые операторы.
$ kubectl get events -n demo --field-selector reason=BackOff -o yaml
apiVersion: v1
items:
- apiVersion: v1
count: 8
eventTime: null
firstTimestamp: "2026-11-02T13:57:25Z"
involvedObject:
apiVersion: v1
fieldPath: spec.containers{hello}
kind: Pod
name: hello-798bd968d9-vkz9z
namespace: demo
resourceVersion: "1699629"
uid: cab28704-1a6a-40d9-bd16-172760839f9d
kind: Event
lastTimestamp: "2026-11-02T13:58:06Z"
message: Back-off restarting failed container hello in pod hello-798bd968d9-vkz9z_demo(cab28704-1a6a-40d9-bd16-172760839f9d)
metadata:
creationTimestamp: "2026-11-02T13:57:25Z"
name: hello-798bd968d9-vkz9z.18e43f05b2daee83
namespace: demo
resourceVersion: "1699788"
uid: e12dcf66-8527-4226-9af6-5775de85c8f8
reason: BackOff
reportingComponent: kubelet
reportingInstance: k8s-node-2
source:
component: kubelet
host: k8s-node-2
type: Warning
kind: List
metadata:
resourceVersion: ""
Что важно в устройстве событий:
- Тип только два:
NormalиWarning. Фильтр--types=Warningотсекает шум из «образ скачан, контейнер запущен». - Агрегация. Повторяющееся событие не создаётся заново, а увеличивает
count(в новом API —series.count) и сдвигаетlastTimestamp. «8 раз за 41 секунду» — это одно событие. - Срок жизни. События хранятся в etcd как отдельные объекты и удаляются через
--event-ttlkube-apiserver, по умолчанию 1 час. Через час после аварии в кластере не останется ни одного события о ней. Поэтому их собирают в систему логов — как, разберём в части 33 — а при ручной диагностике смотрят сразу. - События не гарантированы. Компоненты пишут их «по возможности» и с ограничением частоты; отсутствие события не доказывает, что ничего не происходило.
Смотреть удобнее всего командой kubectl events: в отличие от kubectl get events, она по умолчанию сортирует по времени и умеет фильтровать по объекту:
$ kubectl events -n demo --for pod/hello-798bd968d9-vkz9z
LAST SEEN TYPE REASON OBJECT MESSAGE
55s Normal Scheduled Pod/hello-798bd968d9-vkz9z Successfully assigned demo/hello-798bd968d9-vkz9z to k8s-node-2
9s (x4 over 51s) Normal Pulled Pod/hello-798bd968d9-vkz9z Container image "ghcr.io/example/hello:1.1" already present on machine and can be accessed by the pod
8s (x4 over 49s) Normal Created Pod/hello-798bd968d9-vkz9z Container created
8s (x4 over 49s) Normal Started Pod/hello-798bd968d9-vkz9z Container started
8s (x6 over 49s) Warning Unhealthy Pod/hello-798bd968d9-vkz9z Readiness probe failed: Get "http://172.20.172.6:8080/readyz": dial tcp 172.20.172.6:8080: connect: connection refused
4s (x8 over 45s) Warning BackOff Pod/hello-798bd968d9-vkz9z Back-off restarting failed container hello in pod hello-798bd968d9-vkz9z_demo(cab28704-1a6a-40d9-bd16-172760839f9d)
$ kubectl events -A --types=Warning --watch
--for принимает kind/name, -A смотрит по всем неймспейсам, --watch держит поток. События узла лежат в неймспейсе default и привязаны к объекту Node: kubectl events --for node/k8s-node-2.
Объект и под: describe, get, conditions
kubectl describe — сводка, собранная клиентом из нескольких запросов: сам объект в человекочитаемом виде плюс связанные события. Для пода это узел, IP, контейнеры с их состоянием, тома, tolerations и события в конце. Для Deployment — стратегия, реплики и ReplicaSet'ы; для узла — conditions, ресурсы, выделенные подам, и taints.
kubectl get с форматами отвечает на вопросы точнее:
$ kubectl get pods -n demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello-798bd968d9-vkz9z 0/1 CrashLoopBackOff 3 (8s ago) 55s 172.20.172.6 k8s-node-2 <none> <none>
hello-865b67cc76-682fc 1/1 Running 0 67s 172.20.211.15 k8s-node-1 <none> <none>
hello-865b67cc76-6vcck 1/1 Running 0 67s 172.20.172.8 k8s-node-2 <none> <none>
hello-865b67cc76-ntcj4 1/1 Running 0 67s 172.20.211.16 k8s-node-1 <none> <none>
-o wide добавляет узел и IP: если все проблемные поды на одном узле, дело в узле, а не в приложении. Колонка STATUS — не поле API, а строка, которую kubectl собирает из фазы пода и причины ожидания или завершения контейнеров. Настоящее состояние — в -o yaml; ветку status вырезает sed (строки lastProbeTime и observedGeneration ниже убраны):
$ kubectl get pod hello-798bd968d9-vkz9z -n demo -o yaml | sed -n '/^status:/,$p'
status:
conditions:
- lastTransitionTime: "2026-11-02T13:57:19Z"
status: "True"
type: PodReadyToStartContainers
- lastTransitionTime: "2026-11-02T13:57:15Z"
status: "True"
type: Initialized
- lastTransitionTime: "2026-11-02T13:57:15Z"
message: 'containers with unready status: [hello]'
reason: ContainersNotReady
status: "False"
type: Ready
- lastTransitionTime: "2026-11-02T13:57:15Z"
message: 'containers with unready status: [hello]'
reason: ContainersNotReady
status: "False"
type: ContainersReady
- lastTransitionTime: "2026-11-02T13:57:15Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://13884b1b4b6703e8cf8001a66cf0831dda91aafeb4fad1c0d5dc1133271b1ae4
image: ghcr.io/example/hello:1.1
imageID: ghcr.io/example/hello@sha256:2d8774f801594f2b1b115963e54ec54b1b49f95c2dece1d6b0fd1e59ab447c7b
lastState:
terminated:
containerID: containerd://13884b1b4b6703e8cf8001a66cf0831dda91aafeb4fad1c0d5dc1133271b1ae4
exitCode: 1
finishedAt: "2026-11-02T13:58:02Z"
reason: Error
startedAt: "2026-11-02T13:58:02Z"
name: hello
ready: false
resources: {}
restartCount: 3
started: false
state:
waiting:
message: back-off 40s restarting failed container=hello pod=hello-798bd968d9-vkz9z_demo(cab28704-1a6a-40d9-bd16-172760839f9d)
reason: CrashLoopBackOff
user:
linux:
gid: 10001
supplementalGroups:
- 10001
uid: 10001
volumeMounts:
- mountPath: /tmp
name: tmp
hostIP: 192.168.10.21
hostIPs:
- ip: 192.168.10.21
phase: Running
podIP: 172.20.172.6
podIPs:
- ip: 172.20.172.6
qosClass: BestEffort
resources: {}
startTime: "2026-11-02T13:57:15Z"
Conditions пода — пять флагов в порядке, в котором они становятся True:
| Condition | Что значит | Если False — смотреть |
|---|---|---|
PodScheduled |
планировщик выбрал узел | событие FailedScheduling |
PodReadyToStartContainers |
kubelet создал песочницу пода: сетевой namespace, адрес от CNI, pause-контейнер | события FailedCreatePodSandBox, CNI на узле |
Initialized |
все init-контейнеры завершились успешно | логи init-контейнера, -c <имя> |
ContainersReady |
все контейнеры прошли readiness | события Unhealthy, логи |
Ready |
под готов принимать трафик и попадает в эндпоинты сервиса | то же плюс readiness gates |
У каждого контейнера три возможных state: waiting с reason (ContainerCreating, ErrImagePull, CrashLoopBackOff, CreateContainerConfigError), running со временем старта и terminated с exitCode, reason и signal. lastState хранит предыдущий запуск — именно там видно, почему контейнер упал в прошлый раз, пока текущий ждёт перезапуска. Поле user.linux показывает, с какими UID и группами процесс запущен на самом деле, после всех securityContext и настроек образа.
Для скриптов и быстрых вопросов — jsonpath и custom-columns:
$ kubectl get pods -n demo -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName,REASON:.status.containerStatuses[0].lastState.terminated.reason,EXIT:.status.containerStatuses[0].lastState.terminated.exitCode'
POD NODE REASON EXIT
hello-798bd968d9-vkz9z k8s-node-2 Error 1
hello-865b67cc76-682fc k8s-node-1 <none> <none>
hello-865b67cc76-6vcck k8s-node-2 <none> <none>
hello-865b67cc76-ntcj4 k8s-node-1 <none> <none>
Код выхода расшифровывается по правилу shell: до 128 — код, который вернуло приложение, больше 128 — процесс убит сигналом с номером код − 128:
| Код | Что случилось |
|---|---|
| 0 | процесс завершился сам и успешно; для Deployment это всё равно перезапуск: restartPolicy: Always |
| 1, 2 | ошибка приложения: не прочитался конфиг, не открылся порт, паника Go даёт 2 |
| 126, 127 | команда не исполняемая или не найдена: опечатка в command, нет shell в distroless-образе |
| 137 | 128 + 9, SIGKILL: OOM killer или kubelet, не дождавшийся завершения после SIGTERM |
| 139 | 128 + 11, SIGSEGV: ошибка памяти в самом процессе |
| 143 | 128 + 15, SIGTERM: процесс корректно остановлен по сигналу |
Логи: kubectl logs
kubectl logs читает не из приложения и не из containerd напрямую: запрос идёт в kube-apiserver, тот проксирует его в kubelet на нужном узле (порт 10250, подресурс pods/log), а kubelet читает файл, в который containerd пишет stdout и stderr контейнера. Как эти файлы устроены и ротируются, разберём в части 33; сейчас важно, что kubectl logs видит только то, что процесс написал в stdout и stderr, и только пока файлы лежат на узле.
$ kubectl logs -n demo hello-798bd968d9-vkz9z --previous
2026/11/02 13:58:02 listen: listen tcp: lookup tcp/8080 : unknown port
Основные флаги:
| Флаг | Что делает |
|---|---|
--previous, -p |
логи прошлого запуска контейнера. Главный флаг при CrashLoopBackOff: текущий контейнер только что стартовал или ещё не запущен, причина падения — в предыдущем |
-c <контейнер> |
контейнер в многоконтейнерном поде; без флага берётся аннотация kubectl.kubernetes.io/default-container или первый контейнер |
--all-containers |
все контейнеры пода, включая init |
--since=10m, --since-time=... |
только свежие строки |
--tail=100 |
последние N строк; при -l по умолчанию 10 на под |
-f |
следить за потоком |
-l app.kubernetes.io/name=hello |
по селектору: логи всех подов Deployment сразу; с --prefix — с именем пода в начале строки |
--timestamps |
время, которое записал containerd, а не приложение |
deploy/hello |
вместо имени пода: kubectl сам выберет один под Deployment |
--previous показывает ровно один предыдущий запуск: kubelet хранит на узле только последний завершившийся контейнер пода и его файл лога, более старые удаляет. Пока контейнер ждёт перезапуска, нового запуска нет, и kubectl logs без флага показывает тот же упавший запуск, что и --previous. Если под удалён или пересоздан контроллером на другом узле, его логи исчезли вместе с ним — ещё одна причина собирать их централизованно.
Внутрь контейнера: exec и эфемерные контейнеры
kubectl exec запускает новый процесс в namespaces работающего контейнера — через тот же kubelet и CRI-вызов Exec. В образе с shell это обычный способ посмотреть изнутри: kubectl exec -it <под> -- sh. В hello не выйдет даже это:
$ kubectl exec -n demo deploy/hello -- cat /etc/resolv.conf
error: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "d817a624cf51c53c50ac96b2a8d0818a31674ae132f8a9cc324a6a31d1900f23": OCI runtime exec failed: exec failed: unable to start container process: exec: "cat": executable file not found in $PATH
Это обычная ситуация с правильно собранным образом из части 5: в distroless- или scratch-образе нет shell, cat, curl и ps, и это хорошо для безопасности. Не нужно возвращать в образ отладочные утилиты — для этого есть эфемерные контейнеры.
Эфемерный контейнер — контейнер, который добавляют в уже работающий под через подресурс pods/ephemeralcontainers. Обычный список containers у пода неизменяем, а ephemeralContainers можно дополнять. У эфемерного контейнера нет портов, проб и ресурсов, он не перезапускается и не удаляется, пока жив под. Запускается он рядом с остальными контейнерами пода — в том же сетевом namespace, с тем же IP и localhost. Добавляет его kubectl debug:
$ kubectl debug -it -n demo hello-865b67cc76-682fc --image=nicolaka/netshoot:v0.15 --target=hello
Warning: Non-root user is configured for the entire target Pod, and some capabilities granted by debug profile may not work. Please consider using "--custom" with a custom profile that specifies "securityContext.runAsUser: 0".
Error from server (Forbidden): pods "hello-865b67cc76-682fc" is forbidden: violates PodSecurity "restricted:v1.36": allowPrivilegeEscalation != false (container "debugger-x2gvb" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "debugger-x2gvb" must set securityContext.capabilities.drop=["ALL"]; container "debugger-x2gvb" must not include "SYS_PTRACE" in securityContext.capabilities.add)
Отказ — от Pod Security Admission из части 30: эфемерный контейнер — часть спецификации пода, и demo под уровнем restricted проверяет его так же, как обычный. Настройки безопасности отладочного контейнера задаёт профиль kubectl debug. В kubectl 1.36 профиль по умолчанию — general: он добавляет SYS_PTRACE, чтобы strace и /proc/<pid>/root чужого процесса работали, и ничего не запрещает. Остальные варианты — baseline и restricted (совместимы с одноимёнными уровнями Pod Security Standards), netadmin (capabilities NET_ADMIN и NET_RAW для tcpdump и ip) и sysadmin (привилегированный контейнер). В неймспейсе restricted проходит только --profile=restricted:
$ kubectl debug -it -n demo hello-865b67cc76-682fc --image=nicolaka/netshoot:v0.15 --target=hello --profile=restricted
Targeting container "hello". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-lxwkm.
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
hello-865b67cc76-682fc ~ id
uid=10001 gid=10001 groups=10001
hello-865b67cc76-682fc ~ ps
PID USER TIME COMMAND
1 10001 0:00 /hello
14 10001 0:00 zsh
22 10001 0:00 ps
hello-865b67cc76-682fc ~ ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 *:8080 *:* users:(("hello",pid=1,fd=4))
hello-865b67cc76-682fc ~ curl -s localhost:8080/healthz
ok
hello-865b67cc76-682fc ~ ls /proc/1/root/
bin dev hello lib proc run sys usr
boot etc home lib64 root sbin tmp var
hello-865b67cc76-682fc ~ exit
Session ended, the ephemeral container will not be restarted but may be reattached using 'kubectl attach hello-865b67cc76-682fc -c debugger-lxwkm -n demo -i -t' if it is still running
netshoot — образ с набором сетевых утилит для отладки: curl, dig, nslookup, tcpdump, ss, ip, iperf3, nmap, mtr и другие. Тег указан явно: правило из части 5 «никаких latest» действует и для отладочных образов. --target=hello помещает эфемерный контейнер в PID namespace контейнера hello: виден его процесс под номером 1, а через /proc/1/root/ — его файловая система. Без --target эфемерный контейнер видит только собственные процессы, если в поде не включён shareProcessNamespace.
Почему всё это сработало без SYS_PTRACE: runAsUser: 10001 задан на уровне пода и действует на все его контейнеры, включая эфемерные, — о том же предупреждал kubectl debug в первой попытке. Отладчик работает с тем же UID, что и приложение, а заглядывать в /proc процессов своего пользователя ядро разрешает без capabilities. Если бы приложение работало от root, понадобился бы SYS_PTRACE, а с ним — неймспейс без restricted и baseline или отладка с узла. Строка All commands and output ... will be recorded in container logs — напоминание kubectl 1.36: ввод и вывод интерактивной сессии попадают в лог эфемерного контейнера, и пароль, набранный в ней, увидит каждый, у кого есть право на pods/log.
Что kubectl debug сделал в API — дописал элемент в спецификацию пода:
$ kubectl get pod hello-865b67cc76-682fc -n demo -o jsonpath='{.spec.ephemeralContainers}' | jq
[
{
"image": "nicolaka/netshoot:v0.15",
"imagePullPolicy": "IfNotPresent",
"name": "debugger-lxwkm",
"resources": {},
"securityContext": {
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": [
"ALL"
]
},
"runAsNonRoot": true,
"seccompProfile": {
"type": "RuntimeDefault"
}
},
"stdin": true,
"targetContainerName": "hello",
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"tty": true
}
]
Удалить эфемерный контейнер нельзя: после выхода из shell он останется в статусе terminated до пересоздания пода. Поэтому на продуктивных подах отладку ведут осознанно, а по завершении под можно перезапустить через kubectl rollout restart.
Права. Всё перечисленное — отдельные подресурсы в RBAC (часть 29): pods/log (get) для логов, pods/exec (create) для exec, pods/ephemeralcontainers (patch) для debug, pods/portforward для port-forward. Право на exec в под — это право на всё, что может процесс внутри, включая его токен ServiceAccount и смонтированные секреты; выдают его тем, кому выдали бы и сами секреты.
Копия пода
Эфемерный контейнер не поможет, если основной контейнер падает сразу после старта: смотреть не в чем. Для этого kubectl debug умеет создать копию пода с изменениями:
$ kubectl debug -n demo hello-798bd968d9-vkz9z -it --copy-to=hello-debug \
--container=hello --profile=restricted -- sh
Здесь копия запускает в контейнере hello вместо приложения sh — если в образе он есть. Для distroless заменяют образ:
$ kubectl debug -n demo hello-798bd968d9-vkz9z --copy-to=hello-debug \
--set-image=hello=busybox:1.37 --container=hello --profile=restricted -it -- sh
--set-image=<контейнер>=<образ> (или *=<образ> для всех) меняет образ, --container говорит, в каком контейнере заменить команду; без него kubectl debug добавил бы в копию ещё один контейнер. Профиль нужен и здесь: general добавил бы SYS_PTRACE самому контейнеру hello копии, и PSA отклонил бы под. restricted заменяет securityContext контейнера своим набором, так что readOnlyRootFilesystem в копии пропадает. Общий PID namespace (--share-processes) в копии включён по умолчанию, так что из отладочного контейнера видны процессы остальных. Копия — это отдельный под без владельца: ReplicaSet о нём не знает, метки по умолчанию не копируются (--keep-labels копирует), а значит он не попадёт в эндпоинты сервиса и не получит пользовательский трафик. Пробы тоже по умолчанию сняты (--keep-liveness, --keep-readiness их сохраняют), чтобы kubelet не убивал копию. После отладки её удаляют руками: kubectl delete pod -n demo hello-debug.
На узле: debug node, crictl, journalctl
Когда проблема ниже пода — контейнер не создаётся, узел NotReady, тома не монтируются, — нужен доступ к самому узлу. SSH есть не всегда, а kubectl debug умеет запустить под прямо на узле:
$ kubectl debug node/k8s-node-2 -it --profile=sysadmin --image=ubuntu:26.04
Creating debugging pod node-debugger-k8s-node-2-8lwdc with container debugger on node k8s-node-2.
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
root@k8s-node-2:/# chroot /host
# crictl ps -a --name hello --state exited | head -3
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
40bef406c15b6 c16806bc0dd63 58 seconds ago Exited hello 4 712c804be67d4 hello-798bd968d9-vkz9z demo
# exit
root@k8s-node-2:/# exit
Session ended, resume using 'kubectl attach node-debugger-k8s-node-2-8lwdc -c debugger -n default -i -t' command
Под node-debugger-... создаётся в текущем неймспейсе (здесь default, без меток PSA) с nodeName, заданным напрямую (планировщик не участвует), в сетевом, PID и IPC namespace узла, а корневая файловая система узла смонтирована в /host через hostPath. Без --profile=sysadmin под не привилегированный, и chroot /host или чтение части /proc не сработают. После chroot /host вы фактически в shell узла: доступны crictl, journalctl, systemctl, файлы kubelet. Такой под — это root на узле, поэтому право создавать поды с hostPath и privileged даётся только администраторам, а Pod Security Admission в неймспейсах вроде demo его и не пропустит. Под удаляется руками: после выхода он остаётся в Completed.
crictl — консольный клиент CRI, того gRPC-интерфейса, через который kubelet управляет containerd (часть 2). Он видит то же, что kubelet, и работает, даже когда API-сервер недоступен. Адрес сокета берётся из /etc/crictl.yaml (runtime-endpoint: unix:///run/containerd/containerd.sock):
user@k8s-node-2:~$ sudo crictl pods --namespace demo
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
712c804be67d4 2 minutes ago Ready hello-798bd968d9-vkz9z demo 0 (default)
f61cf58e184f0 2 minutes ago Ready hello-865b67cc76-6vcck demo 0 (default)
user@k8s-node-2:~$ sudo crictl ps -a --pod 712c804be67d4
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
40bef406c15b6 c16806bc0dd63 About a minute ago Exited hello 4 712c804be67d4 hello-798bd968d9-vkz9z demo
user@k8s-node-2:~$ sudo crictl logs 40bef406c15b6
2026/11/02 13:58:45 listen: listen tcp: lookup tcp/8080 : unknown port
user@k8s-node-2:~$ sudo crictl inspect 40bef406c15b6 | jq '.status | {state, exitCode, reason, logPath}'
{
"state": "CONTAINER_EXITED",
"exitCode": 1,
"reason": "Error",
"logPath": "/var/log/pods/demo_hello-798bd968d9-vkz9z_cab28704-1a6a-40d9-bd16-172760839f9d/hello/4.log"
}
crictl inspect показывает и то, чего в API нет: PID процесса на узле у работающего контейнера (.info.pid), путь к cgroup, спецификацию OCI, с которой runc запустил контейнер, точки монтирования. crictl images — образы на узле, crictl pull — проверить скачивание образа с узла тем же containerd, crictl stats — потребление по контейнерам.
Журналы самих служб — в journald: kubelet и containerd на узлах kubeadm работают как юниты systemd.
user@k8s-node-2:~$ sudo journalctl -u kubelet --since "10 min ago" --no-pager | grep hello-798bd968d9 | tail -1
Nov 02 13:58:56 k8s-node-2 kubelet[763948]: E1102 13:58:56.206825 763948 pod_workers.go:1338] "Error syncing pod, skipping" err="failed to \"StartContainer\" for \"hello\" with CrashLoopBackOff: \"back-off 1m20s restarting failed container=hello pod=hello-798bd968d9-vkz9z_demo(cab28704-1a6a-40d9-bd16-172760839f9d)\"" pod="demo/hello-798bd968d9-vkz9z" podUID="cab28704-1a6a-40d9-bd16-172760839f9d"
user@k8s-node-2:~$ sudo journalctl -u containerd -p warning --since today --no-pager
-- No entries --
Буква в начале строки kubelet — уровень: I info, W warning, E error. Уровень подробности задаётся logging.verbosity в /var/lib/kubelet/config.yaml (kubeadm ставит 0) или флагом -v; 4 и выше — для поиска конкретной ошибки и ненадолго.
Типовые состояния
Pending
Под создан, но не назначен на узел или назначен, но ещё не дошёл до создания песочницы. Если PodScheduled=False, причину называет событие планировщика — одно сообщение со сводкой по всем узлам:
$ kubectl events -n demo --for pod/big
LAST SEEN TYPE REASON OBJECT MESSAGE
60s (x2 over 88s) Warning FailedScheduling Pod/big 0/3 nodes are available: 1 node(s) had untolerated taint(s), 2 Insufficient memory. no new claims to deallocate, preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
Читается так: под big просит 5 ГиБ памяти; из трёх узлов мастер отпал из-за taint, а на двух воркерах не хватает невыделенной памяти — планировщик считает сумму requests уже размещённых подов, а не реальное потребление (часть 10). Вытеснение (preemption) не поможет: на мастер под не пустит taint, а на воркерах не хватит памяти, даже если убрать все поды ниже приоритетом. Фраза no new claims to deallocate — от планирования динамических ресурсов (DRA), без запросов resourceClaims её можно не читать. Типовые фразы и что они значат:
| Фраза в событии | Причина | Что делать |
|---|---|---|
Insufficient cpu / Insufficient memory |
сумма requests на узлах исчерпана | kubectl describe node → «Allocated resources»; уменьшить requests или добавить узел |
had untolerated taint(s) |
на узле taint, у пода нет toleration | kubectl describe node | grep Taints; узел мог получить node.kubernetes.io/disk-pressure сам |
didn't match Pod's node affinity/selector |
nodeSelector или nodeAffinity не находит узлов с такими метками |
kubectl get nodes --show-labels |
didn't match pod anti-affinity rules |
реплик больше, чем узлов, при обязательном anti-affinity | ослабить до preferred… или добавить узлы |
didn't match pod topology spread constraints |
whenUnsatisfiable: DoNotSchedule и перекос по зонам или узлам |
проверить maxSkew и метки топологии |
pod has unbound immediate PersistentVolumeClaims |
PVC в Pending: нет StorageClass, драйвер CSI не создал том |
kubectl describe pvc, логи контроллера CSI-драйвера |
node(s) had volume node affinity conflict |
локальный том привязан к другому узлу | том и под должны встретиться на одном узле |
node(s) were unschedulable |
узел в cordon после drain |
kubectl uncordon |
ContainerCreating
Под назначен, kubelet создаёт песочницу и монтирует тома, но контейнер ещё не запущен. Дольше минуты — повод смотреть события:
Warning FailedCreatePodSandBox Pod/sandbox Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "b099e20b0b04a67fe6cc7f85455d66eb87b745ce177e151e6d09bc89601b9c81": plugin type="calico" failed (add): stat /var/lib/calico/nodename: no such file or directory: check that the calico/node container is running and has mounted /var/lib/calico/
Это CNI: containerd вызвал плагин Calico, а тот не нашёл файла, который пишет агент calico-node при старте. Значит, calico-node на этом узле не запущен или не готов — смотреть kubectl get pods -n calico-system -o wide и его логи. Другие варианты того же события: исчерпан IP-пул, плагин не может достучаться до API-сервера.
Warning FailedMount Pod/no-volume-cm MountVolume.SetUp failed for volume "config" : configmap "hello-settings" not found
Warning FailedMount Pod/data-reader MountVolume.MountDevice failed for volume "pvc-8f1c..." : rpc error: code = DeadlineExceeded desc = context deadline exceeded
Первое — том из ConfigMap или Secret, которого нет в неймспейсе (часть 12). Второе — CSI-драйвер не смонтировал сетевой том за отведённое время: для NFS это недоступный сервер, файрвол на 2049 или экспорт, не разрешающий адрес узла (часть 22); подробности — в логах node-плагина драйвера на этом узле и в dmesg узла.
ErrImagePull и ImagePullBackOff
ErrImagePull — неудачная попытка скачать образ, ImagePullBackOff — kubelet ждёт перед следующей, и пауза растёт от 10 секунд до 5 минут. Причина — в тексте события Failed:
$ kubectl events -n demo --for pod/pull-missing --types=Warning
LAST SEEN TYPE REASON OBJECT MESSAGE
35s (x3 over 81s) Warning Failed Pod/pull-missing Failed to pull image "ghcr.io/example/hello:1.2": rpc error: code = NotFound desc = failed to pull and unpack image "ghcr.io/example/hello:1.2": failed to resolve image: ghcr.io/example/hello:1.2: not found
35s (x3 over 81s) Warning Failed Pod/pull-missing Error: ErrImagePull
8s (x4 over 80s) Warning Failed Pod/pull-missing Error: ImagePullBackOff
| Текст ошибки | Причина |
|---|---|
failed to resolve image: ...hello:1.2: not found |
нет такого тега или опечатка в имени; проверить реестр |
failed to fetch anonymous token: ... 401 Unauthorized |
частный реестр, а containerd пришёл без учётных данных: нет imagePullSecrets у пода или его ServiceAccount, либо секрет в другом неймспейсе |
x509: certificate signed by unknown authority |
у ghcr.io сертификат публичный, так бывает со своим реестром с внутренним CA (раздел «Свой реестр» части 5): containerd не доверяет его CA, нужен файл в /etc/containerd/certs.d/registry.example.com/ на каждом узле |
dial tcp: lookup ghcr.io: no such host |
узел не резолвит имя реестра: DNS узла, а не CoreDNS |
toomanyrequests |
лимит публичного реестра; зеркало или свой реестр |
Самая быстрая проверка — скачать образ с узла тем же containerd: sudo crictl pull <образ>. Для публичного образа это сразу отвечает, есть ли тег и доходит ли узел до реестра. Частный реестр анонимному клиенту отвечает 401 Unauthorized на любой тег, даже существующий, поэтому для него нужен crictl pull --creds <пользователь>:<токен>, и тогда разница между «скачивается с узла» и «не скачивается в под» указывает на учётные данные пода.
CrashLoopBackOff
Контейнер запускается и завершается, kubelet перезапускает его с растущей паузой: 10 с, 20, 40, 80 … до 5 минут; после 10 минут нормальной работы счётчик сбрасывается. Это не причина, а режим ожидания, и причина всегда в прошлом запуске:
kubectl get pod ... -o yaml→lastState.terminated: код выхода иreason.kubectl logs --previous— что приложение написало перед смертью.- Если логов нет и код 127 или 126 — проблема в
command/argsили образе. - Если код 137 и
reason: OOMKilled— следующий раздел. Если 137 без OOM — контейнер убил kubelet: смотреть событияKillingи liveness-пробу, которая могла убивать контейнер, не успевший стартовать. - Если код 0 — процесс просто закончил работу: это не долгоживущая программа, ей место в Job (часть 9).
Когда логов слишком мало, помогает копия пода с sh вместо приложения: запустить бинарник руками и посмотреть, что ему не нравится.
OOMKilled
$ kubectl get pod oom -n demo -o jsonpath='{.status.containerStatuses[0].lastState}' | jq
{
"terminated": {
"containerID": "containerd://4f06596a129437c311776738cf4f195302872e962c70d645c2ac68950af12fe3",
"exitCode": 137,
"finishedAt": "2026-11-02T13:46:55Z",
"reason": "OOMKilled",
"startedAt": "2026-11-02T13:46:55Z"
}
}
Ядро убило процесс, потому что cgroup контейнера достигла memory.max, то есть лимита памяти (часть 10). Подтверждение на узле — в журнале ядра:
user@k8s-node-2:~$ sudo journalctl -k --since "10 min ago" --no-pager | grep -i "out of memory\|oom.group"
Nov 02 13:47:38 k8s-node-2 kernel: Memory cgroup out of memory: Killed process 763094 (tail) total-vm:69260kB, anon-rss:64948kB, file-rss:2060kB, shmem-rss:0kB, UID:0 pgtables:184kB oom_score_adj:982
Nov 02 13:47:38 k8s-node-2 kernel: Tasks in /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod637609c0_506e_417f_b645_73a37ad52b03.slice/cri-containerd-feeeb009a4322979cfd70bb0fd0fca347a47184f9505a94a034f3f3a92f5f11d.scope are going to be killed due to memory.oom.group set
Вторая строка — о том, что kubelet на cgroup v2 включает memory.oom.group: при нехватке памяти ядро убивает не один самый жадный процесс, а все процессы контейнера разом, чтобы контейнер не остался полуживым. Решение — не «поднять лимит, пока не перестанет», а понять, сколько памяти приложению нужно (метрики — в следующей части), и выставить requests и limits по этой цифре. Для рантаймов со своей кучей (JVM, .NET, Node.js) важно, чтобы рантайм знал о лимите и не планировал кучу больше него. Отдельный случай — под выселен kubelet'ом из-за нехватки памяти на всём узле: это Evicted, а не OOMKilled.
Running, но не Ready
Процесс работает, но readiness-проба не проходит, и под исключён из эндпоинтов сервиса:
Warning Unhealthy Pod/ready-path Readiness probe failed: HTTP probe failed with statuscode: 404
Warning Unhealthy Pod/ready-port Readiness probe failed: Get "http://172.20.172.48:8081/readyz": dial tcp 172.20.172.48:8081: connect: connection refused
Первое — приложение отвечает, но не тем: путь в пробе не совпадает с путём в приложении (404) или приложение само считает себя неготовым (503, часто — не дотягивается до базы). Второе — порт в пробе не совпадает с портом приложения или приложение слушает 127.0.0.1 вместо всех адресов: kubelet ходит на IP пода, а не на localhost. Проверить изнутри пода — kubectl debug с netshoot и curl на тот же адрес.
Terminating навсегда
kubectl delete выставил deletionTimestamp, но объект не исчезает. Две независимые причины.
Finalizers. Объект с непустым metadata.finalizers не удаляется, пока каждый контроллер из списка не уберёт свою запись. PVC с kubernetes.io/pvc-protection ждёт, пока его не перестанут использовать поды; неймспейс ждёт удаления всех объектов внутри; CRD-объект ждёт своего оператора — и если оператор удалён раньше, ждать будет вечно:
$ kubectl get pvc data -n demo -o jsonpath='{.metadata.finalizers}'
["kubernetes.io/pvc-protection"]
Снимать finalizer руками (kubectl patch ... --type=merge -p '{"metadata":{"finalizers":null}}') можно, только поняв, что именно он защищает: это отказ от уборки, которую должен был сделать контроллер, — например, от удаления тома в хранилище.
Под, который kubelet не может остановить. Для пода finalizers редки; обычно kubelet не может завершить процесс или размонтировать том. Классика — NFS-том с опцией hard (часть 23), сервер которого недоступен: процесс висит в состоянии D (непрерываемое ожидание ввода-вывода), SIGKILL до него не доходит, umount тоже висит. Это не ошибка опции: hard гарантирует, что запись не потеряется молча, и при возвращении сервера всё продолжится. Лечится возвращением сервера. На узле это видно так:
user@k8s-node-1:~$ ps -eo pid,stat,wchan:32,cmd | awk '$2 ~ /D/'
48211 D rpc_wait_bit_killable /hello
user@k8s-node-1:~$ sudo dmesg | tail -2
[512334.118] nfs: server nfs.example.com not responding, still trying
kubectl delete pod --grace-period=0 --force удаляет только объект в API и ничего не делает на узле: процесс и монтирование остаются. Для Deployment это терпимо, а для StatefulSet опасно — контроллер создаст под с тем же именем и тем же томом на другом узле, пока старый ещё пишет. Если узел мёртв, правильный путь — убедиться, что он выключен, и повесить на него taint node.kubernetes.io/out-of-service=nodeshutdown:NoExecute: тогда Kubernetes штатно отвяжет тома и пересоздаст поды.
Evicted
Kubelet сам выселяет поды в двух случаях. Первый — на узле кончается память, место на диске (nodefs — /var/lib/kubelet, imagefs — образы containerd) или PID'ы. Пороги по умолчанию: memory.available<100Mi, nodefs.available<10%, imagefs.available<15%, nodefs.inodesFree<5% и imagefs.inodesFree<5%. Первыми уходят поды, превысившие свои requests, в порядке класса QoS, а в сообщении будет The node was low on resource: .... Второй — под превысил собственный лимит ephemeral-storage или sizeLimit тома emptyDir. Так выглядит под, записавший 64 МиБ во временный каталог при лимите 32 МиБ:
$ kubectl get pods -n demo disk-eater
NAME READY STATUS RESTARTS AGE
disk-eater 0/1 ContainerStatusUnknown 1 105s
$ kubectl get pod disk-eater -n demo -o jsonpath='{.status.phase} {.status.reason}{"\n"}{.status.message}{"\n"}'
Failed Evicted
Pod ephemeral local storage usage exceeds the total limit of containers 32Mi.
$ kubectl events -n demo --for pod/disk-eater
LAST SEEN TYPE REASON OBJECT MESSAGE
65s Warning Evicted Pod/disk-eater Pod ephemeral local storage usage exceeds the total limit of containers 32Mi.
Колонка STATUS здесь обманывает: она показывает причину завершения контейнера (ContainerStatusUnknown, kubelet убил его вместе с подом), а то, что под выселен, видно только в .status.reason и событии. Выселенный под остаётся в фазе Failed, контроллер создаёт замену, а сами объекты удаляет сборщик мусора только когда завершённых подов в кластере больше --terminated-pod-gc-threshold kube-controller-manager (12 500). Убрать вручную: kubectl delete pods -n demo --field-selector=status.phase=Failed. Причину лечат по случаю: кто пишет логи в файлы внутри контейнера, кому нужен emptyDir с sizeLimit, хватает ли диска под образы.
CreateContainerConfigError
kubelet не может собрать конфигурацию контейнера из того, на что ссылается спецификация:
Warning Failed Pod/no-env-cm Error: configmap "hello-settings" not found
Warning Failed Pod/no-key Error: couldn't find key DB_PASSWORD in Secret demo/hello-db
Warning Failed Pod/root-image Error: container has runAsNonRoot and image will run as root (pod: "root-image_demo(01ba9ce7-a9a3-45ed-b522-127686918b02)", container: busybox)
Нет ConfigMap или Secret, нет ключа в нём (для env.valueFrom — если ссылка не помечена optional: true) или securityContext противоречит образу (часть 30). Как только недостающий объект появится, kubelet сам продолжит запуск.
Узел NotReady
Узел каждые 10 секунд продлевает свой объект Lease в неймспейсе kube-node-lease и обновляет status, когда тот меняется, но не реже раза в 5 минут. Если продлений нет дольше --node-monitor-grace-period (50 секунд в 1.36), контроллер жизненного цикла узлов ставит conditions в Unknown и taint node.kubernetes.io/unreachable; поды с этого узла через 300 секунд (стандартный tolerationSeconds) будут выселены. Так выглядит узел через минуту после systemctl stop kubelet:
$ kubectl describe node k8s-node-2 | sed -n '/Conditions/,/Addresses/p'
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
NetworkUnavailable False Sun, 01 Nov 2026 20:32:25 +0000 Sun, 01 Nov 2026 20:32:25 +0000 CalicoIsUp Calico is running on this node
MemoryPressure Unknown Mon, 02 Nov 2026 13:44:13 +0000 Mon, 02 Nov 2026 13:49:43 +0000 NodeStatusUnknown Kubelet stopped posting node status.
DiskPressure Unknown Mon, 02 Nov 2026 13:44:13 +0000 Mon, 02 Nov 2026 13:49:43 +0000 NodeStatusUnknown Kubelet stopped posting node status.
PIDPressure Unknown Mon, 02 Nov 2026 13:44:13 +0000 Mon, 02 Nov 2026 13:49:43 +0000 NodeStatusUnknown Kubelet stopped posting node status.
Ready Unknown Mon, 02 Nov 2026 13:44:13 +0000 Mon, 02 Nov 2026 13:49:43 +0000 NodeStatusUnknown Kubelet stopped posting node status.
Addresses:
$ kubectl get node k8s-node-2 -o jsonpath='{.spec.taints}' | jq -c '.[]'
{"effect":"NoSchedule","key":"node.kubernetes.io/unreachable","timeAdded":"2026-11-02T13:49:43Z"}
{"effect":"NoExecute","key":"node.kubernetes.io/unreachable","timeAdded":"2026-11-02T13:49:43Z"}
kubelet остановлен в 13:48:53, а Unknown выставлен в 13:49:43 — ровно через 50 секунд. LastHeartbeatTime при этом на пять минут старше: полный status kubelet присылает, только когда что-то изменилось, а живость узла контроллер видит по Lease. NetworkUnavailable ставит не kubelet, а Calico, поэтому он остался False.
Ready=False с причиной — значит, kubelet жив и сам сообщает о проблеме. Например, когда на узле пропала конфигурация CNI, в строке Ready будет container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized. Типовые причины, от частых к редким:
- kubelet не работает:
systemctl status kubelet,journalctl -u kubelet. Частый случай после ручных правок — включённый swap или ошибка в/var/lib/kubelet/config.yaml. - containerd не работает или не отвечает:
systemctl status containerd,crictl info. - CNI не готов — нет конфигурации в
/etc/cni/net.d/, потому чтоcalico-nodeна узле не запустился. Состояние сети containerd показываетcrictl info | jq '.status.conditions'. Если файл конфигурации вернули на место переименованием (mvиз другого каталога), containerd может его не заметить: событие переименования внутрь каталога он пропускает и перечитывает каталог только на следующей записи в него. - Истёк клиентский сертификат kubelet: в журнале
x509: certificate has expired. Kubelet сам продлевает/var/lib/kubelet/pki/kubelet-client-current.pem, если узел был включён; после долгого простоя продление не успевает, и узел приходится заново присоединять. Подробно о сертификатах kubeadm — в части 35. - PLEG (Pod Lifecycle Event Generator) — цикл kubelet, который опрашивает containerd о состоянии всех контейнеров. Если один проход занимает больше трёх минут, kubelet считает себя нездоровым:
PLEG is not healthy: pleg was last seen active 3m5s ago; threshold is 3m0s. Причины — перегруженный containerd, тысячи завершённых контейнеров, зависший на NFS процесс, медленный диск. - Давление на ресурсы —
MemoryPressure,DiskPressure,PIDPressureвTrue; узел готов, но получает taint и перестаёт принимать новые поды. - Расхождение часов: TLS к API-серверу ломается при большом сдвиге времени;
timedatectlна узле.
Сервис без эндпоинтов
Под работает, сервис есть, а запросы к нему получают connection refused или висят:
$ kubectl get endpointslices -n demo -l kubernetes.io/service-name=hello-typo
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
hello-typo-5mzdd IPv4 <unset> <unset> 89s
$ kubectl get svc hello-typo -n demo -o jsonpath='{.spec.selector}'; echo
{"app.kubernetes.io/name":"helo"}
Пустой EndpointSlice (часть 15) — либо селектор сервиса не совпадает с метками подов (как здесь: опечатка), либо подходящие поды есть, но не Ready, и тогда они видны в EndpointSlice с ready: false. Если эндпоинты есть, а трафик не идёт, — targetPort не совпадает с портом контейнера или трафик режет NetworkPolicy (часть 20).
Ошибки DNS
Внутри пода имя не резолвится или резолвится медленно. Проверять из того же неймспейса:
$ kubectl debug -it -n demo hello-865b67cc76-682fc --image=nicolaka/netshoot:v0.15 --profile=restricted -- \
dig +search +short hello
10.100.210.84
$ kubectl debug -it -n demo hello-865b67cc76-682fc --image=nicolaka/netshoot:v0.15 --profile=restricted -- \
dig @10.96.0.10 kubernetes.default.svc.cluster.local +short
10.96.0.1
Если второй запрос не проходит — недоступен сам CoreDNS: его поды (kubectl get pods -n kube-system -l k8s-app=kube-dns), их логи, NetworkPolicy, которая забыла разрешить egress на UDP и TCP 53 к kube-system. Если внутренние имена работают, а внешние медленные — это ndots:5 (часть 16): имя api.example.org содержит меньше пяти точек, и резолвер сначала перебирает три суффикса из search, получая NXDOMAIN, и только потом спрашивает имя как есть — четыре запроса вместо одного, каждый по A и AAAA. Лечится точкой в конце имени в конфигурации приложения (api.example.org.) или dnsConfig.options с ndots: 2 у пода.
Сводная таблица
| Симптом | Частая причина | С чего начать |
|---|---|---|
Deployment 0/3 готово, подов нет |
ReplicaSet не может создать поды: квота, admission | kubectl describe rs, kubectl events --for deploy/... |
Pending |
нет ресурсов, taints, affinity, PVC не привязан | kubectl events --for pod/... → FailedScheduling |
ContainerCreating дольше минуты |
CNI, том не монтируется, нет ConfigMap/Secret для тома | события FailedCreatePodSandBox, FailedMount |
ErrImagePull, ImagePullBackOff |
тег, имя, imagePullSecrets, CA реестра |
текст события Failed, crictl pull на узле |
CrashLoopBackOff |
ошибка приложения или конфигурации | lastState.terminated, kubectl logs --previous |
OOMKilled, код 137 |
лимит памяти мал или утечка | lastState, journalctl -k на узле, метрики памяти |
Running, 0/1 |
readiness не проходит | события Unhealthy, curl из эфемерного контейнера |
Terminating не уходит |
finalizer; процесс в D на недоступном NFS |
metadata.finalizers; ps и dmesg на узле |
Evicted (или ContainerStatusUnknown) |
давление на память, диск или PID узла; лимит ephemeral-storage |
.status.reason и .status.message, kubectl describe node |
CreateContainerConfigError |
нет ConfigMap, Secret или ключа | текст события Failed |
Узел NotReady |
kubelet, containerd, CNI, сертификат, PLEG | describe node, journalctl -u kubelet |
| Сервис не отвечает | селектор, неготовые поды, targetPort, политика |
kubectl get endpointslices -l kubernetes.io/service-name=... |
| Имена не резолвятся или медленно | CoreDNS, egress на 53, ndots:5 |
dig из netshoot, логи CoreDNS |
Смотрим руками
Разберём один случай от начала до конца. Выпущена версия hello:1.1; вместе с ней в ConfigMap hello-config, из которого hello берёт переменные окружения, руками вписали порт PORT. Через десять минут после kubectl set image раскатка не закончилась.
Объект. Deployment сообщает, что новая версия не готова:
$ kubectl rollout status deploy/hello -n demo --timeout=10s
error: deployment "hello" exceeded its progress deadline
$ kubectl get deploy hello -n demo -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Available=True MinimumReplicasAvailable
Progressing=False ProgressDeadlineExceeded
$ kubectl get deploy hello -n demo
NAME READY UP-TO-DATE AVAILABLE AGE
hello 3/3 1 3 11m
Progressing=False с ProgressDeadlineExceeded — за progressDeadlineSeconds (600 секунд по умолчанию) раскатка не продвинулась. Старые поды работают (Available=True): при трёх репликах maxUnavailable по умолчанию (25 %) округляется до нуля, и rolling update из части 7 не убирает старый под, пока новый не готов. Считается не время с начала выкатки, а время с последнего продвижения: если за эти десять минут число готовых реплик выросло хоть раз — скажем, вернулся упавший узел со старыми подами, — отсчёт начинается заново.
События и под. Новая ReplicaSet создала один под, и он в CrashLoopBackOff — вывод выше: код выхода 1, restartCount растёт.
Контейнер. Логи прошлого запуска называют причину:
$ kubectl logs -n demo hello-798bd968d9-vkz9z --previous
2026/11/02 13:58:02 listen: listen tcp: lookup tcp/8080 : unknown port
Порт 8080 приложение не узнало, а между 8080 и двоеточием в сообщении стоит пробел, которого не должно быть. Посмотрим, что приложение получает в переменной. Shell в образе нет, а эфемерный контейнер не поможет: контейнер падает через миллисекунды после старта, и --target не успеет к нему подключиться. Делаем копию с busybox вместо приложения:
$ kubectl debug -n demo hello-798bd968d9-vkz9z --copy-to=hello-debug \
--set-image=hello=busybox:1.37 --container=hello --profile=restricted -it -- sh
/ $ id
uid=10001 gid=10001 groups=10001
/ $ printenv PORT | cat -A
8080^I$
/ $ exit
$ kubectl delete pod -n demo hello-debug
pod "hello-debug" deleted from demo namespace
cat -A показывает невидимые символы: ^I после 8080 — табуляция, $ — конец строки. Значение скопировали из таблицы вместе с табуляцией, а kubectl и YAML честно её сохранили.
Узел, для полноты. Причина найдена, но, пока падающий под ещё существует, посмотрим, как он выглядит на узле — тем же kubectl debug node:
$ kubectl debug node/k8s-node-2 -it --profile=sysadmin --image=ubuntu:26.04
root@k8s-node-2:/# chroot /host
# ls /var/log/pods/demo_hello-798bd968d9-vkz9z_*/hello/
4.log
# exit
root@k8s-node-2:/# exit
$ kubectl get pods -o name | grep node-debugger | xargs kubectl delete
pod "node-debugger-k8s-node-2-8lwdc" deleted from default namespace
Один файл — от последнего запуска (номер 4), по которому kubectl logs и --previous и показывали ошибку. Всё, что старше, уже удалено.
Исправление. Откат делает сервис снова однородным, затем исправленный ConfigMap и повторная раскатка:
$ kubectl rollout undo deploy/hello -n demo
Warning: resource deployments/hello was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/hello rolled back
$ kubectl apply -f hello-config.yaml
configmap/hello-config configured
$ kubectl set image deploy/hello -n demo hello=ghcr.io/example/hello:1.1
deployment.apps/hello image updated
$ kubectl rollout status deploy/hello -n demo
Waiting for deployment spec update to be observed...
Waiting for deployment "hello" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "hello" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "hello" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "hello" rollout to finish: 1 old replicas are pending termination...
deployment "hello" successfully rolled out
Предупреждение при откате — о том, что rollout undo меняет Deployment в обход kubectl apply, и аннотация last-applied-configuration по-прежнему описывает версию 1.1. Правильная привычка — после отката исправить манифест в Git и применить его. Под hello-798bd968d9-vkz9z удалён вместе с файлами логов на узле, а через час исчезнут и события о нём. Если бы этот сбой случился ночью и прошёл сам, утром в кластере не нашлось бы ни одного следа. Именно поэтому дальше нужны метрики и централизованные логи.
Уборка: kubectl delete pod -n demo <отладочные поды> для всего, что создавалось в разделе «Типовые состояния»; эфемерные контейнеры в подах hello исчезнут при следующем перезапуске Deployment.
Ручная диагностика отвечает на вопрос «что сломалось сейчас». Чтобы ответить на «что было ночью», «сколько памяти на самом деле нужно» и «когда мы упрёмся в ресурсы», нужны данные, собранные заранее.
Источники
- Debug Pods и Determine the Reason for Pod Failure — порядок диагностики пода,
lastStateи сообщение о завершении. - Debug Running Pods —
kubectl exec, эфемерные контейнеры, копия пода, профилиkubectl debug. - Ephemeral Containers — что можно и чего нельзя у эфемерного контейнера.
- kubectl debug и kubectl events — справочник флагов.
- Debugging Kubernetes Nodes With Kubectl и Debugging Kubernetes nodes with crictl —
kubectl debug nodeиcrictl. - Pod Lifecycle — фазы, conditions, состояния контейнера, пауза перезапуска.
- Deployments: Failed Deployment —
progressDeadlineSecondsиProgressDeadlineExceeded. - Resource Management for Pods and Containers и Node-pressure Eviction — лимиты памяти и
ephemeral-storage, пороги выселения. - Node Status и Node Shutdowns — conditions и Lease узла, taint
out-of-service. - Finalizers — почему объект застревает в удалении.
- Debug Services и Debugging DNS Resolution — сервис без эндпоинтов, проверки CoreDNS.
- nicolaka/netshoot — состав отладочного образа.
Дальше — часть 32: метрики и автомасштабирование. metrics-server и kubectl top, HorizontalPodAutoscaler и его алгоритм, Prometheus с kube-prometheus-stack, запросы PromQL и правила алертов.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.