🐳 Розділ 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 (для stateless-сервісів), 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.

Пов’язані питання