🐳 Раздел 14 · Вопрос #9

Что такое Pod в Kubernetes

Kubernetes никогда не запускает отдельные Docker-контейнеры напрямую; он всегда оборачивает их в абстракцию Pod.


🟢 Junior Level

30-секундный ответ

Pod (Под) — это минимальная атомарная единица развёртывания и управления в Kubernetes. Kubernetes никогда не запускает отдельные Docker-контейнеры напрямую; он всегда оборачивает их в абстракцию Pod.

Pod представляет собой логический хост для одного или нескольких тесно связанных контейнеров, которые:

  • Разделяют один сетевой стек: у всех контейнеров пода ровно один общий IP-адрес и единый порт-пространство (они общаются друг с другом через localhost).
  • Разделяют дисковые тома (Volumes): файлы, созданные одним контейнером, могут быть моментально прочитаны другим через общую директорию.
  • Имеют общий жизненный цикл: все контейнеры пода планируются планировщиком (kube-scheduler) на один и тот же физический сервер (Node) и вместе создаются и уничтожаются.

Наглядная аналогия из жизни

Pod — это стручок гороха (англ. pea pod):

  • Горошины внутри стручка — это контейнеры.
  • Стручок может содержать одну большую горошину (классический Pod с одним Java-микросервисом — 95% случаев в индустрии).
  • Либо стручок содержит две горошины (основное Java-приложение + вспомогательный контейнер-помощник, например, сборщик логов).
  • Стручок неделим: вы не можете переложить одну горошину в один холодильник, а вторую — в другой. Pod целиком живёт на одной ноде.

Базовый манифест пода для Java 21

apiVersion: v1
kind: Pod
metadata:
  name: payment-service-pod
  labels:
    app: payment-service
spec:
  containers:
  - name: payment-app
    image: payment-service:2.0
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "512Mi"
        cpu: "500m"
      limits:
        memory: "1Gi"
        cpu: "1000m"

[!IMPORTANT] Поды эфемерны! В продакшене поды никогда не создаются вручную. Ими управляют высокоуровневые контроллеры: Deployment (для сервисов без состояния), StatefulSet (для БД) или DaemonSet.


🟡 Middle Level

Внутреннее устройство: Контейнер pause (Инфраструктурный фундамент)

Как Kubernetes заставляет несколько независимых контейнеров делить один IP-адрес и один сетевой стек?

  1. При создании каждого пода первым делом запускается скрытый системный контейнер — pause (образ registry.k8s.io/pause).
  2. Контейнер pause инициализирует пространства имён ядра Linux (Network, IPC, UTS).
  3. Затем запускаются пользовательские контейнеры (Java, Nginx, Fluentd), которые подключаются к уже созданным namespaces контейнера pause с помощью флага --net=container:pause.
  4. Даже если ваш Java-контейнер перезапустится из-за ошибки OOMKilled, сетевой адрес пода и открытые соединения не потеряются, так как контейнер pause продолжает непрерывно работать в памяти ядра.

Паттерны использования нескольких контейнеров в одном Pod

                    +------------------------------------+
                    |             K8S POD                |
                    |                                    |
                    |  [ Main App: Java 21 (Port 8080) ] |
                    |                  ^                 |
                    |                  | localhost       |
                    |                  v                 |
                    |  [ Sidecar: Envoy Proxy (Port 15001)]
                    |                  ^                 |
                    |                  | Shared Volume   |
                    |                  v (/var/log)      |
                    |  [ Sidecar: Fluent-Bit ]           |
                    +------------------------------------+
                                       │ IP: 10.244.1.45
                                       ▼

1. Sidecar (Прицеп / Помощник)

Расширяет функционал основного контейнера без изменения его кода:

  • Сбор логов из локального тома (emptyDir) и отправка в Elastic/Kafka.
  • Прокси-агент Service Mesh (Envoy в Istio) для шифрования mTLS.

2. Init Containers (Контейнеры инициализации)

Выполняются строго последовательно ДО старта основных контейнеров:

  • Проверка доступности БД (nc -z postgres 5432).
  • Скачивание сертификатов или применение миграций схемы Liquibase/Flyway.

3. Ambassador и Adapter

  • Ambassador: скрывает внешнюю топологию (приложение шлёт запросы в localhost:6379, а ambassador маршрутизирует их в сложный шардированный кластер Redis).
  • Adapter: нормализует вывод приложения к единому формату мониторинга Prometheus.

