Pod и контейнеры

Уровень: beginner ~60 мин Практика: Excalidraw (MIT)

Что нужно знать перед уроком

  • 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 значений):

ФазаЗначение
PendingPod принят кластером, но хотя бы один контейнер ещё не запущен (образ качается, Pod ждёт планирования на узел)
RunningPod привязан к узлу, все контейнеры созданы, хотя бы один запущен или запускается/перезапускается
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), прежде чем запустится следующий. Классическое применение — миграции БД, ожидание доступности зависимого сервиса, подготовка конфигурации.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
  name: excalidraw-demo
spec:
  initContainers:
    - name: wait-for-network
      image: busybox:1.36
      command: ["sh", "-c", "echo 'подготовка завершена'; sleep 2"]
  containers:
    - name: excalidraw
      image: excalidraw/excalidraw:latest
      ports:
        - containerPort: 80

Начиная с Kubernetes 1.29 (стабильная фича) появились нативные sidecar-контейнеры — тот же список initContainers, но с явным restartPolicy: Always у конкретного элемента:

1
2
3
4
5
initContainers:
  - name: log-shipper-sidecar
    image: log-shipper:latest
    restartPolicy: Always # именно этот флаг превращает init-контейнер в sidecar
    # ... дальше живёт всё время работы Pod'а

Ключевое отличие: обычный 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 (масштабирование вниз, обновление, удаление), происходит следующее:

  1. Pod помечается как «завершающийся» (в kubectl get pods увидите Terminating), из Endpoints Service (см. M1 урок 3) он убирается немедленно — новый трафик перестаёт приходить.
  2. Если у контейнера задан хук preStop, kubelet выполняет его параллельно с отправкой сигнала SIGTERM основному процессу контейнера.
  3. Процессу даётся время — terminationGracePeriodSeconds (по умолчанию 30 секунд) — на самостоятельное корректное завершение (доработать текущие запросы, закрыть соединения с БД).
  4. Если по истечении этого времени контейнер всё ещё жив — kubelet посылает SIGKILL (принудительное завершение, без возможности приложению «доделать» что-либо).
1
2
3
4
5
6
7
8
9
containers:
  - name: excalidraw
    image: excalidraw/excalidraw:latest
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 5"] # дать балансировщику время убрать Pod из ротации
spec:
  terminationGracePeriodSeconds: 45

Если приложение не обрабатывает 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 вообщеубивается первым при нехватке памяти на узле
1
2
3
4
5
6
7
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "250m"
    memory: "256Mi"

Прямая практическая связь: если ваш 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».

1
2
# внутри узла (для понимания, не типичная команда разработчика)
docker ps | grep pause      # инфраструктурный контейнер Pod'а

Практика в кластере

🧩 Практика урока: Excalidraw · лицензия MIT

Excalidraw — простой self-hosted редактор диаграмм на React, разворачивается одним статическим контейнером (готовый образ excalidraw/excalidraw — nginx с отданными статическими файлами) — минимальная сложность, идеальная для первого Pod’а курса.

  1. Создайте namespace и Pod:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kubectl create namespace course-m1-pod
kubectl apply -n course-m1-pod -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: excalidraw
  labels:
    app: excalidraw
spec:
  terminationGracePeriodSeconds: 20
  containers:
    - name: excalidraw
      image: excalidraw/excalidraw:latest
      ports:
        - containerPort: 80
      resources:
        requests:
          cpu: "50m"
          memory: "64Mi"
        limits:
          cpu: "200m"
          memory: "128Mi"
EOF
  1. Проверьте фазу Pod’а и состояние контейнера:
1
2
kubectl get pod excalidraw -n course-m1-pod -o wide
kubectl describe pod excalidraw -n course-m1-pod

Обратите внимание в выводе describe на секции State, QoS Class (должен быть Burstable, т.к. requests != limits) и Events внизу — именно там видна вся история планирования и старта.

  1. Проверьте логи и доступ внутрь контейнера:
1
2
kubectl logs excalidraw -n course-m1-pod
kubectl exec -it excalidraw -n course-m1-pod -- sh -c "ps aux"
  1. Проверьте доступность приложения через port-forward (Service мы разберём в следующем уроке, а сейчас — временный туннель напрямую в Pod):
1
2
kubectl port-forward -n course-m1-pod pod/excalidraw 8080:80
# откройте http://localhost:8080
  1. Смоделируйте graceful shutdown и посмотрите на фазу Terminating:
1
2
kubectl delete pod excalidraw -n course-m1-pod --wait=false
kubectl get pod excalidraw -n course-m1-pod -w    # понаблюдайте переход в Terminating и исчезновение

Практика разработки

Сборки образов в этом уроке нет — используется готовый официальный образ excalidraw/excalidraw. Первая собственная сборка через werf появится в M3, когда у нас будет исходный код, который нужно собрать самостоятельно.

Шпаргалка команд урока

1
2
3
4
5
6
7
8
kubectl create namespace <name>
kubectl apply -f <file> [-n <ns>]
kubectl get pod <name> [-n <ns>] [-o wide|-o yaml]
kubectl describe pod <name> [-n <ns>]
kubectl logs <pod> [-n <ns>] [-c <container>] [-f]
kubectl exec -it <pod> [-n <ns>] -- <cmd>
kubectl port-forward pod/<name> <local-port>:<pod-port> [-n <ns>]
kubectl delete pod <name> [-n <ns>] [--wait=false]

Общая шпаргалка: 📋 kubectl .

Вопросы для самопроверки

Почему Kubernetes использует Pod, а не запускает контейнеры напрямую?

Pod — единица co-location: общий сетевой namespace (localhost между контейнерами) и общие volumes, при этом каждый контейнер — отдельный процесс со своим образом.

Источник: Kubernetes — pods

Чем нативный sidecar-контейнер (initContainer с restartPolicy: Always) отличается от обычного initContainer?

restartPolicy: Always у элемента initContainers превращает его в sidecar: стартует до основных контейнеров, но не завершается вместе с обычными init-контейнерами, а живёт до конца, останавливаясь последним, в обратном порядке объявления.

Источник: Kubernetes — pod lifecycle

Что произойдёт с контейнером, если основной процесс не завершится сам за terminationGracePeriodSeconds после SIGTERM?

После истечения terminationGracePeriodSeconds (по умолчанию 30с) kubelet принудительно завершает процесс сигналом SIGKILL.

Источник: Kubernetes — pod lifecycle

Pod с requests.cpu=100m/limits.cpu=100m и requests.memory=128Mi/limits.memory=128Mi у единственного контейнера — какой у него QoS-класс?

Guaranteed требует, чтобы requests равнялись limits и для CPU, и для памяти у всех контейнеров Pod'а — именно этот случай.

Источник: Kubernetes — pod qos

Pod находится в фазе Running, но kubectl describe показывает у одного из контейнеров state: Waiting. Это нормально?

Pod phase и container state — разные системы координат; Running у Pod'а означает, что Pod привязан к узлу и хотя бы один контейнер запущен/запускается, остальные могут быть в Waiting.

Источник: Kubernetes — pod lifecycle

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • 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.

Связанные материалы