Фундамент Kubernetes: Архитектура и базовые абстракции

Курс закладывает концептуальную базу для понимания оркестрации контейнеров. Вы изучите логику взаимодействия компонентов Control Plane и Worker Nodes, чтобы понимать, как декларативное описание превращается в работающую инфраструктуру.

Контейнеризация и генезис Kubernetes: от изоляции процессов к оркестрации систем

Контейнеризация и генезис Kubernetes: от изоляции процессов к оркестрации систем

Представьте, что у вас есть идеально упакованное приложение в Docker-контейнере. Запустить его на своем ноутбуке — дело одной секунды. А теперь представьте, что у вас 50 микросервисов, каждый из которых должен работать в 10 экземплярах для отказоустойчивости, и вся эта система распределена по 100 физическим серверам. Внезапно один сервер сгорает, сеть моргает, а на сайт приходит в три раза больше пользователей. Кто будет перезапускать упавшие контейнеры, перенаправлять трафик и выделять новые ресурсы?

Человек с этой задачей не справится. Нам нужна система, которая будет управлять дата-центром так же легко, как операционная система управляет процессами на одном компьютере.

Иллюзия контейнера и предел изоляции

Чтобы понять Kubernetes, нужно сначала развеять главную иллюзию о контейнерах. Контейнер — это не маленькая виртуальная машина. С точки зрения ядра Linux, контейнер — это просто обычный процесс (или группа процессов), вокруг которого возвели невидимые стены с помощью механизмов namespaces (изоляция видимости) и cgroups (ограничение ресурсов).

Контейнеризация решила колоссальную проблему: она устранила конфликт зависимостей («на моей машине работает, а на сервере нет»). Мы получили предсказуемый, изолированный юнит развертывания.

Но контейнеры сами по себе «слепы и глупы». Docker-движок на конкретном сервере знает только о тех процессах, которые запущены локально. Если сервер выйдет из строя, все запущенные на нем контейнеры умрут вместе с ним. Чтобы построить отказоустойчивую систему, нам необходим надсмотрщик, который видит картину целиком. Этот процесс управления множеством контейнеров на множестве серверов называется оркестрацией.

Kubernetes (K8s) — это платформа с открытым исходным кодом для автоматизации развертывания, масштабирования и управления контейнеризированными приложениями. По сути, это операционная система для вашего кластера серверов.

Разделение труда: Мозг и Мускулы

Любая сложная система требует разделения на управляющий центр и исполнителей. Kubernetes доводит этот принцип до абсолюта. Кластер K8s физически и логически разделен на две большие части: Control Plane (панель управления) и Worker Nodes (рабочие узлы).

Worker Nodes: Мускулы кластера

Это серверы (физические или виртуальные), на которых непосредственно выполняется ваш полезный код. Их главная задача — принимать приказы, запускать контейнеры, следить за их работой и отчитываться о состоянии. Если Worker Node выходит из строя, кластер теряет часть вычислительной мощности, но продолжает работать — система просто перенесет задачи на другие узлы.

Control Plane: Мозг кластера

Это набор системных компонентов, которые принимают глобальные решения о кластере. Control Plane не запускает пользовательские приложения. Его задача — хранить информацию о том, как должна выглядеть система, следить за тем, как она выглядит сейчас, и отдавать команды Worker Nodes, чтобы исправить расхождения.

Если выходит из строя Control Plane (в случае отсутствия резервирования), ваши приложения на Worker Nodes продолжат работать, но кластер «ослепнет»: вы не сможете развернуть ничего нового, а упавшие контейнеры никто не перезапустит.

Декларативный подход: как думает Kubernetes

Разделение на мозг и мускулы — это лишь структура. Настоящая магия Kubernetes кроется в том, как именно Control Plane управляет узлами.

В традиционном администрировании мы используем императивный подход. Мы пишем скрипты, которые содержат пошаговые инструкции: «подключись к серверу А, скачай образ Б, запусти процесс В, если ошибка — сделай Г». Это похоже на то, как вы диктуете таксисту каждый поворот: «сейчас направо, потом 100 метров прямо, затем налево». Если дорога перекрыта, ваша инструкция ломается.