Фазы жизненного цикла Pod (Pod Phase)

  1. Pending: манифест принят API-сервером, но под ещё не запущен (идёт планирование ноды или скачивание образов ImagePullBackOff).
  2. Running: под привязан к ноде, все контейнеры созданы, и хотя бы один работает или находится в процессе запуска/перезапуска.
  3. Succeeded: все контейнеры пода успешно завершили работу с кодом 0 (характерно для Job).
  4. Failed: все контейнеры завершились, и хотя бы один из них упал с ненулевым кодом ошибки.
  5. Unknown: связь с нодой потеряна, статус пода не может быть получен от kubelet.

🔴 Senior Level

Пространства имён Linux: Изоляция vs Разделение в Pod

Senior-инженер должен точно понимать, какие пространства имён Linux разделяются, а какие изолированы:

Namespace Linux По умолчанию в Pod Пояснение
Network (NET) 🟢 Разделяется Единый IP, общие сетевые интерфейсы veth, обмен по localhost.
IPC 🟢 Разделяется Общая разделяемая память (POSIX shared memory /dev/shm), семафоры.
UTS 🟢 Разделяется Общее сетевое имя хоста (Hostname совпадает с metadata.name).
PID 🔴 Изолирован Каждый контейнер имеет собственное дерево процессов и свой PID 1.
Mount (MNT) 🔴 Изолирован У каждого контейнера свой rootfs, но тома (Volumes) монтируются совместно.

Флаг shareProcessNamespace: true

Если в спецификации пода установить shareProcessNamespace: true:

  • Дерево процессов становится единым для всех контейнеров пода.
  • Процесс контейнера pause получает PID 1 и берёт на себя функцию Zombie Reaping.
  • Основной Java-процесс и sidecar видят процессы друг друга через ps aux.
  • Сценарий применения: сайдкар-контейнер безопасности может послать системный сигнал kill -HUP или снять дамп потоков jcmd с процесса Java, работающего в соседнем контейнере.

Классы качества обслуживания (Quality of Service — QoS) и OOM-Score

Kubernetes автоматически присваивает каждому поду один из трёх классов QoS, который напрямую определяет значение флага oom_score_adj в ядре Linux:

[Гарантированная живучесть]                          [Первый кандидат на убой]
     Guaranteed                  Burstable                  BestEffort
(requests == limits)        (requests < limits)        (нет requests/limits)
oom_score_adj = -997        oom_score_adj = 2..999      oom_score_adj = 1000
  1. Guaranteed:
    • Для всех контейнеров пода заданы requests и limits, и они строго равны по CPU и RAM.
    • Значение oom_score_adj = -997. Ядро Linux убьёт этот под в последнюю очередь, только если физическая память сервера полностью исчерпана системными процессами.
  2. Burstable:
    • Заданы requests, но они меньше limits (или заданы только для одного ресурса).
    • Значение oom_score_adj рассчитывается динамически в диапазоне [2, 999] пропорционально превышению лимита.
  3. BestEffort:
    • Не задано ни одного requests и limits.
    • Значение oom_score_adj = 1000. При малейшей нехватке памяти на ноде Linux OOM Killer мгновенно уничтожает поды этого класса в первую очередь.

Native Sidecar Containers (Kubernetes 1.28+ / 1.29 GA)

Исторически сайдкары запускались как обычные параллельные контейнеры, что вызывало две огромные проблемы:

  1. Основной сервис стартовал раньше, чем сайдкар безопасности (Envoy/Vault), и падал из-за отсутствия сети.
  2. В пакетных задачах (Job) задача завершалась, но сайдкар сбора логов продолжал работать вечно, не давая Job завершиться со статусом Completed.

Современный стандарт (JEP / KEP-753): Сайдкары объявляются в секции initContainers со свойством restartPolicy: Always:

spec:
  initContainers:
  - name: vault-agent
    image: vault:1.15
    restartPolicy: Always # Kubernetes понимает, что это Native Sidecar!
  containers:
  - name: java-app
    image: payment-service:2.0

Kubelet дожидается запуска сайдкара и прохождения его startupProbe, запускает основное приложение, а при завершении основного приложения сам корректно гасит сайдкар!


4 Tricky Questions

1. Какую роль играет служебный контейнер pause внутри каждого Pod’а, и что произойдёт, если убить процесс pause на сервере вручную?

