Наблюдаемость и диагностика

Когда под не работает, кластер не угадывает причину за вас — он лишь даёт инструменты; учимся читать метрики, логи и события и чинить падающие поды.

Наблюдаемость (observability) — способность по внешним сигналам (метрики, логи, события) понять, что происходит внутри системы, не залезая в неё отладчиком.

Зачем это на практике

В базовом разделе мы настраивали liveness- и readiness-пробы, чтобы кластер сам перезапускал нездоровые поды. Но самовосстановление — это половина дела: оно лечит симптом, а инженеру нужно найти причину. Под рестартится каждые 30 секунд — почему? Сервис отдаёт 500 — где затык? Без наблюдаемости вы отвечаете на эти вопросы гаданием и перезапусками наугад. С ней — за минуты находите корень. Наблюдаемость стоит на трёх китах: метрики (что), логи (детали), события (что сделал сам кластер).

Метрики: Prometheus и Grafana

Стандарт де-факто для метрик в Kubernetes — Prometheus. Его модель — pull: Prometheus сам периодически опрашивает (scrape) HTTP-эндпоинт /metrics каждого приложения и узла, складывая значения в свою time-series базу. Приложение лишь выставляет метрики в текстовом формате:

# HELP http_requests_total Всего HTTP-запросов
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 14237
http_requests_total{method="GET",status="500"} 12

Запросы к этим данным пишут на языке PromQL. Например, скорость ошибок в секунду за 5 минут:

rate(http_requests_total{status="500"}[5m])

Grafana — это слой визуализации поверх Prometheus: дашборды, графики, алерты. Связка стандартная: Prometheus собирает и хранит, Grafana показывает. Метрики отвечают на вопрос «что не так» на уровне трендов — рост latency, всплеск ошибок, утечка памяти видны именно здесь раньше, чем кто-то пожалуется.

Логи в кластере

Приложение в Kubernetes должно писать логи в stdout/stderr, а не в файл внутри контейнера. Причина: под эфемерен — при пересоздании его файловая система исчезает вместе с логами. Kubernetes же подхватывает stdout/stderr и хранит их, пока жив под:

# логи текущего контейнера
kubectl logs web

# логи предыдущего, упавшего экземпляра — бесценно при CrashLoop
kubectl logs web --previous

# живой поток + последние 100 строк
kubectl logs web -f --tail=100

Флаг --previous — ключевой при отладке падений: он показывает логи прошлого контейнера, который уже умер и был заменён. Именно там лежит сообщение об ошибке, из-за которой под упал. Поскольку kubectl logs теряет историю при пересоздании пода, в проде логи собирают централизованно (агент вроде Fluent Bit на каждом узле шлёт их в Loki или Elasticsearch), но контракт тот же: пишите в stdout.

События: что делал сам кластер

Метрики и логи — про приложение. А что делал сам Kubernetes? Почему под не запланирован, почему образ не скачался? Это рассказывают события (Events) — журнал действий control plane:

kubectl get events --sort-by=.lastTimestamp

# или сразу всё про конкретный под — события внизу вывода
kubectl describe pod web

Команда kubectl describe — главный инструмент диагностики: внизу её вывода всегда секция Events с хронологией («Scheduled», «Pulling image», «Failed», «Back-off restarting»). Именно отсюда узнаёшь, что узел отказал в размещении из-за нехватки памяти или что реестр вернул «нет доступа».

Отладка CrashLoopBackOff по шагам

CrashLoopBackOff — самый частый «красный» статус. Он значит: контейнер запускается, почти сразу падает, Kubernetes ждёт нарастающую паузу (back-off: 10с, 20с, 40с… до 5 минут) и пробует снова. Это не ошибка планирования — под размещён, но приложение внутри умирает. Алгоритм разбора:

# 1. что вообще со статусом и сколько рестартов
kubectl get pod web
# web   0/1   CrashLoopBackOff   5 (40s ago)   3m

# 2. почему упал ПРЕДЫДУЩИЙ запуск — здесь обычно вся правда
kubectl logs web --previous

