Под глубже: init, sidecar, lifecycle hooks и QoS

Рейтинг: 67.6% · 8 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Под глубже: init, sidecar, lifecycle hooks и QoS

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS (вы здесь)
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Под - это не один контейнер

Когда новичок слышит "kubernetes pod", в голове рисуется один контейнер с приложением. Это упрощение, которое потом больно бьёт. Под (Pod) - это атомарная единица планирования в Kubernetes, группа из одного или нескольких контейнеров, которые всегда едут на одной ноде, разделяют сеть и могут делить тома. Их нельзя растащить по разным машинам, нельзя отскейлить по отдельности - под живёт и умирает целиком.

Боль, которую мы сегодня лечим, такая: твоё приложение запускается, но падает, потому что база ещё не подняла порт. Логи валятся в stdout, а никто их не собирает. При деплое часть запросов отваливается с 502, потому что под убили грубо. Под жрёт память сверх лимита, и kubelet выселяет именно твой критичный сервис, а не какую-то фоновую джобу. Всё это - вопросы устройства пода: порядок старта контейнеров, init и sidecar, хуки жизненного цикла, корректная остановка и QoS-классы. Разберёмся, как это работает под капотом, а не на уровне "скопировал манифест из интернета".

Изображение

Анатомия пода и pause-контейнер

Внутри одного kubernetes под все контейнеры делят несколько Linux namespaces: сетевой (один IP на весь под, общий localhost), IPC, и опционально PID. При этом файловые системы у контейнеров раздельные - root-каталог у каждого свой, общими становятся только смонтированные тома.

Кто держит эти namespaces, пока контейнеры перезапускаются? Скрытый pause-контейнер (в CRI его называют sandbox). Это крошечный процесс, который не делает ничего - просто спит и держит сетевой и IPC namespace открытыми. Когда твой основной контейнер крашится и рестартует, IP пода не меняется именно потому, что pause его держит. Увидеть его можно прямо на ноде:

Код: Выделить всё

crictl pods
POD ID         NAME              STATE   ATTEMPT
3f9a1c2b...    web-7d4f-abcde    Ready   0

crictl ps --pod 3f9a1c2b...
CONTAINER   IMAGE          STATE     NAME
a1b2c3...   nginx:1.27     Running   web
Сам pause в обычном выводе kubectl не виден - это инфраструктурная деталь рантайма (containerd/CRI-O). С переходом на CRI и отказом от dockershim управление sandbox полностью ушло на сторону рантайма, и в 2026 это норма, а не экзотика.

Init-контейнеры: подготовка строго по порядку

Init container - это контейнер, который выполняется до основных и обязан завершиться успешно (exit code 0). Их может быть несколько, и они идут строго последовательно: второй не стартует, пока первый не отработал. Если init упал - kubelet перезапускает его согласно restartPolicy пода, и под висит в фазе Pending, не пуская основное приложение. Это ровно то поведение, которое нужно для "ждать зависимость" и "подготовить данные".

Классический сценарий - дождаться, пока поднимется база, и накатить миграции:

Код: Выделить всё

apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
spec:
  initContainers:
  - name: wait-for-db
    image: busybox:1.36
    command: ['sh', '-c', 'until nc -z postgres 5432; do echo waiting db; sleep 2; done']
  - name: run-migrations
    image: myapp/migrator:1.4
    command: ['./migrate', 'up']
  containers:
  - name: app
    image: myapp/api:2.1
    ports:
    - containerPort: 8080
Сначала отработает wait-for-db (крутится в цикле, пока порт 5432 не ответит), затем run-migrations накатит схему, и только потом стартанёт app. Важная деталь про ресурсы: эффективный запрос пода считается как максимум из суммы основных контейнеров и максимального init-контейнера, потому что init-ы не работают одновременно с основными. Это влияет на то, поместится ли под на ноду.

Sidecar: нативные restartable init с 1.29

