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 принимает глобальные архитектурные решения: следит за сбоями, балансирует емкость и непрерывно согласует конфигурацию. Он состоит из четырёх ключевых служб:

  1. kube-apiserver (Сердце и шлюз системы):
    Высокопроизводительный сервер REST API, написанный на Go. В Kubernetes действует фундаментальный закон: ни один компонент кластера не имеет права общаться с другим компонентом напрямую. Все запросы — от утилиты администратора до системных демонов — проходят строго через kube-apiserver. Он проводит аутентификацию, проверяет ролевую модель доступа (RBAC), мутирует и валидирует входящие документы.
  2. etcd (Память и источник правды):
    Распределённое, строго согласованное хранилище формата «ключ-значение». Это единственный компонент с состоянием (Stateful) во всем Control Plane. Все остальные сервисы абсолютно stateless. etcd использует алгоритм консенсуса (который мы подробно препарировали в лекции 2. Raft. Основы). Если кластер etcd необратимо повреждён — весь кластер Kubernetes мгновенно теряет память и погибает.
  3. kube-scheduler (Логист):
    Когда вы создаёте новый контейнер, kube-apiserver лишь валидирует манифест и сохраняет JSON-запись в etcd. Сам по себе сервер API ничего не запускает! В этот момент в игру вступает kube-scheduler. Он обнаруживает новый объект, у которого ещё не назначен целевой сервер, проводит двухфазный анализ доступных нод (фильтрация по ресурсам CPU/RAM, учёт Taints, Tolerations и правил Affinity) и назначает контейнеру оптимальный узел.
  4. kube-controller-manager (Нервная система и автопилот):
    Оркестр фоновых процессов (бесконечных циклов for), которые непрерывно следят за состоянием кластера: контроллер узлов, контроллер репликации, контроллер эндпоинтов.

Worker Nodes (Рабочие узлы)

Это пул серверов (виртуальных машин или bare-metal серверов), на которых непосредственно исполняется ваша полезная нагрузка — контейнеры с микросервисами Go.

  1. kubelet (Капитан узла):
    Системный демон, функционирующий на каждой ноде. Он держит постоянный канал связи с kube-apiserver. Когда scheduler назначает контейнер на данный узел, kubelet принимает спецификацию и транслирует её рантайму контейнеров (containerd, CRI-O) через интерфейс Container Runtime Interface (CRI): «Скачай образ и запусти изолированный процесс». Затем kubelet непрерывно проверяет здоровье процесса и отправляет телеметрию обратно в API.
  2. 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 логика любого контроллера выглядит до гениального просто:

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.

Итог

  1. Суть оркестратора: Kubernetes — это не монолитная система, а событийно-ориентированный ансамбль независимых микросервисов, координирующих свои действия через единую базу состояния etcd.
  2. Асинхронная природа: Команда развёртывания фиксирует лишь декларативное намерение в API; создание физических процессов на нодах происходит асинхронно через цепочку контроллеров.
  3. Реактивный Watch API: Благодаря механизму стриминга событий в библиотеке client-go, кластер реагирует на любые инциденты за миллисекунды без накладных расходов на периодический опрос.
  4. Контракт эфемерности: Поды смертны и заменимы. Go-бэкенд должен проектироваться с расчётом на внезапные рестарты и корректную обработку сигнала SIGTERM.

Мы разобрались с фундаментом и устройством управляющего контура Kubernetes. Но какими именно строительными блоками мы, как разработчики, должны оперировать, чтобы описать топологию, запустить реплики и открыть наш сервис внешнему миру? В следующей лекции мы детально изучим базовую триаду объектов: 3. Pod, Deployment, Service.