Pod и контейнеры
Что нужно знать перед уроком
- M0.03 — Практический кластер — нужен работающий kind + Deckhouse CE и настроенные kubectl/d8 k
Что нужно знать перед уроком
M0 целиком — у вас должен быть работающий кластер (kubectl get nodes возвращает Ready) и понимание, что Kubernetes — оркестратор контейнеров, работающий декларативно (M0 урок 1). Понятие «контейнер» как упакованное приложение — если незнакомо совсем, глубокий разбор ждёт в M3 уроке 1, но для этого урока достаточно интуитивного понимания.
Теория
Почему Pod, а не просто «контейнер»
Первое, что удивляет новичков: в Kubernetes нет объекта «контейнер» как единицы деплоя — минимальная единица называется Pod. Даже если вам нужен ровно один контейнер, Kubernetes всё равно оборачивает его в Pod. Причина — Pod решает задачу совместного размещения (co-location) нескольких контейнеров, которым нужно делить одно и то же сетевое пространство и файловую систему (volumes), но при этом оставаться отдельными процессами с отдельным жизненным циклом сборки.
Все контейнеры одного Pod’а:
- получают общий IP-адрес и порты — обращение
localhost:8080из одного контейнера попадёт в другой контейнер того же Pod’а, если тот слушает 8080; - могут делить volumes — один контейнер пишет файл, другой тут же может его прочитать;
- планируются на один и тот же узел (node) целиком — Pod невозможно «разрезать» между узлами.
Практика: чуть позже в модуле (probes, volumes) вы увидите, что реальные production Pod’ы почти всегда содержат больше одного контейнера — просто один из них «основной» (обычно называемый пользовательским термином app container), а остальные — вспомогательные.
Pod phases и container states — две разные системы координат
Новичков сбивает то, что у Pod’а есть фаза (status.phase), а у каждого контейнера внутри — своё состояние (state), и это не одно и то же.
Pod phases (status.phase, всего 5 значений):
| Фаза | Значение |
|---|---|
Pending | Pod принят кластером, но хотя бы один контейнер ещё не запущен (образ качается, Pod ждёт планирования на узел) |
Running | Pod привязан к узлу, все контейнеры созданы, хотя бы один запущен или запускается/перезапускается |
Succeeded | все контейнеры завершились успешно (код 0) и не будут перезапущены — типично для Job |
Failed | хотя бы один контейнер завершился с ошибкой и не будет перезапущен |
Unknown | состояние Pod’а не удалось определить (обычно — потеря связи с узлом) |
Container states (внутри status.containerStatuses[].state, три значения): Waiting (контейнер ещё не запущен — качается образ, ждёт зависимостей), Running (выполняется), Terminated (завершился, с кодом возврата и, если применимо, причиной — Completed/Error/OOMKilled и т.д.).
Важно: Pod может быть в фазе Running, а один из его контейнеров при этом — в состоянии Waiting (например, ждёт initContainer перед собой) или уже Terminated с ненулевым кодом, ожидая перезапуска по restartPolicy. Именно поэтому диагностику всегда нужно вести на двух уровнях: kubectl get pod показывает агрегированную сводку, а kubectl describe pod — детальные состояния каждого контейнера.
initContainers: обычные и нативные sidecar
initContainers — список контейнеров, которые Kubernetes запускает строго последовательно, до старта основных контейнеров, и каждый должен завершиться успешно (код 0), прежде чем запустится следующий. Классическое применение — миграции БД, ожидание доступности зависимого сервиса, подготовка конфигурации.
| |
Начиная с Kubernetes 1.29 (стабильная фича) появились нативные sidecar-контейнеры — тот же список initContainers, но с явным restartPolicy: Always у конкретного элемента:
| |
Ключевое отличие: обычный initContainer запускается один раз и завершается до старта основных контейнеров; нативный sidecar (с restartPolicy: Always) стартует как init (то есть до основных контейнеров), но продолжает работать все время жизни Pod’а параллельно с основными контейнерами, и останавливается последним, причём если sidecar’ов несколько — в порядке, обратном порядку их объявления в манифесте. Это устраняет старый паттерн «sidecar-контейнер как обычный контейнер, который просто должен переживать основной» — раньше это решалось хаками (проверка через preStop/скрипты), теперь — декларативно самим Kubernetes.
restartPolicy — на уровне Pod’а и на уровне sidecar-контейнера
spec.restartPolicy Pod’а — одно из Always (по умолчанию, перезапускать контейнер всегда после завершения — типично для сервисов), OnFailure (перезапускать только при ненулевом коде — типично для Job), Never (никогда не перезапускать). Эта политика применяется kubelet’ом на узле к обычным контейнерам Pod’а. Особенность нативных sidecar-контейнеров — у конкретного sidecar-контейнера может быть собственный restartPolicy: Always, отличный от restartPolicy самого Pod’а (например, Pod с restartPolicy: Never, как в Job, но с sidecar’ом, который в любом случае должен переживать основной контейнер, пока тот не завершится).
Graceful shutdown: terminationGracePeriodSeconds и preStop
Когда Kubernetes решает остановить Pod (масштабирование вниз, обновление, удаление), происходит следующее:
- Pod помечается как «завершающийся» (в
kubectl get podsувидитеTerminating), из Endpoints Service (см. M1 урок 3) он убирается немедленно — новый трафик перестаёт приходить. - Если у контейнера задан хук
preStop, kubelet выполняет его параллельно с отправкой сигналаSIGTERMосновному процессу контейнера. - Процессу даётся время —
terminationGracePeriodSeconds(по умолчанию 30 секунд) — на самостоятельное корректное завершение (доработать текущие запросы, закрыть соединения с БД). - Если по истечении этого времени контейнер всё ещё жив — kubelet посылает
SIGKILL(принудительное завершение, без возможности приложению «доделать» что-либо).
| |
Если приложение не обрабатывает SIGTERM вообще (многие простые процессы просто игнорируют его или завершаются мгновенно без доработки запросов) — вы получите резкие обрывы соединений при каждом деплое. Это одна из самых частых причин «пятисоток» во время rollout, и первое, что стоит проверить, если увидели такую картину.
requests/limits и три QoS-класса
Каждый контейнер может декларировать resources.requests (гарантированный минимум CPU/памяти, используется scheduler’ом для выбора узла) и resources.limits (жёсткий потолок: превышение лимита памяти приводит к убийству контейнера OOM Killer’ом, превышение лимита CPU — не к убийству, а к throttling’у).
На основе соотношения requests/limits всех контейнеров Pod’а Kubernetes присваивает Pod’у один из трёх QoS-классов:
| QoS-класс | Условие | Поведение при нехватке памяти на узле |
|---|---|---|
Guaranteed | у всех контейнеров requests == limits для обоих ресурсов (CPU и память) | убивается OOM Killer’ом последним |
Burstable | хотя бы один контейнер задаёт requests, но не выполняется условие Guaranteed | убивается раньше Guaranteed, но позже BestEffort |
BestEffort | ни один контейнер не задаёт requests/limits вообще | убивается первым при нехватке памяти на узле |
| |
Прямая практическая связь: если ваш Application критичен для доступности, задавайте requests/limits, стремясь к Guaranteed для самых важных компонентов — иначе при нехватке ресурсов на узле именно ваш Pod рискует быть убит первым, даже если проблема вызвана соседним «прожорливым» приложением.
Частые ошибки и подводные камни
- Забыть про
terminationGracePeriodSecondsи получить обрывы при каждом деплое. Особенно критично для приложений с длительными запросами (стриминг, загрузка файлов) — безpreStop-задержки Service уберёт Pod из Endpoints, но уже принятые соединения могут обрубиться раньше времени. - Путать
BestEffortс «безопасным по умолчанию». Если не указатьresourcesвообще — Pod становится первой жертвой OOM Killer’а на перегруженном узле, причём непредсказуемо (убьётся сама память-жора, а не обязательно виновник). - Ожидать, что
initContainerможно перезапускать бесконечно параллельно с основным. ОбычныйinitContainerбезrestartPolicy: Always— строго последовательный и одноразовый; если нужен постоянный компаньон — это именно нативный sidecar.
Linux / Сети / Docker
Pod по факту реализован через сетевой namespace Linux (network namespace) — общий для всех контейнеров одного Pod’а. Технически создаётся специальный «инфраструктурный» контейнер (pause-контейнер), который держит этот namespace открытым, а все остальные контейнеры Pod’а «присоединяются» к нему через --net=container:<pause-id> (на уровне containerd/Docker). Именно поэтому все контейнеры Pod’а видят один и тот же localhost и один и тот же набор сетевых интерфейсов — с точки зрения Linux-ядра это буквально общий network namespace, а не «магия Kubernetes».
| |
Практика в кластере
Excalidraw — простой self-hosted редактор диаграмм на React, разворачивается одним статическим контейнером (готовый образ excalidraw/excalidraw — nginx с отданными статическими файлами) — минимальная сложность, идеальная для первого Pod’а курса.
- Создайте namespace и Pod:
| |
- Проверьте фазу Pod’а и состояние контейнера:
| |
Обратите внимание в выводе describe на секции State, QoS Class (должен быть Burstable, т.к. requests != limits) и Events внизу — именно там видна вся история планирования и старта.
- Проверьте логи и доступ внутрь контейнера:
| |
- Проверьте доступность приложения через port-forward (Service мы разберём в следующем уроке, а сейчас — временный туннель напрямую в Pod):
| |
- Смоделируйте graceful shutdown и посмотрите на фазу
Terminating:
| |
Практика разработки
Сборки образов в этом уроке нет — используется готовый официальный образ excalidraw/excalidraw. Первая собственная сборка через werf появится в M3, когда у нас будет исходный код, который нужно собрать самостоятельно.
Шпаргалка команд урока
| |
Общая шпаргалка: 📋 kubectl .
Вопросы для самопроверки
Pod — единица co-location: общий сетевой namespace (localhost между контейнерами) и общие volumes, при этом каждый контейнер — отдельный процесс со своим образом.
Источник: Kubernetes — pods
restartPolicy: Always у элемента initContainers превращает его в sidecar: стартует до основных контейнеров, но не завершается вместе с обычными init-контейнерами, а живёт до конца, останавливаясь последним, в обратном порядке объявления.
Источник: Kubernetes — pod lifecycle
После истечения terminationGracePeriodSeconds (по умолчанию 30с) kubelet принудительно завершает процесс сигналом SIGKILL.
Источник: Kubernetes — pod lifecycle
Guaranteed требует, чтобы requests равнялись limits и для CPU, и для памяти у всех контейнеров Pod'а — именно этот случай.
Источник: Kubernetes — pod qos
Pod phase и container state — разные системы координат; Running у Pod'а означает, что Pod привязан к узлу и хотя бы один контейнер запущен/запускается, остальные могут быть в Waiting.
Источник: Kubernetes — pod lifecycle
Рекомендуемая литература
Официальная документация
- kubernetes.io — concepts/workloads/pods/pod-lifecycle
- kubernetes.io — Pods
- kubernetes.io — Pod Quality of Service Classes
Статьи и блоги
- Kubernetes для начинающих: что такое Pod и как с ним работать — Flant на Habr.
- Основные концепции сетевой архитектуры Kubernetes — Habr.
- Миграция с ingress-nginx: выбор нового контроллера — Habr.
Книги
- Marko Lukša. Kubernetes in Action. 2nd ed., 2022. ISBN 978-1617297618.
- Kelsey Hightower, Brendan Burns, Joe Beda. Kubernetes: Up and Running. 3rd ed., 2022. ISBN 978-1098110201.
- Michael Hausenblas, Stefan Schimanski. Programming Kubernetes. 2019. ISBN 978-1492047107.
Связанные материалы
- Предыдущий урок: M0.03 — Практический кластер.
- Следующий урок: M1.02 — Deployment и ReplicaSet.
- Глоссарий: Pod.
- Шпаргалка: 📋 kubectl .
✓ Урок пройден — все вопросы самопроверки отвечены верно