2. Kubernetes. Основы
Оркестрация хаоса: Как управлять армией контейнеров
В предыдущей статье 1. Контейнеризация и Docker мы научились упаковывать наш Go-бэкенд в герметичный, легковесный и самодостаточный артефакт. На локальном компьютере разработчика этой магии вполне достаточно: вы вводите команду docker run, ядро изолирует процесс через namespaces, и сервер готов принимать локальный трафик.
Но что происходит, когда проект выходит в открытое море продакшена?
Представьте инфраструктуру современного финтеха или маркетплейса: сотня физических серверов (нод) в нескольких дата-центрах и более тысячи запущенных микросервисов.
- На какой конкретно сервер поместить свежесобранный экземпляр сервиса платежей?
- Что предпринять в три часа ночи, если на физическом сервере сгорел блок питания или лопнула планка оперативной памяти?
- Как плавно доставить пользовательский трафик к новому контейнеру, если при каждом перезапуске виртуальный интерфейс получает совершенно случайный IP-адрес?
- Как обновить бинарник без потери соединений и без единой секунды простоя (Zero Downtime)?
Инструмент docker run бессилен перед этими вызовами. Чтобы управлять армией разрозненных контейнеров, распределённой системе требуется главнокомандующий — Оркестратор.
Безоговорочным стандартом современной индустрии стал Kubernetes (K8s). По своей сути это распределённая операционная система дата-центра, превращающая парк разнородного серверного железа в единый вычислительный пул.
Для Go-разработчика Kubernetes имеет сакральное значение: вся экосистема Kubernetes от первой до последней строчки написана на Go. Изучая его архитектуру, мы не просто учимся грамотно развёртывать сервисы, но и перенимаем лучшие инженерные паттерны распределённых систем у архитекторов Google.
В этой лекции мы препарируем анатомию кластера, разберём циклы примирения (Reconciliation) и выясним, как один неосторожный вызов из Go-кода способен обрушить управляющий контур всей компании.
Архитектура: Мозг и Мышцы
Любой кластер Kubernetes физически и логически разделяется на два взаимодействующих слоя: Control Plane (Управляющий слой, мозг кластера) и Worker Nodes (Рабочие узлы, его исполнительные мышцы).
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 CP ["Control Plane (Управляющий контур)"]
direction TB
API["kube-apiserver<br>(Единый REST API шлюз на Go)"]
ETCD[("etcd<br>Raft KV-хранилище состояния")]:::data
SCHED["kube-scheduler<br>(Интеллектуальный логист)"]
CTRL["kube-controller-manager<br>(Автопилот: циклы примирения)"]
end
subgraph Node1 ["Worker Node 1"]
direction TB
KUB1["kubelet (Агент узла)"]
PROXY1["kube-proxy (Сетевой координатор)"]
CRI1["Container Runtime (containerd)"]
POD1["Pod: Go-сервис (ELF)"]
end
subgraph Node2 ["Worker Node 2"]
direction TB
KUB2["kubelet"]:::process
PROXY2["kube-proxy"]:::network
CRI2["Container Runtime"]:::system
POD2["Pod: Реплика Go-сервиса"]:::process
end
API <-->|"Транзакции состояния"| ETCD
API <-->|"Watch API: отслеживание Pod"| SCHED
API <-->|"Watch API: контроль реплик"| CTRL
API <-->|"gRPC / HTTPS приказы"| KUB1
API <-->|"gRPC / HTTPS приказы"| KUB2
API -->|"Сетевая топология"| PROXY1
API -->|"Сетевая топология"| PROXY2
KUB1 -->|"CRI gRPC"| CRI1 --> POD1
KUB2 -->|"CRI gRPC"| CRI2 --> POD2
Control Plane (Управляющий слой)
Control Plane принимает глобальные архитектурные решения: следит за сбоями, балансирует емкость и непрерывно согласует конфигурацию. Он состоит из четырёх ключевых служб:
kube-apiserver(Сердце и шлюз системы):
Высокопроизводительный сервер REST API, написанный на Go. В Kubernetes действует фундаментальный закон: ни один компонент кластера не имеет права общаться с другим компонентом напрямую. Все запросы — от утилиты администратора до системных демонов — проходят строго черезkube-apiserver. Он проводит аутентификацию, проверяет ролевую модель доступа (RBAC), мутирует и валидирует входящие документы.etcd(Память и источник правды):
Распределённое, строго согласованное хранилище формата «ключ-значение». Это единственный компонент с состоянием (Stateful) во всем Control Plane. Все остальные сервисы абсолютно stateless.etcdиспользует алгоритм консенсуса (который мы подробно препарировали в лекции 2. Raft. Основы). Если кластерetcdнеобратимо повреждён — весь кластер Kubernetes мгновенно теряет память и погибает.kube-scheduler(Логист):
Когда вы создаёте новый контейнер,kube-apiserverлишь валидирует манифест и сохраняет JSON-запись вetcd. Сам по себе сервер API ничего не запускает! В этот момент в игру вступаетkube-scheduler. Он обнаруживает новый объект, у которого ещё не назначен целевой сервер, проводит двухфазный анализ доступных нод (фильтрация по ресурсам CPU/RAM, учёт Taints, Tolerations и правил Affinity) и назначает контейнеру оптимальный узел.kube-controller-manager(Нервная система и автопилот):
Оркестр фоновых процессов (бесконечных цикловfor), которые непрерывно следят за состоянием кластера: контроллер узлов, контроллер репликации, контроллер эндпоинтов.
Worker Nodes (Рабочие узлы)
Это пул серверов (виртуальных машин или bare-metal серверов), на которых непосредственно исполняется ваша полезная нагрузка — контейнеры с микросервисами Go.
kubelet(Капитан узла):
Системный демон, функционирующий на каждой ноде. Он держит постоянный канал связи сkube-apiserver. Когдаschedulerназначает контейнер на данный узел,kubeletпринимает спецификацию и транслирует её рантайму контейнеров (containerd, CRI-O) через интерфейс Container Runtime Interface (CRI): «Скачай образ и запусти изолированный процесс». Затемkubeletнепрерывно проверяет здоровье процесса и отправляет телеметрию обратно в API.kube-proxy(Сетевой менеджер):
Компонент, отвечающий за сетевую магию. Он следит за объектамиServiceи манипулирует пакетным фильтром ядра Linux (iptablesилиIPVS), обеспечивая маршрутизацию трафика к подам вне зависимости от того, на каком узле они запущены.
Парадигма Декларативности и Reconciliation Loop
Фундаментальное отличие Kubernetes от традиционных bash-скриптов и устаревших систем автоматизации заключается в смене парадигмы управления:
- Императивный подход (Как сделать):
Инженер отдаёт набор последовательных инструкций: «Создай виртуальную машину. Установи туда Go. Запусти бинарник. Проверь порт 8080. Если упал — выполни перезапуск». Если скрипт сломался на середине, состояние системы становится непредсказуемым. - Декларативный подход (Что должно быть):
Инженер формулирует желаемое целевое состояние (Desired State): «В кластере обязано стабильно работать ровно 3 работоспособные реплики сервиса биллинга версии v1.4».
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
subgraph Loop ["Reconciliation Loop (Цикл примирения)"]
direction TB
Observe["1. Observe (Наблюдение)<br>Считывание Actual State из кластера"]
Diff["2. Analyze (Анализ разницы)<br>Diff = Desired State - Actual State"]
Act["3. Act (Действие)<br>Устранение расхождения (создать / удалить)"]
Observe --> Diff --> Act --> Observe
end
Как именно система гарантирует достижение цели — забота самого оркестратора. Ядром архитектуры Kubernetes выступает Reconciliation Loop (Цикл примирения). В псевдокоде на Go логика любого контроллера выглядит до гениального просто:
for {
desired := getDesiredState() // Что хочет пользователь из YAML
actual := getActualState() // Что реально крутится на нодах
if diff := compare(desired, actual); diff != nil {
reconcile(diff) // Создать недостающий под или прибить лишний
}
waitForChangeOrTimeout()
}
Анатомия создания: Что происходит, когда вы деплоите приложение?
На технических интервью уровня Senior классический вопрос звучит обманчиво просто, обнажая подлинное понимание асинхронного устройства K8s.
sequenceDiagram
autonumber
actor Dev as Инженер (kubectl)
participant API as kube-apiserver
participant DB as etcd
participant Ctrl as Controller Manager
participant Sched as kube-scheduler
participant Kube as kubelet (Node A)
participant CRI as containerd (CRI)
Dev->>API: kubectl apply -f app.yaml (HTTP POST)
API->>API: AuthN / AuthZ / Admission
API->>DB: Сохранить Deployment
DB-->>API: OK
API-->>Dev: HTTP 201 Created (Только намерение!)
Note over API,Ctrl: Watch API уведомление
Ctrl->>API: Создать 3 Pod-объекта (nodeName="")
API->>DB: Запись Pods в etcd
Note over API,Sched: Watch API: новые Pods без ноды
Sched->>Sched: Filtering & Scoring нод
Sched->>API: Bind: назначить Pod на Node A
Note over API,Kube: Watch API: назначен Pod для Node A
Kube->>CRI: RunPodSandbox + CreateContainer (gRPC)
CRI->>CRI: Linux namespaces + cgroups -> запуск Go ELF
Kube->>API: Статус: Pod Running
Архитектурные ловушки (Gotchas)
1. Перегрузка API Сервера
В Kubernetes управляющий шлюз kube-apiserver — главная критическая точка отказа.
Если команда разрабатывает собственный оператор на Go (Custom Controller / CRD) и неопытный разработчик в цикле выполняет прямые вызовы LIST всех подов в кластере вместо использования кэширующей прослойки Informer / Lister из пакета client-go, API-сервер будет вынужден сериализовать мегабайты JSON на каждый запрос. В крупных кластерах это моментально вызывает взрывное потребление памяти, OOM на мастер-нодах и паралич Control Plane.
2. Split Brain в etcd
Хранилище etcd строго подчиняется законам консенсуса Raft: для выполнения любых операций записи требуется строгое математическое большинство узлов — кворум:
$$\text{Кворум} = \left\lfloor \frac{N}{2} \right\rfloor + 1$$
Если в кластере развёрнуто 2 мастер-узла (грубейший антипаттерн), и сеть между ними моргнула:
- На каждом изолированном узле остаётся $1$ нода из $2$.
- Кворум ($2$) не достигнут ни на одной стороне!
- Кластер Control Plane намертво блокирует любые изменения и переходит в режим Read-Only.
Существующие поды на рабочих узлах продолжат обслуживать сетевые пакеты, но вы не сможете задеплоить ни один фикс, а упавшие поды никогда не будут перезапущены.
Стандарт надежности: Control Plane всегда комплектуется нечётным количеством мастер-узлов: минимум 3 ноды (допускает падение 1 узла) или 5 нод (допускает падение 2 узлов).
3. Эфемерность всего сущего
Ключевой ментальный сдвиг при переходе от классических серверов к парадигме Kubernetes: ни один под не является постоянным.
В любой момент времени планировщик или контроллер узла может без предупреждения вытеснить ваш под (Eviction): при нехватке памяти на ноде, при накатывании обновлений на операционную систему сервера или при срабатывании автоскейлера.
Ваш Go-сервер обязан соблюдать архитектурный контракт:
- Не хранить постоянное состояние на локальной файловой системе пода;
- Быть абсолютно stateless-компонентом;
- Мгновенно перехватывать системный сигнал
SIGTERMот ядра Linux и корректно освобождать сетевые соединения (Graceful Shutdown), не дожидаясь контрольного выстрелаSIGKILL.
Итог
- Суть оркестратора: Kubernetes — это не монолитная система, а событийно-ориентированный ансамбль независимых микросервисов, координирующих свои действия через единую базу состояния
etcd. - Асинхронная природа: Команда развёртывания фиксирует лишь декларативное намерение в API; создание физических процессов на нодах происходит асинхронно через цепочку контроллеров.
- Реактивный Watch API: Благодаря механизму стриминга событий в библиотеке
client-go, кластер реагирует на любые инциденты за миллисекунды без накладных расходов на периодический опрос. - Контракт эфемерности: Поды смертны и заменимы. Go-бэкенд должен проектироваться с расчётом на внезапные рестарты и корректную обработку сигнала
SIGTERM.
Мы разобрались с фундаментом и устройством управляющего контура Kubernetes. Но какими именно строительными блоками мы, как разработчики, должны оперировать, чтобы описать топологию, запустить реплики и открыть наш сервис внешнему миру? В следующей лекции мы детально изучим базовую триаду объектов: 3. Pod, Deployment, Service.