← Kubernetes: разбор поломок
7 мин · средне · Урок 2 из 12

livenessProbe рестартит контейнер

Как отличить упавшее приложение от криво настроенной probe и почему логи нужно читать с флагом --previous.

Deployment раскатан, образ правильный, приложение внутри контейнера работает. При этом счётчик RESTARTS растёт, а статус скачет между Running и CrashLoopBackOff.

NAME                  READY  STATUS             RESTARTS  AGE
app-7c9d8f4b6-x2k4j   0/1    CrashLoopBackOff   4         2m

Классический случай: контейнер убивает не сбой в приложении, а livenessProbe - проверка, по которой kubelet решает, жив ли контейнер. Ниже разбор, как это выглядит в диагностике.

Что показывает kubectl

Смотреть стоит в трёх местах и именно в таком порядке.

1. kubectl get pod

Первое, что замечают глаза: RESTARTS больше нуля и растёт.

READY 0/1 значит, что контейнер запущен, но не прошёл readinessProbe (проверку готовности принимать трафик) или вообще упал. Четыре рестарта за две минуты это уже паттерн, а не случайная ошибка.

2. kubectl describe pod

Эта команда показывает три вещи, которых нет в get:

Типичный вывод при битой liveness:

State:        Running
Last State:   Terminated
  Reason:     Error
  Exit Code:  137
Events:
  Warning  Unhealthy  liveness probe failed: Get "http://10.42.0.7:8081/": dial tcp 10.42.0.7:8081: connect: connection refused
  Normal   Killing    Container app failed liveness probe, will be restarted

Exit code 137 это 128 + SIGKILL(9). Контейнер прибил kubelet за то, что probe три раза подряд (по умолчанию failureThreshold: 3) не получила ответ.

Ключевое слово в Events - connection refused по адресу с портом 8081. Приложение на этом порту не слушает.

3. kubectl logs --previous

kubectl logs pod показывает текущий запуск контейнера. Если он только что рестартанул, в выводе будут свежие строки, а не те, где случилась проблема. Флаг --previous (или -p) даёт логи прошлого запуска.

kubectl logs app-7c9d8f4b6-x2k4j --previous

Вывод вроде listening on 0.0.0.0:8080 рядом с probe, которая бьёт в :8081, закрывает вопрос о причине.

Состояния, которые путают

СтатусЧто значит
PendingPod не запланирован или образ тянется. Смотри Events.
RunningКонтейнер запущен. Готовность к трафику это отдельный вопрос.
CrashLoopBackOffКонтейнер падает, kubelet увеличивает паузу между рестартами (10s, 20s, 40s, 80s, до 5 минут).
ErrorКонтейнер вышел с ненулевым exit code при restartPolicy: Never.
CompletedВышел с exit 0. Для Job это норма, для Deployment плохо.
ImagePullBackOffОбраз не качается: опечатка в имени, нет авторизации, нет интернета.
CreateContainerConfigErrorPod ссылается на ConfigMap или Secret, которого нет.
OOMKilledПревысил resources.limits.memory. Видно в Last State.

Probe против реального состояния приложения

RESTARTS растёт kubectl describe pod Events: Liveness probe failed в сообщении адрес и порт Last State: OOMKilled, Error про probe в событиях ни слова probe бьёт не туда порт из шаблона не совпал упало само приложение чинить процесс, не probe
Развилка одна: упоминает ли событие Killing про probe.

livenessProbe отвечает на вопрос «жив ли контейнер». Если probe говорит «нет» три раза подряд, kubelet рестартит контейнер. Дальше возможны два сценария.

Первый: приложение действительно упало или зависло. Probe сработала правильно, чинить надо приложение.

Второй: probe настроена криво. Приложение живо, но проверка бьёт не туда - неправильный порт, неправильный путь, слишком маленький initialDelaySeconds для медленного старта. Этот вариант встречается чаще. Особенно когда контейнер слушает на 8080 (типично для unprivileged-образов nginx), а в probe остался порт 80 из шаблона.

Медленный старт под liveness выглядит как бесконечный CrashLoopBackOff: контейнер не успевает подняться за initialDelaySeconds, и его убивают снова. Лечит startupProbe, а не отключение проверки.

Как читать Events

kubectl describe pod в конце даёт таблицу:

Events:
  Type     Reason     Age   From     Message
  Warning  Unhealthy  10s   kubelet  Liveness probe failed: ...
  Normal   Killing    10s   kubelet  Container app failed liveness probe
  Normal   Pulled     11s   kubelet  Container image "..." already present
  Normal   Created    11s   kubelet  Created container app
  Normal   Started    11s   kubelet  Started container app

Читать надо снизу вверх по времени (Age): сначала Started, потом Killing, потом снова Started. Цикл повторяется. Reason Unhealthy со словами Liveness probe failed указывает прямо на probe.

Если бы контейнер убивала реальная смерть процесса, картина была бы другой: OOMKilled в Last State и события Killing без упоминания probe.

Как чинят

Порт в probe сверяют с портом, на котором реально слушает приложение:

kubectl get deploy app -o jsonpath='{.spec.template.spec.containers[0].livenessProbe}'

Дальше правят шаблон Deployment, а не Pod:

kubectl patch deploy app --type=json \
  -p='[{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/port","value":8080}]'

или через kubectl edit deploy app. Deployment создаёт новый ReplicaSet, старые Pod уезжают, у новых счётчик RESTARTS остаётся на нуле. Это и есть признак того, что починка сработала.

Как не допустить

Порт в probe и containerPort берут из одного места. Когда в манифесте используется именованный порт, probe ссылается на имя, а не на число:

ports:
  - name: http
    containerPort: 8080
livenessProbe:
  httpGet:
    path: /healthz
    port: http

Тогда смена порта в одном месте не оставляет probe с устаревшим значением.

Второе правило: liveness и readiness проверяют разное. Readiness убирает Pod из балансировки, пока он не готов, и не убивает контейнер. Liveness убивает.

Домашка

Теорию закрепляет задание на своём кластере: 1 шаг с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.

./run.sh start debug-probe     # развернуть стартовое состояние
./run.sh verify debug-probe    # проверить текущий шаг

Коротко

Прошли материал до конца? Отметьте урок пройденным.