Ответ: Контейнер pause является «якорем» пространств имён Linux:

  1. Он удерживает открытыми дескрипторы общих namespaces (сетевого стека, IPC, UTS), позволяя пользовательским контейнерам пода перезапускаться без смены IP-адреса и разрыва сетевых таблиц хоста.
  2. Если процесс pause убить вручную из консоли хоста через kill -9:
    • Дескрипторы namespaces закрываются ядром Linux.
    • С точки зрения kubelet, инфраструктурный фундамент пода разрушен.
    • Kubelet фиксирует отказ пода, принудительно останавливает все остальные пользовательские контейнеры в этом поде и пересоздаёт весь Pod с нуля с выделением нового контейнера pause и нового IP-адреса.

2. Почему два контейнера в одном Pod не могут слушать один и тот же TCP-порт (например, 8080), даже если у них разные имена и разные образы?

Ответ: Поскольку все контейнеры внутри одного Pod разделяют одно общее сетевое пространство имён (Network Namespace), они работают на едином сетевом стеке с общим интерфейсом lo (localhost) и общим виртуальным интерфейсом eth0. Попытка второго контейнера открыть сокет на том же порту 8080 приведёт к ошибке ядра Linux java.net.BindException: Address already in use, ровно так же, как если бы вы попытались запустить два приложения на порту 8080 на одном физическом компьютере. Контейнеры одного пода обязаны использовать разные порты.


3. Как настроить Pod, чтобы Java-приложение гарантированно получило класс QoS Guaranteed, и почему для Java в Production это лучший выбор?

Ответ: Чтобы получить класс Guaranteed, необходимо:

  1. Указать секции requests и limits для всех контейнеров пода (включая сайдкары).
  2. Значения requests.cpu должны быть строго равны limits.cpu, а requests.memory строго равны limits.memory:
    resources:
      requests: { memory: "2Gi", cpu: "2000m" }
      limits:   { memory: "2Gi", cpu: "2000m" }
    

    Почему это критично для Java: При классе Guaranteed ядро выставляет oom_score_adj = -997. При нехватке памяти ноды K8s сначала убьёт поды BestEffort и Burstable. Кроме того, при совпадении CPU requests и limits Kubernetes выделит поду выделенные физические ядра CPU (CPU Pinning / Static CPU Manager Policy), исключая переключение контекста между ядрами и обеспечивая минимальный джиттер для JVM Garbage Collector.


4. В каком порядке завершаются контейнеры при удалении Pod (kubectl delete pod), и как выполняется Graceful Shutdown?

Ответ:

  1. Под переходит в статус Terminating, его IP немедленно удаляется из списков Endpoints сервисов (трафик перестаёт поступать).
  2. Если у контейнера задан хук preStop, он вызывается первым.
  3. Kubelet посылает сигнал SIGTERM процессу с PID 1 внутри каждого контейнера.
  4. Контейнеры завершают активные транзакции (Spring Boot graceful shutdown).
  5. Запускается таймер обратного отсчёта terminationGracePeriodSeconds (по умолчанию 30 секунд).
  6. Если по истечении таймера контейнеры не остановились, kubelet посылает беспощадный сигнал SIGKILL, принудительно очищая память.

🎯 Шпаргалка для интервью

Главное о Pod

  • Определение: минимальная атомарная единица планирования в Kubernetes.
  • Состав: один (обычно) или несколько контейнеров.
  • Общие ресурсы: единый IP-адрес, единый порт-пространство, общие тома (Volumes), IPC.
  • Контейнер pause: удерживает namespaces в ядре Linux.
  • Паттерны: Sidecar (расширение/логи), Init Containers (предпусковая подготовка).
  • QoS классы: Guaranteed (-997), Burstable (2..999), BestEffort (1000).
  • Эфемерность: поды не лечат сами себя; ими управляет Deployment.

Классификация QoS

| Класс QoS | Условие в YAML | oom_score_adj | Приоритет выселения (Eviction) | | :— | :— | :—: | :— | | Guaranteed | requests == limits (CPU & RAM) | -997 | 🟢 Последний (максимальная защита) | | Burstable | requests < limits | 2 .. 999 | 🟡 Средний | | BestEffort | Лимиты и реквесты не заданы | 1000 | 🔴 Первый кандидат на уничтожение |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «В продакшене мы создаём поды напрямую через манифест kind: Pod» — при падении ноды такой под не восстановится, нужен Deployment.
  • ❌ «Каждый контейнер внутри пода получает свой собственный IP-адрес» — весь под имеет ровно один общий IP.
  • ❌ «Контейнеры внутри одного пода могут слушать один и тот же порт» — произойдёт конфликт портов Address already in use.

Связанные вопросы