Kubernetes использует декларативный подход. Вы не говорите системе, как делать. Вы описываете, какой результат хотите получить. Это похоже на GPS-навигатор: вы просто вводите конечную точку, а система сама строит маршрут. Если на пути возникает пробка, навигатор перестраивает маршрут без вашего участия.

В Kubernetes вы создаете YAML-файл (манифест), в котором заявляете: «Я хочу, чтобы в кластере всегда работало 3 копии моего веб-сервера». Вы отдаете этот файл в Control Plane. На этом ваша работа закончена.

Цикл согласования (Reconciliation Loop)

Чтобы декларативный подход работал, компоненты Control Plane непрерывно выполняют бесконечный цикл, который называется Reconciliation Loop (цикл согласования).

Его математическая логика предельно проста. У системы есть два состояния:

  1. StatedesiredState_{desired} — желаемое состояние (то, что вы описали в YAML).
  2. StateactualState_{actual} — текущее состояние (то, что реально работает на серверах прямо сейчас).

Каждую секунду Kubernetes проверяет уравнение: Stateactual=StatedesiredState_{actual} = State_{desired}.

Если уравнение сходится, система ничего не делает. Но если происходит сбой — например, один сервер сгорел, и количество запущенных серверов упало до двух, возникает неравенство: StateactualStatedesiredState_{actual} \neq State_{desired}.

Увидев расхождение, Kubernetes немедленно генерирует задачи для Worker Nodes, чтобы вернуть систему в баланс (в данном случае — приказывает уцелевшим узлам запустить еще один контейнер).

Этот непрерывный цикл — бьющееся сердце Kubernetes. Благодаря ему система способна к самовосстановлению (self-healing). Вам не нужно писать скрипты обработки ошибок для падающих серверов. Вы просто фиксируете желаемое состояние, а Kubernetes берет на себя всю грязную работу по его поддержанию в хаотичном мире реального дата-центра.

Мы заложили фундамент: поняли, зачем нужен кластер, как он разделен на управляющую и рабочую части, и по какому принципу мыслит система. В следующем шаге мы заглянем под капот Control Plane и разберем, из каких именно шестеренок состоит «мозг» Kubernetes и как они общаются между собой.

Архитектура Control Plane: мозг кластера и механизмы принятия решений

Архитектура Control Plane: мозг кластера и механизмы принятия решений

Если вытащить кабель питания из сервера, на котором работает база данных, система рухнет мгновенно. Но если выключить сервер, управляющий кластером Kubernetes, ваши приложения продолжат работать как ни в чём не бывало. Этот парадокс — ключ к пониманию того, как устроен Control Plane.

В предыдущей главе мы выяснили, что Kubernetes опирается на декларативный подход и бесконечный цикл согласования (Reconciliation Loop). Мы задаем желаемое состояние, а кластер подгоняет под него реальность. Но где именно хранится это «желаемое состояние»? Кто конкретно сравнивает его с реальностью? И кто принимает решение, на каком физическом сервере запустить контейнер?

Все эти задачи решает Control Plane (плоскость управления). Это не монолитная программа, а набор из четырёх основных (в облачных средах к ним добавляется пятый — cloud-controller-manager) независимых компонентов, которые общаются друг с другом строго по сети.

Давайте разберем каждый из них, двигаясь от точки входа пользователя вглубь системы.

kube-apiserver: строгий швейцар и единая точка входа

Что это: HTTP REST API сервер, который предоставляет интерфейс для управления кластером.

Зачем нужен: Это единственный компонент кластера, с которым вы взаимодействуете напрямую, когда вводите команду kubectl apply -f app.yaml. Более того, это единственный компонент, которому разрешено общаться с базой данных кластера. Все остальные части Kubernetes (и Control Plane, и рабочие узлы) общаются только с kube-apiserver.