# 3. что говорит сам кластер: причина и exit code
kubectl describe pod web

Типичные причины и куда смотреть:

СимптомВероятная причина
exit 137 в describeOOMKilled — мало limits.memory
в логах трассировка/ошибка конфигурациибаг приложения, нет переменной окружения, недоступна БД
падает мгновенно, логи пустыневерная команда/ENTRYPOINT, нет файла, опечатка в пути
liveness-проба валит здоровый подслишком жёсткий initialDelaySeconds — приложение не успевает стартовать

Отдельно стоит ImagePullBackOff — это уже не падение приложения, а невозможность скачать образ: опечатка в теге, приватный реестр без imagePullSecrets, недоступность registry. Логов приложения тут нет вовсе (контейнер не стартовал) — ответ ищут в kubectl describe.

Как это работает под капотом

Статус пода формирует kubelet на узле: он запускает контейнер, ловит его код выхода и, если контейнер завершился, применяет политику рестарта с экспоненциальным back-off. Само слово BackOff в статусе — это и есть пауза перед очередной попыткой, растущая вдвое, чтобы не молотить кластер бесконечными перезапусками падающего контейнера. События же — это объекты Kubernetes (kind Event), которые компоненты (scheduler, kubelet, controller) создают в API; у них короткий TTL (по умолчанию около часа), поэтому события «протухают» — разбирать инцидент нужно по горячим следам или иметь централизованный сбор. А kubectl logs не хранит ничего сам: он в реальном времени читает лог-файл контейнера на узле через kubelet, и как только под пересоздан — старый файл недоступен (кроме как через --previous, и то лишь для одного прошлого экземпляра).

Частые ошибки

  • Смотреть kubectl logs без --previous при CrashLoop. Текущий контейнер только что стартовал и ещё «чистый»; причина — в логах упавшего предыдущего.
  • Игнорировать секцию Events в describe. Половина диагнозов (OOM, ошибка размещения, проблема образа) лежит именно там.
  • Писать логи в файл внутри контейнера. Они не видны в kubectl logs и исчезают при пересоздании пода.
  • Путать CrashLoopBackOff с ImagePullBackOff. Первый — приложение падает (есть логи), второй — образ не скачался (логов нет, смотри describe).
  • Лечить CrashLoop перезапусками. kubectl delete pod лишь сбрасывает счётчик back-off; причина никуда не делась — её надо найти, а не перезапускать.

Итоги

  • Три источника правды: метрики (тренды, Prometheus/Grafana), логи (детали, stdout) и события (действия кластера).
  • Prometheus собирает метрики по pull-модели с эндпоинтов /metrics, запросы пишут на PromQL, графики строит Grafana.
  • Логи пишите в stdout/stderr; kubectl logs --previous показывает причину падения упавшего контейнера.
  • kubectl describe pod и секция Events — первое, что смотрят при любой проблеме пода.
  • CrashLoopBackOff = приложение падает по кругу (back-off растёт); ищите причину в логах и exit code, а не перезапускайте.
Проверьте себя
1. Какая команда покажет причину падения контейнера, который уже перезапустился в CrashLoopBackOff?
Akubectl logs web
Bkubectl logs web --previous
Ckubectl get pod web
Dkubectl top pod web
2. Чем ImagePullBackOff отличается от CrashLoopBackOff?
AЭто одно и то же, просто разные названия
BImagePullBackOff — образ не удалось скачать (логов приложения нет), CrashLoopBackOff — приложение запустилось и упало
CImagePullBackOff означает нехватку памяти, CrashLoopBackOff — нехватку CPU
DImagePullBackOff бывает только у StatefulSet
3. По какой модели Prometheus собирает метрики приложений?
APush: приложение само отправляет метрики в Prometheus
BPull: Prometheus периодически опрашивает HTTP-эндпоинт /metrics приложения
CМетрики передаются только через kubectl logs
DPrometheus читает метрики напрямую из etcd