Sidecar kubernetes - это контейнер-спутник: сборщик логов, прокси сервисной сетки (Envoy в Istio), агент метрик. Раньше его просто добавляли вторым в containers, и это порождало две классические проблемы. Первая - порядок старта не гарантирован: приложение могло начать слать трафик до того, как Envoy готов. Вторая - на Job-подах sidecar не завершался сам, и джоба висела вечно в Running, потому что прокси не понимает, что основная работа окончена.

С 1.28 (alpha), 1.29 (beta, включено по умолчанию) и стабильно с 1.33 появились нативные sidecar - это init-контейнер с restartPolicy: Always. Магия в том, что такой контейнер:
  • стартует в порядке initContainers, до основных, но не блокирует их - kubelet идёт дальше, как только sidecar запущен (и прошёл startup-проба, если задана);
  • продолжает работать всё время жизни пода, как обычный контейнер;
  • рестартует независимо, не валя при этом основное приложение;
  • на Job-подах завершается после основных контейнеров - джоба корректно переходит в Succeeded.

Код: Выделить всё

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels: {app: web}
  template:
    metadata:
      labels: {app: web}
    spec:
      initContainers:
      - name: log-shipper
        image: fluent/fluent-bit:3.1
        restartPolicy: Always
        volumeMounts:
        - name: varlog
          mountPath: /var/log/app
      containers:
      - name: app
        image: myapp/api:2.1
        volumeMounts:
        - name: varlog
          mountPath: /var/log/app
      volumes:
      - name: varlog
        emptyDir: {}
Обрати внимание: log-shipper лежит в initContainers, но с restartPolicy: Always - это и есть признак нативного sidecar. Старый способ (второй контейнер в containers) всё ещё работает, но для меша и логов в 2026 правильно использовать нативный механизм.

Общие тома и namespace между контейнерами

В примере выше app пишет логи в /var/log/app, а log-shipper их оттуда читает - оба монтируют один emptyDir-том. emptyDir создаётся при старте пода и живёт ровно столько, сколько под; при удалении пода данные исчезают. Это идеальный канал обмена между контейнерами одного пода.

Сеть тоже общая: внутри пода контейнеры обращаются друг к другу через localhost. Если app слушает на 127.0.0.1:8080, sidecar-прокси достучится туда без всякого Service. А вот PID namespace по умолчанию не общий - чтобы один контейнер видел процессы другого, нужно явно выставить shareProcessNamespace: true в spec. Это бывает нужно для отладочных sidecar, которые шлют сигналы основному процессу.

Lifecycle hooks: postStart и preStop

Хуки жизненного цикла - это команды, которые kubelet выполняет в ключевые моменты. Их два.

postStart запускается сразу после создания контейнера, параллельно с ENTRYPOINT - это не "после запуска приложения", а одновременно с ним. Гарантии порядка между ними нет. Если postStart висит долго, контейнер не перейдёт в Running, а если хук вернёт ошибку - kubelet убьёт и перезапустит контейнер. Поэтому в postStart не суют тяжёлую инициализацию (для этого есть init-контейнеры).

preStop - наш главный инструмент graceful shutdown. Он выполняется синхронно перед отправкой SIGTERM: kubelet ждёт завершения preStop, и только потом шлёт сигнал. Классический трюк - дать балансировщику и kube-proxy время убрать под из EndpointSlices, прежде чем приложение начнёт отключаться:

Код: Выделить всё

spec:
  containers:
  - name: app
    image: myapp/api:2.1
    lifecycle:
      preStop:
        exec:
          command: ['sh', '-c', 'sleep 10']
    terminationGracePeriodSeconds: 45
Эти 10 секунд sleep закрывают гонку: удаление пода и обновление эндпоинтов происходят асинхронно, и без паузы приложение успеет закрыться раньше, чем трафик перестанет на него идти - отсюда те самые 502 при деплое.

Pod lifecycle: остановка по шагам и terminationGracePeriod

Когда ты делаешь kubectl delete pod, разворачивается строгая последовательность (это и есть pod lifecycle на этапе остановки):
  • API-сервер помечает под на удаление, ставит deletionTimestamp, под уходит в фазу Terminating;
  • параллельно эндпоинты пода выпиливаются из EndpointSlices - новый трафик перестаёт направляться;
  • kubelet запускает preStop-хук (если есть) и ждёт его;
  • kubelet шлёт SIGTERM основному процессу контейнера;
  • идёт отсчёт terminationGracePeriodSeconds (по умолчанию 30 секунд);
  • если процесс не завершился за грейс-период - прилетает SIGKILL, и контейнер убивается жёстко.