Как работает внутри: Когда запрос поступает в kube-apiserver, он проходит строгий конвейер проверок:

  1. Аутентификация (Authentication): «Кто ты?» (Проверка сертификатов, токенов).
  2. Авторизация (Authorization): «Имеешь ли ты право это делать?» (Проверка RBAC — ролевой модели доступа).
  3. Admission Control (Контроль допуска): «Не нарушает ли твой запрос глобальные правила кластера?» На этом этапе специальные плагины могут изменить ваш запрос (например, принудительно добавить лимиты памяти) или отклонить его.
  4. Валидация (Validation): Проверка синтаксиса YAML/JSON.
  5. Сохранение: Если всё отлично, сервер записывает новое состояние в базу данных.

etcd: абсолютная память кластера

Что это: Распределенное, надежное key-value (ключ-значение) хранилище.

Зачем нужен: Это единственное место, где Kubernetes хранит свое состояние. Если информации нет в etcd, значит, этого не существует в кластере. Упал etcd — кластер потерял память и превратился в набор неуправляемых серверов.

Как работает внутри: Поскольку etcd — это сердце системы, его делают отказоустойчивым, запуская сразу на нескольких серверах. Чтобы серверы договорились между собой, какое состояние правильное, используется алгоритм консенсуса Raft.

Для работы Raft требуется кворум (строгое большинство узлов). Формула кворума: Q=N2+1Q = \lfloor \frac{N}{2} \rfloor + 1 Где NN — общее количество узлов etcd, а \lfloor \dots \rfloor — округление вниз.

Практический пример: Если у вас кластер etcd из 3 узлов, кворум равен 3/2+1=2\lfloor 3/2 \rfloor + 1 = 2. Кластер переживет падение одного узла. Если узлов 4, кворум равен 4/2+1=3\lfloor 4/2 \rfloor + 1 = 3. Кластер всё ещё переживет падение только одного узла. Именно поэтому etcd всегда разворачивают нечетным числом (3, 5, 7).

Ключевая фича — Watch API: etcd умеет не просто хранить данные, но и мгновенно уведомлять подписчиков об их изменении. kube-apiserver подписывается на эти изменения и транслирует их остальным компонентам. Благодаря этому компонентам не нужно ежесекундно опрашивать базу («А не появилось ли что-то новое?»). Они просто ждут события.

kube-controller-manager: неутомимый исполнитель

Что это: Фоновый процесс, в котором запущен набор независимых контроллеров.

Зачем нужен: Помните Reconciliation Loop? kube-controller-manager — это и есть тот, кто его крутит. etcd только хранит данные, apiserver только принимает запросы. Сам по себе кластер ничего не сделает, пока контроллер не заметит разницу между желаемым и действительным.

Как работает внутри: Внутри этого бинарного файла работают десятки контроллеров. Например:

  • Node Controller: следит за тем, живы ли рабочие узлы. Если узел перестал отвечать, контроллер помечает его как недоступный.
  • ReplicaSet Controller: следит, чтобы количество запущенных копий приложения всегда совпадало с тем, что вы указали в конфигурации. Убили один контейнер? Контроллер заметит это и попросит создать новый.

Контроллеры работают по принципу подписки (Watch). Они слушают kube-apiserver. Как только вы просите создать 3 копии приложения, контроллер просыпается, видит, что сейчас 0 копий, и отправляет в apiserver запрос: «Создай 3 объекта Pod».

kube-scheduler: расчетливый логист

Что это: Компонент, отвечающий за распределение нагрузки по физическим серверам.

Зачем нужен: Когда контроллер решает, что нужно запустить новый Pod (контейнер), этот Pod изначально висит в статусе Pending (в ожидании) и не привязан ни к какому узлу. kube-scheduler находит для него оптимальный дом.

Как работает внутри: Процесс выбора узла проходит в два этапа:

  1. Filtering (Фильтрация): Планировщик отбрасывает узлы, которые физически не подходят. Например, если Pod требует 4 ГБ оперативной памяти, а на узле свободно только 2 ГБ, этот узел выбывает.
  2. Scoring (Оценка): Оставшимся узлам выставляются баллы от 0 до 100. Планировщик учитывает множество факторов: равномерное распределение нагрузки, наличие уже скачанных образов Docker на узле, правила сродства (affinity — например, «запускать этот Pod поближе к базе данных»).

Под с максимальным баллом побеждает, и kube-scheduler отправляет в apiserver запрос на привязку (Binding) Pod'а к выбранному узлу.

Синтез: Жизненный цикл одного запроса

Чтобы знания не остались разрозненными фактами, давайте посмотрим, как эти четыре компонента работают вместе как единый оркестр.

Представьте, вы выполняете команду: kubectl apply -f my-app.yaml (в файле указано: хочу 1 экземпляр приложения).

  1. kubectl отправляет YAML в kube-apiserver.
  2. kube-apiserver проверяет ваши права, валидирует запрос и сохраняет его в etcd.
  3. etcd подтверждает запись. Теперь желаемое состояние — «должно быть 1 приложение».
  4. kube-controller-manager через Watch API узнает о новом желаемом состоянии. Он видит, что реальность (0 приложений) не совпадает с планом. Он формирует объект Pod и отправляет его обратно в kube-apiserver.
  5. kube-apiserver сохраняет новый Pod в etcd. У Pod'а пока нет узла (Node = пусто).
  6. kube-scheduler через Watch API замечает Pod без узла. Он оценивает доступные серверы, выбирает лучший (например, Node-2) и сообщает об этом в kube-apiserver.
  7. kube-apiserver обновляет запись в etcd: теперь Pod привязан к Node-2.

Заметьте: Control Plane не запускает сами контейнеры. Он лишь принимает управленческие решения и обновляет записи в базе данных. Непосредственным запуском займутся компоненты Worker Node, которые мы детально разберем в следующей главе.

Диагностика проблем Control Plane (Troubleshooting)

Как SRE-инженер, вы должны уметь определять, какой именно компонент сломался по симптомам:

  • Симптом: Команда kubectl get pods выдает ошибку connection refused или таймаут. Кто виноват: Лежит kube-apiserver. Кластер продолжает крутить текущую нагрузку, но управлять им невозможно.
  • Симптом: Вы удаляете Pod, чтобы перезапустить приложение, но новый Pod не появляется. Или вы масштабируете Deployment, но количество реплик остается прежним. Кто виноват: Скорее всего, завис или упал kube-controller-manager. Некому отследить разницу между желаемым и действительным состоянием и выдать команду на создание новых объектов.
  • Симптом: Вы создаете новые Pod'ы, они появляются в системе, но бесконечно висят в статусе Pending, хотя ресурсов на серверах полно. Кто виноват: Не работает kube-scheduler. Некому назначить узлы.

Анатомия Worker Node: исполнительные механизмы kubelet, CRI и kube-proxy

Анатомия Worker Node: исполнительные механизмы kubelet, CRI и kube-proxy

В прошлой главе мы остановились на моменте, когда Control Plane принял решение. Компонент kube-scheduler выбрал подходящий узел и записал в etcd: «Этот Pod должен работать на сервере worker-1».

Но Control Plane не подключается к серверам по SSH, чтобы запустить контейнеры. Он лишь обновляет базу данных. Как же абстрактная запись в etcd превращается в реальный процесс Linux, потребляющий CPU и оперативную память?

В этой статье мы разберем анатомию Worker Node — рабочего узла кластера. Мы спустимся с уровня глобальной оркестрации на уровень конкретного сервера и посмотрим, как три главных компонента — kubelet, CRI и kube-proxy — воплощают решения Control Plane в жизнь.

kubelet: Капитан рабочего узла

Если Control Plane — это генеральный штаб, то kubelet — это полевой командир на конкретном сервере. Это единственный компонент Kubernetes, который чаще всего работает не как контейнер, а как обычный системный сервис Linux (через systemd).

Как он работает внутри

kubelet не ждет команд сверху. Он сам активно использует Watch API (с которым мы познакомились при разборе kube-apiserver), чтобы подписаться на изменения в etcd, касающиеся только его узла.

Как только kubelet видит, что к его узлу привязан новый Pod, он запускает свой локальный цикл согласования (Reconciliation Loop):

  1. Читает спецификацию Pod'а (какие образы нужны, какие порты открыть, какие тома примонтировать).
  2. Дает команду локальной среде выполнения (Container Runtime) скачать образы и запустить контейнеры.
  3. Постоянно мониторит состояние запущенных контейнеров (Liveness и Readiness пробы).
  4. Каждые несколько секунд отправляет Heartbeat (пульс) обратно в kube-apiserver, сообщая: «Узел жив, Pod'ы работают».

Если kubelet падает, Control Plane перестает получать Heartbeat. Через 40 секунд (значение по умолчанию) узел помечается как NotReady. Если связь не восстанавливается еще 5 минут (таймаут эвакуации по умолчанию), контроллеры начинают переносить (эвакуировать) Pod'ы на другие серверы.

CRI: Почему Kubernetes отказался от Docker

kubelet знает, что нужно запустить, но сам он контейнеры не создает. Для этого он обращается к Container Runtime (среде выполнения контейнеров).

На заре Kubernetes единственной средой выполнения был Docker. Код для работы с ним был жестко вшит прямо в исходный код kubelet (это называлось in-tree). Когда появлялись новые технологии (например, rkt), разработчикам Kubernetes приходилось дописывать код под каждую из них. Это привело к раздуванию кодовой базы и уязвимостям: ошибка в интеграции с Docker могла положить весь kubelet.

Решением стал CRI (Container Runtime Interface).

CRI — это стандартизированный gRPC-интерфейс. kubelet больше не знает, что такое Docker или containerd. Он просто отправляет стандартные запросы: RunPodSandbox, PullImage, StartContainer. Любая программа, которая умеет слушать этот gRPC-сокет и выполнять команды, может быть средой выполнения для Kubernetes.

Сегодня стандартом де-факто являются легковесные среды выполнения: containerd (выделившийся из Docker) и CRI-O.

Практический инсайт: На современных кластерах команда docker ps на Worker Node выдаст ошибку command not found или покажет пустой список. Docker там просто не установлен. Чтобы посмотреть запущенные контейнеры на уровне узла в обход kubectl, инженеры используют утилиту crictl, которая общается напрямую с CRI: crictl ps — список контейнеров. crictl images — список скачанных образов.

kube-proxy: Сетевой диспетчер (который ничего не проксирует)

Итак, kubelet через CRI запустил контейнер базы данных. У контейнера появился IP-адрес. Но есть проблема: Pod'ы смертны. Если узел перезагрузится, Pod пересоздастся на другом сервере и получит другой IP-адрес.

