Под в Kubernetes состоит из двух уровней: инфраструктурного pause-контейнера, который создаёт и удерживает общие пространства имён, и одного или нескольких прикладных контейнеров, которые к этим пространствам присоединяются. У всех контейнеров пода один сетевой интерфейс и один IP-адрес, они обращаются друг к другу через localhost, делят IPC-шину и монтируют одни и те же тома.
Отсюда следствие, которое снимает большую часть путаницы: в kubectl get pods вы видите поды, а не контейнеры. Внутри пода контейнеров почти всегда больше, чем один, и как минимум один из них - pause-контейнер, который вы не описывали в манифесте.
Ниже разбираем по частям: роль pause-контейнера, поведение прикладных и init-контейнеров, общие namespace и тома, критерии для sidecar и команды проверки на своём кластере.
Что такое под в Kubernetes и почему это не просто контейнер
Под - минимальная единица развёртывания и планирования в Kubernetes. kube-scheduler выбирает для пода одну ноду, и все контейнеры пода гарантированно оказываются на ней: разнести их по разным машинам нельзя. Внутри ноды kubelet через CRI просит container runtime создать sandbox, а затем контейнеры. Полный путь от kubectl apply до запуска процесса разобран в статье про архитектуру Kubernetes и путь пода от манифеста до запуска.
Контейнер с точки зрения ядра Linux - это процесс в изолированных пространствах имён, ограниченный cgroup. Docker и containerd запускают такие процессы из образов, а Kubernetes добавляет ещё один уровень абстракции: под как обёртку над группой процессов с частично общими пространствами имён. Один под может содержать один контейнер (типовой случай для stateless-сервиса) или несколько: прикладные, init и sidecar.
Под vs контейнер: в чём разница на практике
Разница проявляется в трёх местах: сеть, адресация и команды. У двух контейнеров одного пода один и тот же IP и одна таблица маршрутизации, поэтому связь между ними идёт через localhost, а Service для этого не нужен. У контейнеров разных подов адреса разные, и для связи понадобится Service, прямой podIP или DNS-имя.
Второй момент: kubectl get pods не показывает контейнеры. Чтобы получить их список, запросите spec пода:
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.containers[*].name}'
Вывод вида app nginx-exporter означает два прикладных контейнера в одном поде. Флаг -o wide добавляет podIP и ноду, а -o yaml показывает полный манифест с томами, портами и ресурсами.
Pause-контейнер: инфраструктурный контейнер пода
pause-контейнер создаётся первым, ещё до прикладных. Прикладной логики в нём нет: его процесс ничего не обрабатывает и не слушает порты. Задача другая - создать общие пространства имён и удерживать их, пока в поде работает хотя бы один контейнер. pause-контейнер существует в каждом поде и служит контейнером-шаблоном, от которого все новые контейнеры в поде наследуют namespaces; он резервирует namespaces, общие для всех контейнеров внутри пода (разбор роли pause-контейнера).
Образ у pause-контейнера минималистичный: в поставке Kubernetes это registry.k8s.io/pause, и в документации containerd рекомендуется использовать registry.k8s.io/pause:3.10 как CRI sandbox-образ. Конкретные имена образов и версии sandbox зависят от версии kubelet и container runtime, поэтому проверяйте их на своей ноде командами из раздела про crictl, а не по памяти.
Как pause-контейнер удерживает network namespace
container runtime создаёт sandbox: новый сетевой namespace, виртуальный интерфейс, IP-адрес из podCIDR ноды, таблицы маршрутизации. В этом namespace живёт pause-контейнер. При создании пода процесс pause вызывает интерфейс CRI, чтобы создать окружение «песочницу» и настроить базовую среду выполнения, например сеть; как только процесс запущен, в его namespace можно добавлять другие процессы, формируя под с общими namespace (разбор роли pause-контейнера). Прикладные контейнеры стартуют с указанием присоединиться к готовому namespace, поэтому видят тот же интерфейс, тот же IP и тот же loopback.
Пример: в поде nginx слушает 80, а sidecar-прокси слушает 15001. Прокси обращается к бэкенду по 127.0.0.1:80, nginx отвечает. Ни Service, ни DNS в этой схеме не участвуют: трафик не покидает под.
Проверка - посмотреть интерфейсы изнутри прикладных контейнеров:
kubectl exec my-app-7d9c8f4b6f-2xk9p -c app -- ip addr kubectl exec my-app-7d9c8f4b6f-2xk9p -c nginx-exporter -- ip addr
Список интерфейсов и IPv4-адрес совпадут. Если утилиты ip в образе нет, добавьте временный контейнер в под:
kubectl debug -it my-app-7d9c8f4b6f-2xk9p --image=nicolaka/netshoot --target=app
Контейнеры внутри одного пода и так используют общий network namespace, а флаг --target нужен, чтобы видеть ещё и процессы конкретного контейнера. Как устроены маршруты внутри пода и на ноде, разбираем отдельно в материале про таблицы маршрутов между подами и нодами.
Почему pause-контейнер не виден в kubectl get pods
pause-контейнер не описан в spec пода: его создаёт kubelet, а не вы в манифесте. Поэтому ни kubectl get pods, ни kubectl describe pod его не покажут, и в status.containerStatuses его тоже не будет. Чтобы найти pause-контейнер, зайдите на ноду по SSH и выполните:
crictl ps --name pause crictl pods
Первая команда покажет все sandbox-контейнеры на ноде, вторая - поды, которым они соответствуют. Диагностическая ценность высокая: если прикладные контейнеры не получают IP или под висит в ContainerCreating, причина часто в sandbox, а не в приложении.
Прикладные контейнеры в поде: сколько их может быть и зачем
Прикладной контейнер выполняет работу: обслуживает HTTP, забирает сообщения из очереди, проксирует трафик, отдаёт метрики. Количество таких контейнеров в поде не ограничено единицей, но ограничено ресурсами ноды и логикой планирования.
Что общего и что разного: сеть, IPC и UTS у контейнеров общие, файловые системы раздельные. Каждый контейнер стартует из своего образа, и файл, записанный в /tmp одного контейнера, другому не виден. Обмен файлами идёт только через тома, смонтированные в оба контейнера.
Классический пример связки без sidecar: nginx и php-fpm в одном поде. php-fpm слушает 127.0.0.1:9000, nginx проксирует к нему по тому же адресу, статика монтируется в оба контейнера общим томом.
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.containers[*].name}'
Init-контейнеры: подготовка перед запуском прикладных
Init-контейнеры запускаются до прикладных, выполняются строго последовательно и должны завершиться с кодом 0. Пока последний init не завершится, прикладные контейнеры не стартуют, а под остаётся в фазе Pending с причиной вида Init:0/2. Типовые задачи: прогнать миграции, выкачать конфиг из внешнего источника, выставить права на том.
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: fetch-config
image: alpine/git:2.45.2
command: ["git", "clone", "https://git.example.com/app/config.git", "/config"]
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: example/app:1.4.2
volumeMounts:
- name: config
mountPath: /etc/app
volumes:
- name: config
emptyDir: {}
Посмотреть список init-контейнеров можно отдельным запросом:
kubectl get pod app-with-init -o jsonpath='{.spec.initContainers[*].name}'
Init-контейнер, созданный с restartPolicy: Always, запускается и остаётся работающим в течение всей жизни пода: Kubernetes реализует sidecar-контейнеры как особый случай init-контейнеров, и такие контейнеры продолжают работать после старта пода (Sidecar Containers). Эта функциональность впервые стала доступна в релизе Kubernetes v1.28 и является стабильной начиная с версии v1.33.
Общие ресурсы контейнеров в поде: network namespace, IPC и тома
Набор общих ресурсов у контейнеров пода невелик, но именно он определяет всю механику взаимодействия.
| Ресурс | Статус в поде | Что это даёт |
|---|---|---|
| network namespace | Общий, создаёт pause-контейнер | Один IP, один loopback, связь через localhost |
| IPC namespace | Общий по умолчанию | Shared memory, семафоры, очереди сообщений между контейнерами |
| UTS namespace | Общий | Все контейнеры видят одно hostname пода |
| PID namespace | Раздельный по умолчанию | Процессы соседнего контейнера не видны; включается через shareProcessNamespace: true |
| Тома | Монтируются в указанные контейнеры | Основной штатный способ обмена файлами внутри пода |
Network namespace: один IP на весь под
Адрес пода берётся из podCIDR ноды и живёт до удаления пода. Своего IP-стека на каждый контейнер нет, поэтому два контейнера не могут слушать один и тот же порт на одном IP. Связка nginx:80 и экспортера, который тоже хочет 80, не запустится: конфликт портов. Разносите сервисы по разным портам или по разным подам.
Проверить адрес пода и общее hostname:
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.status.podIP}'
kubectl exec my-app-7d9c8f4b6f-2xk9p -c app -- hostname
hostname у всех контейнеров пода одинаковый - это следствие общего UTS namespace. Для доступа к поду снаружи используют Service: он получает ClusterIP, по которому Kubernetes распределяет трафик между подами. Каждый ClusterIP уникален в кластере, а попытка занять уже выделенный адрес завершается ошибкой; динамическое выделение идёт из верхней части диапазона, и только когда она исчерпана - из нижней (Allocation des ClusterIP des Services). Место пода среди остальных объектов API разобрано в обзоре ключевых объектов Kubernetes.
IPC namespace: общая шина для взаимодействия
Контейнеры одного пода видят одни и те же объекты межпроцессного взаимодействия: POSIX shared memory, семафоры, очереди сообщений. Это сценарий для приложений, которые передают данные через /dev/shm без сети и без файлов.
Уточнение, где часто путаются: shareProcessNamespace управляет PID namespace, к IPC он отношения не имеет. Общий IPC внутри пода - поведение по умолчанию, отдельного флага для его отключения в PodSpec нет. Вариант с hostIPC выносит контейнеры в IPC-пространство ноды и снижает изоляцию.
kubectl exec my-app-7d9c8f4b6f-2xk9p -c app -- ipcs -m
Утилита ipcs есть не во всех образах, поэтому для проверки берите образ с пакетом util-linux или временный контейнер через kubectl debug.
Общие тома: emptyDir и другие способы обмена файлами
Том описывается один раз на уровне пода, а монтируется в те контейнеры, где для него указан volumeMounts. Жизненный цикл тома связан с подом, а не с контейнером: временные (ephemeral) тома имеют жизненный цикл, привязанный к конкретному поду, и когда под перестаёт существовать, Kubernetes уничтожает ephemeral-тома, но не уничтожает persistent-тома (Volumes). Поэтому в emptyDir удобно держать данные между перезапусками контейнера.
Типы томов различаются источником данных и сроком жизни:
- emptyDir - каталог на диске ноды (или в памяти при medium: Memory), удаляется вместе с подом;
- configMap и secret - файлы из объектов API, при стандартных настройках монтируются только для чтения;
- hostPath - путь с ноды, годится для системных задач и требует аккуратности с точки зрения безопасности;
- persistentVolumeClaim - том, который переживает под; подключается через StorageClass и CSI-драйвер.
Для emptyDir можно указать medium: Memory - тогда Kubernetes монтирует tmpfs (RAM-backed); если размер не задан, тома в памяти ограничиваются объёмом allocatable-памяти узла (Volumes).
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.volumes[*].name}'
Sidecar-паттерн: когда несколько контейнеров в поде оправданы
Sidecar - вспомогательный контейнер, который живёт рядом с основным и решает задачу из смежной области: доставка логов, проксирование с mTLS, съём метрик, синхронизация конфигурации. Родственные роли называют ambassador (прокси к внешнему сервису) и adapter (приведение формата метрик или логов к нужному виду).
Критерий, по которому видно, что sidecar уместен: задача требует общего network namespace или общего тома и должна решаться вместе с основным контейнером. Если оба условия не выполняются, под - не то место.
Пример sidecar для логирования
Основной контейнер пишет логи в файл, sidecar читает тот же каталог и отправляет их в централизованное хранилище. Приложение при этом ничего не знает о логировании.
apiVersion: v1
kind: Pod
metadata:
name: web-with-logger
spec:
containers:
- name: web
image: nginx:1.27
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: logger
image: fluent/fluent-bit:3.1
volumeMounts:
- name: logs
mountPath: /var/log/nginx
volumes:
- name: logs
emptyDir: {}
Логи sidecar вытаскиваются отдельно от логов приложения:
kubectl logs web-with-logger -c logger --tail=50 kubectl logs web-with-logger -c web --tail=50
Такой же каркас используется для sidecar-прокси на Envoy с mTLS, для nginx-prometheus-exporter и для git-sync, который обновляет конфиг в общем томе. В каждом случае общий ресурс - network namespace или том.
Когда sidecar лишнее усложнение
Есть сценарии, где второй контейнер в поде создаёт больше проблем, чем решает:
- задача закрывается на уровне ноды: сбор системных метрик, ротация логов, сетевые политики через DaemonSet;
- компонент нужно масштабировать и обновлять независимо от приложения;
- контейнеры не взаимодействуют и не делят ресурсы: их логичнее разнести по разным подам и связать через Service;
- sidecar удваивает потребление памяти на каждый экземпляр пода, а выгода не сформулирована;
- один из контейнеров просто ждёт или спит, пока работает второй, - это антипаттерн.
Проверить, не раздулся ли под, помогает тот же jsonpath по контейнерам:
kubectl get pod web-with-logger -o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}'
Как проверить состав пода и его контейнеров: команды для практики
Набор команд ниже закрывает большинство вопросов: какие контейнеры в поде, какой у них статус, какие тома и общий ли у них сетевой стек. Примеры даны для пространства имён по умолчанию; добавьте -n , если под в другом.
Просмотр контейнеров через kubectl
# прикладные и init-контейнеры
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.containers[*].name}'
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.initContainers[*].name}'
# фактически запущенные контейнеры
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.status.containerStatuses[*].name}'
# все поды с их контейнерами
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].name}{"\n"}{end}'
# тома и IP пода
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.spec.volumes[*].name}'
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{.status.podIP}'
Разница между spec и status принципиальна: spec показывает, что вы попросили, status - что реально запущено. Если в spec два контейнера, а в status один, второй не стартовал: смотрите kubectl describe pod и блок Events.
kubectl describe pod my-app-7d9c8f4b6f-2xk9p
В блоке Containers видны образы, порты, запросы и лимиты, точки монтирования; в Events - причины проблем с планированием, образами и томами.
Просмотр pause-контейнера через crictl
pause-контейнер доступен только со стороны ноды, через CRI. Если у вас есть SSH-доступ к worker-узлу:
sudo crictl ps --name pause sudo crictl pods sudo crictl ps -a | head
Флаг -a показывает и остановленные контейнеры, что полезно, когда sandbox перезапускается. Убедиться, что найденный контейнер действительно инфраструктурный, можно по образу:
sudo crictl inspect | grep -i image
Связка sandbox и пода видна через crictl pods: у каждого пода свой sandbox, а имена совпадают с именами подов в kubectl. Для доступа к узлу без SSH используйте kubectl debug node/ с образом, где есть nsenter и доступ к сокету CRI. Если ноды разворачивались вручную, пригодится материал про установку кластера на kubeadm и containerd.
Частые ошибки и вопросы при работе с подами
Короткий список того, что ломается чаще всего:
- два контейнера слушают один порт на общем IP - второй не стартует;
- ожидание сетевой изоляции между контейнерами пода;
- запись файла в файловую систему контейнера и попытка прочитать его из соседнего контейнера без общего тома;
- sidecar, который не обменивается данными с основным контейнером и не делит с ним ресурсы;
- расчёт на то, что init-контейнер работает в фоне, тогда как он обязан завершиться до старта прикладных.
Отдельно про безопасность: PodSecurityPolicy устарел как механизм управления безопасностью подов, и в документации приведено сопоставление старых политик с Pod Security Standards (PodSecurityPolicy в Pod Security Standards). Планируйте ограничения подов через актуальные стандарты, а не через PSP.
Почему контейнеры в поде не изолированы по сети
Это проектное решение, а не недоработка. Общий network namespace делает связь между контейнерами пода локальной: один процесс слушает 127.0.0.1:8080, второй обращается туда напрямую, без Service, DNS и лишнего сетевого хопа. Плата за удобство - отсутствие сетевой изоляции между контейнерами: если она нужна, разносите их по разным подам и ограничивайте трафик NetworkPolicy.
Как ограничить ресурсы для контейнеров в поде
requests и limits задаются для каждого контейнера отдельно, а scheduler суммирует requests всех контейнеров пода, включая init. Если сумма не помещается ни на одной ноде, под останется в Pending. Kubernetes предоставляет штатные механизмы описания, запроса, выделения и ограничения ресурсов, которые использует рабочая нагрузка (Resource management).
containers:
- name: app
image: example/app:1.4.2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
- name: logger
image: fluent/fluent-bit:3.1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
Посмотреть фактические запросы и лимиты по контейнерам:
kubectl get pod my-app-7d9c8f4b6f-2xk9p -o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}'
Если контейнер регулярно упирается в memory limit, его сбрасывает OOMKiller, а под получает статус OOMKilled. Как измерить реальное потребление и настроить cgroups без запаса «на глаз», разобрано в статье про производительность и эффективность контейнеров.
Перед деплоем прогоняйте короткий чек-лист: список контейнеров в spec совпадает с ожидаемым, у каждого контейнера заданы requests и limits, общие тома смонтированы во все контейнеры, которым они нужны, ни один контейнер не слушает занятый порт, а вспомогательные контейнеры действительно делят с основным сеть или том. Пяти проверок достаточно, чтобы не ловить ContainerCreating, конфликты портов и OOMKilled после выката.