Критично понимать: грейс-период включает время preStop. Если preStop спит 10 секунд, а grace period 30, то на сам SIGTERM-shutdown остаётся 20. Если preStop дольше грейс-периода - под прибьют посреди хука. Для нативных sidecar порядок обратный: они получают SIGTERM после основных контейнеров, чтобы прокси и логгер дожили до конца работы приложения.

Фазы пода и состояния контейнеров

Не путай фазу пода и состояние контейнера - это разные уровни. Фаза пода (поле status.phase) - грубое резюме:
  • Pending - под принят, но контейнеры ещё не запущены: тянутся образы, крутятся init-контейнеры, либо нет места на нодах;
  • Running - под привязан к ноде, все контейнеры созданы, хотя бы один работает;
  • Succeeded - все контейнеры завершились с кодом 0 и не перезапускаются (типично для Job);
  • Failed - все контейнеры завершились, хотя бы один с ненулевым кодом;
  • Unknown - kubelet не отвечает, состояние пода неизвестно.
А вот состояния контейнеров детальнее, и именно по ним диагностируют проблемы: Waiting (ждёт, тут живут причины ImagePullBackOff, CrashLoopBackOff), Running, Terminated (с exit code и причиной). Смотрим в kubectl describe:

Код: Выделить всё

kubectl describe pod web-7d4f-abcde
...
Containers:
  app:
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Mon, 15 Jun 2026 10:02:11
      Finished:     Mon, 15 Jun 2026 10:02:13
    Restart Count:  6
Здесь видно: контейнер падает с кодом 1 через две секунды после старта, kubelet уже 6 раз пробовал, и теперь ждёт с нарастающей задержкой (CrashLoopBackOff - это не ошибка, а защитная пауза рантайма перед очередной попыткой, до 5 минут максимум).

QoS-классы и выселение

Когда на ноде кончается память, kubelet начинает выселять (evict) поды. Кого первым - решает QoS-класс, который Kubernetes вычисляет автоматически из requests и limits. Класса три:
  • Guaranteed - у каждого контейнера requests равны limits по cpu и памяти. Самый защищённый класс, выселяется в последнюю очередь;
  • Burstable - заданы requests, но они меньше limits (или limits не заданы вовсе). Большинство реальных подов тут;
  • BestEffort - не заданы ни requests, ни limits. Первый кандидат на вылет под давлением памяти.

Код: Выделить всё

spec:
  containers:
  - name: app
    image: myapp/api:2.1
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"
Этот под получит класс Guaranteed - requests равны limits. Проверить присвоенный класс:

Код: Выделить всё

kubectl get pod app -o jsonpath='{.status.qosClass}'
Guaranteed
Порядок выселения при нехватке памяти: сначала BestEffort, потом Burstable, превысившие свои requests, и только в крайнем случае Guaranteed. Отдельно стоит OOMKill: если контейнер превысил memory limit, его прибивает ядро (cgroups) независимо от QoS - это локальное событие на уровне контейнера, не путать с eviction всего пода под давлением ноды.

Static pods: поды без API-сервера

Есть особый вид подов, которыми управляет сам kubelet, а не планировщик. Static pod описывается YAML-файлом, лежащим в каталоге на ноде (обычно /etc/kubernetes/manifests), и kubelet поднимает его напрямую, читая файл. Так живут компоненты управляющего слоя в кластерах, развёрнутых kubeadm: kube-apiserver, etcd, controller-manager. Положил манифест в каталог - под поднялся, убрал файл - под удалился. На API-сервере при этом появляется зеркальный (mirror) под только для наблюдения, но управлять им через kubectl нельзя - удалишь, kubelet тут же воссоздаст из файла. В managed-кластерах (Yandex Managed Service for Kubernetes, VK Cloud) управляющий слой скрыт от тебя, а вот в k3s или Deckhouse на своих нодах static pods встречаются регулярно.

