3. Pod, Deployment, Service
Святая троица Кубернетеса: Как код превращается в сервис
В предыдущей статье 2. Kubernetes. Основы мы познакомились с генеральным штабом кластера: мозгом (Control Plane), отдающим приказы, и исполнительными мышцами (Worker Nodes), претворяющими их в жизнь на уровне ядра Linux. Мы выяснили, что Kubernetes управляется по декларативной модели — мы описываем желаемое состояние системы, а оркестратор крутит циклы примирения, непрерывно подгоняя реальность под наше описание.
Но какими именно строительными кирпичиками мы описываем это желаемое будущее? Как объяснить оркестратору, что нам требуется запустить наш скомпилированный бинарный файл на Go, обеспечить ему отказоустойчивость при физической гибели серверов и предоставить другим сервисам стабильную сетевую точку входа для отправки HTTP и gRPC запросов?
В архитектуре Kubernetes за это отвечает фундаментальная триада абстракций: Pod, Deployment и Service. В этой лекции мы заглянем под капот каждого из этих компонентов с точки зрения системного инженера, разберём низкоуровневую магию виртуальных IP-адресов в ядре Linux и выясним, почему «голые» поды в продакшене — это признак архитектурной незрелости.
1. Pod: Атом кластера (и ложь о контейнерах)
Первая фундаментальная истина, которую обязан осознать разработчик: Kubernetes принципиально не оперирует контейнерами напрямую. Он не управляет отдельными сущностями Docker (см. 1. Контейнеризация и Docker).
Минимальной неделимой единицей развёртывания, квантом вычислительной мощности в кластере выступает Pod (Под).
Метафора стручка гороха (pod) предельно точна: под — это изолированная капсула, внутри которой может мирно сосуществовать один или несколько тесно связанных процессов-контейнеров. Все контейнеры, входящие в состав одного пода, гарантированно запускаются на одном и том же физическом сервере и делят общие пространства имён ядра:
- Сетевое пространство (Network Namespace): Все контейнеры пода обладают единым IP-адресом и общим диапазоном портов. Они общаются между собой на космической скорости через локальную петлю
localhost. - Межпроцессное взаимодействие (IPC Namespace): Контейнеры могут использовать общие семафоры и разделяемую память (Shared Memory POSIX).
- Дисковые тома (Volumes): Контейнеры могут монтировать общие каталоги файловой системы для мгновенного обмена файлами.
Именно благодаря тому, что контейнеры пода объединены общим сетевым интерфейсом, архитектурный паттерн Sidecar из статьи 2. Envoy и sidecar работает столь элегантно: ваш основной Go-сервис слушает порт 8080, а прокси-контейнер Envoy перехватывает входящие запросы и передаёт их приложению по адресу 127.0.0.1:8080 с околонулевыми накладными расходами.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Pod ["Анатомия Pod (Единый Network / IPC Namespace)"]
direction TB
Pause["Инфраструктурный pause контейнер<br>(Держит veth и сетевой IP)"]
App["Основной контейнер:<br>Go-бэкенд (порт 8080)"]
Sidecar["Sidecar контейнер:<br>Envoy / Logging агент"]:::process
Vol[("Shared Volume<br>(Общий диск)")]
Pause --- App
Pause --- Sidecar
App <-->|"Связь через localhost"| Sidecar
App --- Vol
Sidecar --- Vol
end
2. Deployment: Армия клонов и менеджмент состояния
Вы можете задеплоить приложение, создав манифест kind: Pod. Однако в промышленной эксплуатации запускать «голые» поды строго запрещено.
Поды по своей природе глубоко эфемерны. В парадигме Cloud Native действует доктрина: «Скот, а не домашние питомцы» (Cattle vs Pets). Если у физического сервера отключится питание, голый под погибнет вместе с ним навсегда. Kubernetes даже не попытается воскресить его на соседнем исправном сервере, потому что у него нет указаний, кто должен его перезапустить.
Чтобы гарантировать отказоустойчивость и автоматическое восстановление, нам необходим высокоуровневый управляющий контроллер — Deployment (Развёртывание).
Цепочка делегирования
Deployment реализует иерархический принцип разделения обязанностей: сам он не порождает поды напрямую, а делегирует эту рутину промежуточной сущности — ReplicaSet.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
D["Deployment<br>(Декларирует стратегию RollingUpdate и образ Go v1.2)"]
subgraph RS_Group ["Управление жизненным циклом"]
RS_Old["ReplicaSet v1.1<br>(Масштабируется 3 -> 0)"]
RS_New["ReplicaSet v1.2<br>(Масштабируется 0 -> 3)"]
end
D -->|"Постепенный перевод реплик"| RS_Old
D -->|"Постепенный перевод реплик"| RS_New
RS_New --> P1["Pod 1 (v1.2)"]
RS_New --> P2["Pod 2 (v1.2)"]
RS_New --> P3["Pod 3 (v1.2)"]
- Deployment описывает мета-стратегию: какую версию образа мы эксплуатируем и как происходит процесс обновления (например, канареечный релиз или плавное замещение — Rolling Update).
- ReplicaSet — это строгий специализированный контроллер из состава демона
kube-controller-manager. В его основе заложен примитивный цикл:
$$\text{Текущее число живых подов} \stackrel{?}{=} \text{Желаемое число подов}$$
Если сервер внезапно сгорает, ReplicaSet фиксирует, что вместо 3 подов в живых осталось 2, и мгновенно регистрирует в etcd спецификацию нового пода, который планировщик немедленно отправляет на уцелевший узел.
3. Service: Стабильный якорь в море хаоса
Поскольку поды смертны, они непрерывно мигрируют по кластеру. При каждом новом рождении под получает совершенно новый динамический IP-адрес.
Возникает критическая проблема сетевой адресации: как клиентскому компоненту Frontend гарантированно отправлять запросы в микросервис Backend, если физические адреса бэкенда меняются ежеминутно?
Для решения этой задачи создана ключевая сетевая абстракция — Service (Сервис). Это стабильная, несгораемая точка входа, обладающая постоянным виртуальным IP-адресом (ClusterIP) и статическим доменным именем в пространстве CoreDNS.
Каждый созданный сервис автоматически регистрируется во внутреннем DNS-сервере кластера. Теперь Go-бэкенд может надежно обращаться к соседу по человекочитаемому адресу:
resp, err := http.Get("http://backend-svc.my-namespace.svc.cluster.local:8080/api/v1/health")
Иллюзия ClusterIP (Mechanical Sympathy)
Для любого сетевого инженера механика ClusterIP поначалу выглядит необъяснимой мистикой.
Всю магию маршрутизации реализует системный демон kube-proxy, работающий на каждом рабочем узле (Worker Node):
- При появлении объекта
Serviceдемонkube-proxyчерез механизм Watch API получает событие от API-сервера вместе со списком живых IP-адресов подов (объектEndpoints/EndpointSlice). kube-proxyобращается к подсистеме ядра Linux и генерирует правила в таблицахiptables(или хэш-таблицахIPVS).- Когда Go-клиент отправляет пакет
TCP SYNна виртуальный адрес10.96.0.1:8080, пакет попадает в сетевой стек ядра хоста. - В цепочке проверок
PREROUTINGядро перехватывает пакет, сверяет его с правиламиiptablesи производит трансляцию адреса назначения — DNAT (Destination Network Address Translation). - Сетевой стек ядра на лету переписывает IP-адрес назначения в заголовке IP-пакета с виртуального
10.96.0.1на реальный адрес одного из доступных подов (например,10.244.2.14).
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Client["Go Клиент (Frontend)<br>Запрос на VIP: 10.96.0.1"]:::entry -->|"Сетевой стек хоста"| Kernel["Ядро Linux (PREROUTING)"]
subgraph DNAT ["Трансляция адреса DNAT (iptables / IPVS)"]
Kernel -->|"Случайный выбор вероятности"| Rule{"Пакетный фильтр"}
Rule -->|"50% трафика"| Target1["Подмена IP на 10.244.1.5"]
Rule -->|"50% трафика"| Target2["Подмена IP на 10.244.2.14"]
end
Target1 --> PodA["Pod A (Backend)<br>10.244.1.5:8080"]
Target2 --> PodB["Pod B (Backend)<br>10.244.2.14:8080"]
Балансировка трафика между подами в стандартном режиме iptables осуществляется за счёт модуля вероятностного распределения statistic ядра Linux.
Headless Service (Безголовый сервис) и gRPC
В микросервисной экосистеме Go протокол gRPC занимает доминирующее положение. И именно здесь неопытные инженеры наступают на классические грабли архитектуры Kubernetes.
gRPC функционирует поверх протокола HTTP/2. Фундаментальная особенность HTTP/2 — использование единственного мультиплексированного долгоживущего TCP-соединения (Persistent Connection).
Что произойдёт, если вы попытаетесь балансировать gRPC-трафик через стандартный Kubernetes Service (ClusterIP)?
- Ваш Go-клиент при старте разрезолвит адрес сервиса, получит ClusterIP и отправит единственный
TCP SYN. - Ядро Linux через
iptablesтранслирует этот коннект наPod A. TCP-рукопожатие успешно завершается. - Соединение остаётся открытым часами. Все последующие сотни тысяч gRPC-вызовов (RPC) будут передаваться внутри этого единственного TCP-соединения строго в
Pod A! - В результате
Pod Aпадает от перегрузки, в то время как соседние 9 реплик бэкенда абсолютно простаивают. Механизмiptablesслеп к протоколу HTTP/2 — он умеет балансировать только новые TCP-соединения, но не отдельные HTTP/2 стримы!
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Problem ["Проблема: Обычный ClusterIP + gRPC"]
C1["Go gRPC Клиент"]:::entry -->|"Единое постоянное TCP соединение"| VIP["ClusterIP VIP (iptables)"]
VIP -->|"Все вызовы летят только сюда!"| PA["Pod A (100% перегрузка)"]
VIP -.->|"0% трафика (простой)"| PB["Pod B"]
VIP -.->|"0% трафика (простой)"| PC["Pod C"]
end
subgraph Solution ["Решение: Headless Service (clusterIP: None)"]
C2["Go gRPC Клиент<br>(Client-side Round Robin)"]
DNS["CoreDNS<br>(Возвращает список всех IP)"]
C2 <-->|"DNS Lookup"| DNS
C2 -->|"Стрим 1"| P1["Pod A"]
C2 -->|"Стрим 2"| P2["Pod B"]
C2 -->|"Стрим 3"| P3["Pod C"]
end
Инженерное решение для Go/gRPC: Использование специального типа сервиса — Headless Service (Безголовый сервис).
Для создания такого сервиса в спецификации манифеста явно указывается флаг:
spec:
clusterIP: None
При такой конфигурации kube-proxy не выделяет виртуальный IP и не создаёт правил трансляции в ядре. Вместо этого кластерный DNS-сервер (CoreDNS) при запросе возвращает клиенту полный список IP-адресов всех активных подов (набор A-записей).
Go-клиент gRPC использует эту информацию для клиентской балансировки (Client-Side Load Balancing). Передавая специальную опцию конфигурации:
conn, err := grpc.Dial(
"dns:///backend-svc.my-namespace.svc.cluster.local:8080",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithInsecure(),
)
Клиентская библиотека Go самостоятельно открывает параллельные TCP-сокеты ко всем подам за сервисом и равномерно распределяет каждый отдельный RPC-запрос по круговому алгоритму (Round Robin).
Итог
- Pod — неделимый атом: Kubernetes оперирует не контейнерами, а подами. Контейнеры внутри пода разделяют общую сеть
localhostблагодаря невидимому инфраструктурному контейнеруpause. - Deployment и ReplicaSet: Контроллеры декларативного состояния преобразуют эфемерные, смертные поды в самоисцеляющуюся систему, способную выполнять бесшовные обновления без простоя.
- ClusterIP — иллюзия ядра: Виртуальный IP-адрес сервиса не существует на физических сетевых интерфейсах, а эмулируется пакетным фильтром ядра Linux (
iptables/IPVS) через операцию DNAT. - Коварство L7 и gRPC: Долгоживущие соединения HTTP/2 ломают балансировку через обычный ClusterIP. Для эффективного распределения gRPC-трафика используйте Headless Service (
clusterIP: None) в связке с клиентской балансировкой Go (grpc.WithDefaultServiceConfig) либо внедряйте Service Mesh (1. Service mesh).
Мы научились собирать контейнеры, запускать реплики и обеспечивать надёжную маршрутизацию сетевого трафика. Однако любое продакшен-приложение требует гибкой конфигурации: паролей от баз данных, приватных ключей шифрования и настроек таймаутов, зависящих от конкретного окружения. Вшивать секреты в бинарник или Docker-образ недопустимо. Как безопасно внедрять и обновлять настройки в работающих подах? Об этом — в следующей лекции: 4. ConfigMap и Secret.