Pod как логический хост: общие Namespaces и разделяемые ресурсы
Pod как логический хост: общие Namespaces и разделяемые ресурсы
Парадокс: Kubernetes называют оркестратором контейнеров, но в нём невозможно запустить контейнер. Если вы попросите Kubernetes запустить образ Nginx, он обернёт его в абстракцию под названием Pod, и только потом отправит на Worker Node. Зачем нужна эта лишняя сущность? Почему нельзя управлять контейнерами напрямую, как это делает Docker?
Чтобы стать профессионалом в Kubernetes, нужно перестать мыслить категориями отдельных контейнеров и понять главную идею: Pod — это логический сервер.
Зачем Kubernetes придумал Pod?
В мире до контейнеров приложения работали на физических серверах или виртуальных машинах (VM). На одной VM часто крутилось несколько тесно связанных процессов. Например: главный веб-сервер (Nginx) и фоновый агент, который собирает его логи и отправляет их в центральное хранилище.
Этим двум процессам было легко: они могли общаться друг с другом через localhost и читать файлы из одной папки на жестком диске.
Когда мы переходим к чистым контейнерам, возникает проблема. Контейнер — это жестко изолированная среда. Если мы запустим Nginx в одном контейнере, а сборщик логов в другом, они окажутся в разных изолированных мирах. У них будут разные IP-адреса, и они не смогут просто так прочитать файлы друг друга.
Pod (в переводе «стая китов» или «стручок») был придуман для того, чтобы вернуть разработчикам удобство виртуальной машины, сохранив при этом легковесность контейнеров.
Pod объединяет один или несколько контейнеров в единую группу, которая делит общее окружение. Они всегда запускаются вместе, на одном и том же Worker Node, и никогда не размазываются по разным серверам кластера.
Анатомия иллюзии: как Pod манипулирует Namespaces
Мы помним, что контейнеризация в Linux строится на механизме Namespaces (пространств имен), который изолирует ресурсы. Когда kubelet через CRI запускает контейнеры для одного Pod'а, он настраивает их Namespaces особым образом: часть из них он делает общими, а часть оставляет изолированными.
1. Что объединяется (Shared)
- Network Namespace (Сеть): Все контейнеры в Pod'е получают один общий IP-адрес и один MAC-адрес. Они делят общую таблицу маршрутизации и порты. Это значит, что контейнеры внутри одного Pod'а могут общаться друг с другом по адресу
localhost. - IPC Namespace (Межпроцессное взаимодействие): Контейнеры могут использовать механизмы разделяемой памяти (System V IPC или POSIX) для сверхбыстрого обмена данными, как если бы они были процессами в одной ОС.
- UTS Namespace (Имя хоста): Все контейнеры внутри Pod'а видят одно и то же имя хоста (hostname), которое по умолчанию совпадает с именем самого Pod'а.
2. Что остается изолированным (Isolated)
- Mount Namespace (Файловая система): По умолчанию каждый контейнер видит только свою собственную файловую систему, запакованную в его Docker-образ. Контейнер A не видит файлы контейнера B.
- PID Namespace (Дерево процессов): Каждый контейнер видит только свои процессы. Процесс Nginx в первом контейнере будет иметь PID 1, и процесс сборщика логов во втором контейнере тоже будет иметь PID 1. Они не видят процессы друг друга (хотя Kubernetes позволяет отключить эту изоляцию через параметр
shareProcessNamespace: true, это используется редко, в основном для глубокого дебага).
Обратная сторона общей сети: конфликт портов
Поскольку Network Namespace общий, все порты делятся между контейнерами. Это работает точно так же, как на вашем ноутбуке: если вы запустите две программы, пытающиеся слушать порт 8080, вторая выдаст ошибку Address already in use.
Внутри Pod'а действует то же правило. Вы не можете запустить два контейнера с Nginx в одном Pod'е, если они оба настроены на прослушивание порта 80. Один из них запустится, а второй упадет с ошибкой привязки к порту.
Как обойти изоляцию Mount: общие Volumes
Если Mount Namespace изолирован, как нашему сборщику логов прочитать файлы веб-сервера? Для этого Kubernetes вводит абстракцию Volume (том) на уровне самого Pod'а.
Volume — это директория, которая создается для Pod'а и монтируется внутрь нужных контейнеров. Самый простой тип тома — emptyDir. Это временная пустая папка, которая создается при запуске Pod'а и удаляется при его уничтожении.
Давайте посмотрим, как это выглядит на практике.
Практика: YAML-манифест мультиконтейнерного Pod'а
Ниже представлен манифест Pod'а, реализующий паттерн Sidecar (коляска). Основной контейнер — веб-сервер, а sidecar-контейнер — утилита, которая что-то делает с его данными.
apiVersion: v1
kind: Pod
metadata:
name: web-with-logger
spec:
# Объявляем тома на уровне Pod'а
volumes:
- name: shared-logs
emptyDir: {}
containers:
# Основной контейнер
- name: nginx-web
image: nginx:alpine
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx # Nginx пишет логи сюда
# Sidecar-контейнер
- name: log-reader
image: busybox
# Простой скрипт, читающий лог каждую секунду
command: ["/bin/sh", "-c", "tail -f /var/log/app/access.log"]
volumeMounts:
- name: shared-logs
mountPath: /var/log/app # Тот же том, но смонтирован в другой путь!
Обратите внимание на важный нюанс: сам том shared-logs один, но внутри контейнеров он может быть примонтирован по разным путям. Nginx думает, что пишет в /var/log/nginx, а busybox читает из /var/log/app. Под капотом это одни и те же файлы на Worker Node.
Ошибки и диагностика (Troubleshooting)
Когда вы работаете с мультиконтейнерными Pod'ами, стандартные команды kubectl требуют уточнения.
Если вы напишете kubectl logs web-with-logger, Kubernetes выдаст ошибку: он не знает, логи какого именно контейнера вы хотите посмотреть. Вам нужно явно указать имя контейнера с помощью флага -c:
kubectl logs web-with-logger -c log-reader
Аналогично с выполнением команд внутри контейнера:
kubectl exec -it web-with-logger -c nginx-web -- /bin/sh
Типичный сценарий сбоя: CrashLoopBackOff из-за Sidecar'а
Статус Pod'а вычисляется на основе статусов всех его контейнеров. Если основной веб-сервер работает отлично, но sidecar-контейнер постоянно падает (например, из-за опечатки в команде запуска), весь Pod не перейдет в статус Running.
Вы увидите статус NotReady или CrashLoopBackOff. В этом случае нужно проверять события Pod'а (kubectl describe pod <name>) и искать, какой именно контейнер не смог стартовать, а затем смотреть его логи через kubectl logs <name> -c <failed-container>.
Мостик к следующей теме
Мы выяснили, что Pod объединяет контейнеры общим сетевым пространством (Network Namespace). Но здесь кроется техническая загадка.
Контейнеры внутри Pod'а могут падать и перезапускаться (kubelet делает это автоматически). Если контейнер Nginx упал, его процесс завершился, а значит, его Namespaces должны быть уничтожены ядром Linux. Как в таком случае Pod сохраняет свой IP-адрес неизменным, пока перезапускаются его рабочие контейнеры? Кто «держит» сеть, пока остальные контейнеры лежат?
Ответ кроется в невидимом компоненте, который kubelet запускает самым первым — pause-контейнере. Его роль и внутреннее устройство мы разберем в следующей главе.