Типичные грабли и антипаттерны
  • Тяжёлая инициализация в postStart вместо init-контейнера - хук не дружит с долгими операциями и валит контейнер по таймауту.
  • Sidecar вторым контейнером в containers на Job - джоба зависает в Running навечно. В 2026 - нативный sidecar (restartPolicy: Always в initContainers).
  • preStop дольше terminationGracePeriodSeconds - под прибивают посреди хука, graceful не случается.
  • Приложение игнорирует SIGTERM (частый случай с shell-обёртками типа sh -c) - сигнал не доходит до реального процесса, всегда прилетает SIGKILL. Используй exec-форму ENTRYPOINT или tini.
  • Все поды BestEffort, потому что лень прописать requests - под нагрузкой выносит именно их, причём в случайном порядке.
  • Логи только в файл внутри контейнера без общего тома - sidecar-сборщик их не видит. Пиши в stdout или в shared emptyDir.
Мини-лаба: собери всё руками
  • Создай под с двумя init-контейнерами (первый ждёт сервис через nc -z, второй пишет файл в emptyDir) и основным контейнером, читающим этот файл. Запусти kubectl get pod -w и понаблюдай переход Pending -> Running.
  • Добавь нативный sidecar (busybox с restartPolicy: Always в initContainers), который раз в секунду пишет в общий том. Проверь kubectl logs pod -c sidecar.
  • Повесь preStop с sleep 10 и terminationGracePeriodSeconds: 30. Сделай kubectl delete pod и засеки время до исчезновения - должно быть около 10+ секунд, не мгновенно.
  • Сделай три копии пода: с requests==limits, с requests<limits, и без ресурсов вовсе. Проверь kubectl get pod -o jsonpath на qosClass и убедись, что классы Guaranteed/Burstable/BestEffort.
Контрольные вопросы
  • Чем нативный sidecar (init с restartPolicy: Always) отличается от обычного второго контейнера в containers, и почему он критичен для Job?
  • В каком порядке выполняются preStop, SIGTERM и SIGKILL при удалении пода, и как с этим связан terminationGracePeriodSeconds?
  • Как Kubernetes вычисляет QoS-класс пода и в каком порядке выселяет поды при нехватке памяти на ноде?
  • Зачем нужен pause-контейнер и почему IP пода не меняется при рестарте основного контейнера?
Итог

Под - это не контейнер, а оркестр: pause держит сеть, init-контейнеры готовят почву строго по очереди, нативные sidecar сопровождают приложение всю его жизнь, lifecycle hooks и terminationGracePeriod дают корректный старт и graceful shutdown, а QoS-классы решают, кто выживет под давлением. Понимаешь эту механику - и pod lifecycle перестаёт быть чёрным ящиком: ImagePullBackOff, зависшие джобы, 502 при деплое и внезапные выселения превращаются из мистики в понятные следствия конкретных решений в манифесте.
👍4 ❤️2 🔥 😄 🤔1
Аватара пользователя
tmmo0812
Сообщения: 1
Зарегистрирован: 21 май 2026, 08:35

Re: Под глубже: init, sidecar, lifecycle hooks и QoS

Сообщение tmmo0812 »

Долго не мог понять почему джоба с istio-прокси висела в Running вечно - оказывается надо было перевести sidecar в нативный init с restartPolicy Always. Спасибо, прям про мою боль
👍1 ❤️1 🔥 😄 🤔
Аватара пользователя
vasiliyro
Сообщения: 1
Зарегистрирован: 15 май 2026, 00:11

Re: Под глубже: init, sidecar, lifecycle hooks и QoS

Сообщение vasiliyro »

Уточните пожалуйста: terminationGracePeriodSeconds считается включая время preStop или отдельно от него? У меня preStop на 20 сек и grace 30, я думал у меня есть ещё 30 на сам shutdown
👍 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
kubectl профессионально: get, describe, explain, jsonpath
Следующая глава →
Метки, селекторы и организация ресурсов

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость