Kubernetes, который наконец понятен: как устроена оркестрация под капотом LLM-инфраструктуры

Kubernetes, который наконец понятен: как устроена оркестрация под капотом LLM-инфраструктуры

Индустрия годами делала вид, что дизайн кластера Kubernetes это высшая математика. Компонентов много, все связаны друг с другом, схемы выглядят как карта метро большого города. Но если убрать весь шум, в основе лежит одна простая идея, и когда ты её ухватишь, всё остальное встанет на места само.

Забудь про Kubernetes на минуту. Представь, что ты управляешь огромным складом Amazon. Тысячи заказов в день, а на полу работают роботы: берут товары, пакуют коробки, отправляют посылки. Ты же не стоишь посреди склада и не командуешь каждым роботом вручную. Это было бы безумием при таких объёмах. Вместо этого ты просто говоришь системе: мне нужно, чтобы на полу постоянно работали пять упаковочных роботов. И всё. Дальше склад разбирается сам.

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

Kubernetes, который наконец понятен: как устроена оркестрация под капотом LLM-инфраструктуры

Что такое желаемое состояние

Это самое важное понятие во всём Kubernetes. Понял его, считай, понял всё. Вспомни круиз-контроль в машине. Ты выставил 110 км/ч, и это твоё желаемое состояние. Ты не жмёшь газ и тормоз каждые пять секунд. Машина делает это за тебя. Пошёл в горку и скорость упала до 105, система добавляет газу. Покатился под уклон и разогнался до 120, она притормаживает. Ты один раз задал цель, а система дерётся за то, чтобы её держать.

Kubernetes работает точно так же. Ты не пишешь скрипты, которые вручную запускают контейнеры, следят за их здоровьем и перезапускают после падения. Ты пишешь конфиг с желаемым состоянием: хочу веб-приложение в пяти репликах. Отдаёшь этот файл Kubernetes, и с этого момента он становится тем самым круиз-контролем. Упал контейнер, поднимется новый. Умер сервер, приложение переедет на живой. Ты задал цель один раз, дальше кластер сам её удерживает.

Разбираем компоненты склада по ролям

kube-apiserver это стойка регистрации на входе. Единственная точка входа во весь склад. Никто ни с кем не общается напрямую: ни менеджер зала, ни планировщик, ни супервайзеры зон. Каждая команда, каждый статус, каждый вопрос сначала идёт через ресепшен. Хочешь развернуть пять роботов, идёшь на ресепшен. Планировщик хочет отдать робота в зону номер два, тоже идёт на ресепшен. При этом apiserver не глотает запросы вслепую, а прогоняет их через три проверки: кто ты и вообще имеешь ли право тут находиться, что именно тебе разрешено делать, и есть ли политики, которые говорят такому запросу нет. Если apiserver ляжет, склад слепнет целиком.

etcd это база данных склада. Единый источник правды для всего кластера. IP каждого робота, привязка к зонам, каждая настройка, каждое желаемое состояние, всё лежит здесь. Если чего-то нет в etcd, склад про это просто не знает. Важный момент: напрямую с etcd разговаривает только apiserver, остальные компоненты к ней не прикасаются. Если etcd умрёт, а бэкапа нет, ты теряешь всё состояние кластера. Поэтому в проде etcd всегда держат кластером из нескольких копий, чтобы падение одной ноды не стоило тебе данных.

kube-controller-manager это менеджер зала. Тот, кто реально заставляет вещи происходить. Он не пакует коробки и не раздаёт зоны, его работа сидеть и через ресепшен смотреть в базу, задавая один и тот же вопрос по кругу: фактическое состояние совпадает с желаемым? Ты сказал, что хочешь пять роботов, а работают четыре? Менеджер это ловит и просит ресепшен создать ещё одного. При этом внутри одного процесса крутится целая пачка контроллеров. Deployment-контроллер следит за деплойментами и создаёт ReplicaSet. ReplicaSet-контроллер держит нужное число подов: мало, создаёт ещё, много, гасит лишние. Node-контроллер отслеживает, какие зоны онлайн, и переносит роботов из отвалившейся зоны. Job-контроллер разруливает разовые задачи по принципу упакуй один заказ и остановись. У каждого свой цикл: наблюдай, сравнивай, действуй.

kube-scheduler это распределитель зон. Когда менеджер зала создал нового робота, тот ещё не знает, в какую зону идти, и висит в состоянии Pending, мол, я существую, но дома у меня нет. Планировщик через ресепшен ловит таких бездомных и подбирает им лучшую зону. Выбирает не наугад: сначала отсеивает зоны, которые физически не потянут робота по ресурсам, потом оставшиеся оценивает по баллам, где больше свободных ресурсов, где лучше ляжет нагрузка, где уже есть нужные зависимости, и привязывает робота к зоне-победителю. Дальше супервайзер этой зоны подхватывает под и запускает его.

kubelet это супервайзер зоны. У каждой зоны склада, то есть у каждой рабочей ноды, есть свой kubelet. Он не сидит в офисе управления, а работает прямо в цеху. Его задача простая: следить у ресепшена за подами, назначенными в его зону, запускать роботов через среду выполнения контейнеров, держать их живыми через проверки здоровья, перезапускать при падении и отчитываться на ресепшен по статусу. Именно kubelet отвечает за liveness и readiness пробы. Если робот говорит, что живой, но по факту не отвечает на работу, kubelet это ловит и перезапускает его.

kube-proxy это маршрутизатор конвейерной ленты. Именно он заставляет работать Сервисы. Помни про стойку приёма заказов с постоянным IP, которая раздаёт заказы нужным роботам? kube-proxy на каждой зоне поднимает правила маршрутизации: если пришёл запрос на IP сервиса, перенаправь его на один из живых IP подов. Когда робот умирает и вместо него поднимается новый с другим IP, kube-proxy автоматически обновляет таблицы маршрутизации, а клиент этого даже не замечает. Он бьёт в тот же самый IP стойки, а трафик спокойно течёт к здоровым роботам. По сути это балансировщик, который сидит на каждой ноде.

Container Runtime это активатор роботов. Когда kubelet говорит запусти этого робота, он не делает это сам, а зовёт среду выполнения контейнеров, которая скачивает образ и поднимает контейнер. kubelet говорит активируй робота с таким-то софтом, а рантайм качает код, разворачивает изолированное окружение и стартует его. Раньше по умолчанию был Docker, но Kubernetes от него ушёл. Рантайму достаточно уметь говорить на языке CRI, и kubelet будет с ним работать.

Pod это сам робот. Наименьшая единица развёртывания в Kubernetes и тот самый работяга, который делает реальную работу: пакует коробки, обрабатывает запросы, крутит твой код. По сути под это обёртка вокруг одного или нескольких контейнеров. Чаще всего внутри один контейнер, один робот на одну задачу, но иногда рядом с основным приложением живут сайдкары, например агент логирования или прокси. Главное, что поды временные. Они не должны жить вечно. Упал под, Kubernetes его не чинит, а убивает и поднимает новый. Поэтому ты никогда не завязываешься на IP конкретного пода напрямую, а всегда ходишь через Сервис.

Как всё это складывается в один поток

Теперь соберём все компоненты вместе. Ты говоришь складу: мне нужно пять упаковочных роботов. Конфиг и данные роботов сохраняются в базу etcd через apiserver. Controller-manager замечает изменения и запускает создание подов. Scheduler подхватывает эти поды и раскидывает их по зонам, где есть ресурсы. kubelet на нужной зоне поднимает робота через рантайм. Готово. Но тут важно не путать две вещи: управление кластером, то есть Control Plane, это не то же самое, что маршрутизация трафика приложения, то есть Data Plane. Вся связка apiserver, etcd, scheduler и контроллеры отвечает за состояние кластера и создание подов. Обычные пользовательские запросы к приложению через etcd, scheduler и apiserver не ходят. Control Plane лишь отдаёт конфигурацию маршрутизации таким компонентам, как kube-proxy и Ingress-контроллеры, а реальный трафик течёт уже через Сервис, Ingress, Gateway или балансировщик к нужному поду.

Как вообще общаться с роботами

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

Метки, селекторы и EndpointSlices

Раз при каждом перезапуске робот получает новый IP, как стойка с постоянным адресом справляется с тысячами роботов, которые непрерывно стартуют, стопаются и падают? Работают три вещи вместе. Метки и селекторы это бейджи: каждый робот носит бейдж вроде app: packing-robot, а стойка настроена на селектор, который говорит отправляй заказы только роботам с таким бейджем. Endpoints это главный свиток, список реальных IP подходящих и здоровых роботов, который обновляется каждый раз, когда робот поднимается или гаснет. EndpointSlices это масштабируемые листы: вместо одного гигантского свитка на все десять тысяч адресов список режут на маленькие листки примерно по сто IP.

Смысл вот в чём. В огромном складе на десять тысяч роботов, если бы при каждом падении одного робота приходилось переписывать свиток на десять тысяч строк и рассылать копию каждому супервайзеру зоны, копировальные машины сгорели бы от нагрузки. Именно так и вёл себя старый механизм Endpoints: при тысячах подов одно изменение состояния требовало разослать весь громадный список на kube-proxy каждой ноды. EndpointSlices решают это тем, что режут список на маленькие листки. Упал один робот, менеджер обновляет только тот самый листок на сто строк и рассылает его, всё остальное остаётся нетронутым.

Как внешний трафик реально доходит до подов в проде

IP сервиса, то есть ClusterIP, виртуальный и живёт исключительно внутри сети кластера. Если пользователь из интернета попробует достучаться до внутреннего адреса, запрос уйдёт в никуда, потому что у публичных роутеров нет маршрута до приватных диапазонов кластера. Чтобы впустить внешний трафик, в проде ставят облачный балансировщик в связке с Ingress-контроллером, например NGINX, Envoy или Traefik. Ingress-контроллер для входящего внешнего трафика обходит kube-proxy стороной и маршрутизирует запросы сразу на IP целевых подов, чтобы убрать двойную маршрутизацию и лишнюю задержку.

И тут возникает вопрос: если мы обходим kube-proxy, зачем вообще нужны EndpointSlices? Затем, что Ingress-контроллер читает их напрямую. EndpointSlices это развязанный и масштабируемый реестр активных точек. Ingress-контроллеры смотрят в них, чтобы держать актуальным пул бэкенд-адресов. kube-proxy смотрит в них, чтобы генерить правила iptables, IPVS или eBPF для внутреннего трафика ClusterIP. Сервис-меши вроде Istio или Linkerd смотрят в них, чтобы настраивать таблицы сайдкар-прокси. CoreDNS смотрит в них, чтобы резолвить доменные имена в живые IP подов. Так что даже когда kube-proxy обходят современные Ingress-контроллеры или сервис-меши, EndpointSlices остаются единым источником правды о расположении и здоровье подов. Без них apiserver расплавился бы под весом обновлений таблиц маршрутизации в больших кластерах.

Почему это важно и при чём тут LLM

Маленький кластер вести легко. Но с ростом нагрузки кластеры превращаются в хаос. Крупные платформы уровня YouTube или Instagram ловят миллионы запросов в секунду, и без крепкой архитектуры кластера такой объём просто не переварить. Представь стриминг 4K миллионам зрителей одновременно, а под капотом всё это тянет именно оркестратор. Сейчас самая горячая тема индустрии это инфраструктура для LLM, и чтобы запускать продакшн-инференс больших моделей, нужен твёрдый фундамент как раз из этих оркестраторов. Автор оригинального разбора признаётся, что полез в кубер настолько глубоко именно потому, что копал масштабирование LLM-инференса вместе с командой llm-d. Так что если ты работаешь с ИИ-инфраструктурой, эта механика склада с роботами теперь твой рабочий инструмент, а не абстрактная схема на собеседовании.

Источник и оригинальный разбор с диаграммами: https://x.com/jaga_prasanna/status/2079614504451838426