Как веб-серверу (другому Pod'у) надежно подключаться к базе данных, если ее IP постоянно меняется? Для этого в Kubernetes есть абстракция Service — стабильный виртуальный IP-адрес, который не меняется никогда. Трафик, отправленный на IP Service, должен автоматически балансироваться между актуальными IP-адресами живых Pod'ов.

За то, чтобы эта магия работала на каждом конкретном узле, отвечает kube-proxy.

Несмотря на название, в современных кластерах kube-proxy не является прокси-сервером. Если бы он пропускал весь сетевой трафик через себя (в user-space), это создало бы колоссальное узкое место в производительности.

Вместо этого kube-proxy работает как менеджер правил для ядра Linux:

  1. Он подписывается на kube-apiserver и следит за созданием Service и изменением IP-адресов Pod'ов.
  2. Как только появляется новый Service, kube-proxy генерирует правила маршрутизации.
  3. Он записывает эти правила напрямую в сетевой стек ядра Linux (используя подсистему iptables или IPVS).

Когда веб-сервер отправляет пакет на виртуальный IP базы данных, этот пакет даже не доходит до kube-proxy. Ядро Linux само перехватывает пакет на уровне iptables, видит правило («весь трафик на этот виртуальный IP нужно переписать на реальный IP вот этого Pod'а») и мгновенно перенаправляет его.

iptables против IPVS

По умолчанию kube-proxy использует режим iptables. Он надежен, но имеет математический изъян: правила проверяются последовательно. Сложность поиска составляет O(N)O(N), где NN — количество правил. Практический пример: Если в кластере 10 000 сервисов, ядру придется перебрать до 10 000 правил по очереди для каждого пакета, пока не найдется совпадение. Это как искать нужную фамилию в телефонной книге, читая каждую страницу с самого начала.

Для высоконагруженных production-кластеров kube-proxy переключают в режим IPVS. IPVS использует хеш-таблицы, что дает сложность поиска O(1)O(1), где 11 означает постоянное время выполнения независимо от объема данных. Практический пример: Пакет маршрутизируется мгновенно, независимо от того, 10 сервисов в кластере или 100 000. Это похоже на то, как если бы вы открыли телефонную книгу сразу на нужной странице благодаря алфавитному указателю-закладке.

Диагностика Worker Node в Production

Как SRE-инженер, вы часто будете сталкиваться с ситуацией, когда узел переходит в статус NotReady или Pod'ы зависают в состоянии ContainerCreating. Вот алгоритм локализации проблемы на самом узле (подключившись к нему по SSH):

  1. Проверьте kubelet: Поскольку это systemd-сервис, используйте стандартные инструменты Linux: systemctl status kubelet — работает ли процесс? journalctl -u kubelet -f — что он пишет в логи? (Здесь вы увидите, если он не может достучаться до apiserver).

  2. Проверьте CRI (containerd): Если kubelet жив, но логи пестрят ошибками Failed to create pod sandbox, проблема в среде выполнения. systemctl status containerd — работает ли демон? crictl pull nginx — может ли узел вообще скачивать образы, или проблема с сетью/DNS на самом сервере?

  3. Проверьте сетевые правила: Если Pod'ы работают, но до них не доходит трафик через Service, проверьте kube-proxy (он обычно запущен как Pod в пространстве имен kube-system) и сами правила ядра: iptables-save | grep <имя-вашего-сервиса> — создал ли kube-proxy правила?

Мы разобрали, как Control Plane хранит состояние, и как Worker Node воплощает его в реальность. Но как именно эти компоненты взаимодействуют в динамике? В следующей главе мы проследим полный жизненный цикл запроса: от момента, когда вы нажимаете Enter, вводя команду kubectl apply, до секунды, когда процесс запускается внутри контейнера.

Жизненный цикл запроса: путь объекта от YAML-файла до запуска в среде выполнения

Жизненный цикл запроса: путь объекта от YAML-файла до запуска в среде выполнения

Вы набираете в терминале kubectl apply -f pod.yaml, нажимаете Enter, и через секунду терминал радостно сообщает: pod/my-app created. Вы проверяете кластер — и приложение уже работает. Со стороны это выглядит как магия или прямой вызов: словно ваш терминал дотянулся до сервера и запустил там процесс.

В реальности за эту секунду разворачивается сложнейшая хореография. Компоненты Kubernetes, с которыми мы познакомились ранее, никогда не отдают друг другу прямых приказов. Секрет масштабируемости и отказоустойчивости кластера кроется в абсолютно асинхронной модели общения, где каждый компонент делает только свою часть работы и реагирует на изменения состояния.

Давайте замедлим время и проследим точный путь запроса: от текста в файле до работающего Linux-процесса на конкретном узле.

Фаза 1: Валидация и сохранение (Entry Point)

Всё начинается на вашем ноутбуке. Утилита kubectl читает YAML-файл, конвертирует его в JSON-формат и формирует HTTP POST-запрос к kube-apiserver.

Когда запрос достигает Control Plane, kube-apiserver прогоняет его через свой строгий конвейер:

  1. Проверяет, кто вы (Authentication).
  2. Проверяет, есть ли у вас права на создание Pod в этом пространстве имен (Authorization).
  3. Пропускает через мутирующие и валидирующие вебхуки (Admission Control).
  4. Если всё успешно, kube-apiserver сохраняет JSON-объект в базу данных etcd.

Именно в этот момент — как только etcd подтвердил запись на диск — kube-apiserver возвращает вашему терминалу HTTP-ответ 201 Created. Терминал пишет pod/my-app created и возвращает управление.

Парадокс: Когда вы видите сообщение об успешном создании, контейнер еще не существует. Ни один образ не скачан, ни один узел не знает о вашем приложении. На этом этапе Pod — это просто запись в базе данных с пометкой «пользователь хочет, чтобы это работало».

Фаза 2: Реакция через Watch API

Если компоненты не общаются напрямую, как остальные части кластера узнают, что в базе появилась новая запись?

В классических системах компоненты постоянно опрашивают базу данных: «Появилось что-то новое? А сейчас?». Это называется Polling. В кластере на тысячи узлов такой подход мгновенно уничтожил бы сеть и процессор Control Plane.

Kubernetes использует механизм Watch API. Компоненты открывают постоянное HTTP/2 соединение с kube-apiserver и говорят: «Сообщи мне, если произойдут изменения с объектами типа Pod». Как только etcd сохраняет новый объект, он уведомляет apiserver, а тот моментально рассылает события (Events) всем подписанным компонентам.

Фаза 3: Поиск дома (Scheduling)

Среди компонентов, слушающих Watch API, есть kube-scheduler. Его интересует очень специфическое событие: появление Pod, у которого поле nodeName пустое (то есть Pod еще не привязан к узлу).

Получив уведомление о нашем новом Pod, планировщик просыпается:

  1. Выполняет фильтрацию (отбрасывает узлы, где не хватает памяти или CPU).
  2. Выполняет оценку (ранжирует оставшиеся узлы, чтобы найти оптимальный).
  3. Выбирает победителя, например, worker-node-2.

Здесь происходит контринтуитивный момент. Планировщик не связывается с worker-node-2. У него вообще нет прав отдавать команды рабочим узлам. Вместо этого планировщик формирует новый HTTP-запрос к kube-apiserver: «Обнови этот Pod в базе данных, запиши в поле nodeName значение worker-node-2». Это действие называется Binding (привязка).

kube-apiserver обновляет запись в etcd. Фаза планирования завершена.

Фаза 4: Исполнение (Execution)

На каждом рабочем узле работает kubelet. Он тоже подписан на Watch API, но с фильтром: «Сообщай мне только о тех Pod, у которых в поле nodeName указано мое имя».

Как только apiserver сохраняет результат работы планировщика, kubelet на worker-node-2 получает уведомление. Он видит: в желаемом состоянии кластера появился Pod, привязанный ко мне, но по факту на моем сервере такого контейнера нет. Включается локальный Reconciliation Loop:

  1. kubelet обращается к Container Runtime (например, containerd) через интерфейс CRI с командой создать PodSandbox (базовую изолированную среду для Pod).
  2. Container Runtime обращается к сетевому плагину (CNI) с задачей: «Выдели IP-адрес для этой песочницы и настрой сеть».
  3. kubelet дает команду через CRI скачать образ приложения из Registry.
  4. kubelet командует через CRI создать и запустить контейнер с приложением внутри уже готовой песочницы.
  5. Как только процесс запущен, kubelet отправляет статус обратно в kube-apiserver: «Pod перешел в состояние Running».

Только теперь фактическое состояние системы совпало с желаемым, которое вы описали в YAML-файле.

Как увидеть этот процесс вживую

Поскольку процесс разбит на независимые асинхронные шаги, любая ошибка локализуется на конкретном этапе. Kubernetes сохраняет следы работы каждого компонента в виде объектов Event.

Вы можете увидеть всю хронологию создания с помощью команды:

kubectl get events --sort-by='.metadata.creationTimestamp'

Вывод покажет точную последовательность:

  1. Scheduled (от scheduler) — Успешно назначен на worker-node-2.
  2. Pulling (от kubelet) — Начинаю скачивание образа.
  3. Pulled (от kubelet) — Образ успешно скачан.
  4. Created (от kubelet) — Контейнер создан.
  5. Started (от kubelet) — Процесс внутри контейнера запущен.

Если вы опечатались в имени образа, scheduler успешно выполнит свою работу (ведь он не проверяет образы, он проверяет ресурсы узлов), kubelet получит задачу, попытается скачать образ, потерпит неудачу и запишет событие Failed to pull image. Pod зависнет в статусе ImagePullBackOff.

Понимание этого асинхронного конвейера — главный навык для траблшутинга. Когда Pod не запускается, профессионал не гадает, а смотрит на статус и события, чтобы понять, какой именно компонент остановил эстафету.