Как мы переписали control plane Kubernetes — и стали поднимать кластер за 90 секунд
Раньше у нас уходило 6–8 минут от «нажал кнопку» до working API. Это разбор того, что мы переписали в провижене, etcd-snapshotting и образах нод — и какие тупики прошли по дороге.
Шесть с половиной минут. Столько в среднем уходило у нас весной 2025-го от «пользователь нажал создать кластер» до момента, когда apiserver отвечает 200 на /healthz. Это нормальная цифра по индустрии. И именно поэтому она нас не устраивала.
В этом посте я расскажу, что мы переделали в control plane h3llo cloud, какие компромиссы выбрали и где ошиблись — потому что ошибок, как обычно, было больше, чем я бы хотел писать в публичный блог. Если коротко: сейчас медиана подъёма production-grade кластера — 1 минута 30 секунд, а 95-й перцентиль — около двух минут. Дальше — как именно.
Что нас не устраивало
Долгий подъём кластера — это не «неудобно для пользователя». Это конкретная боль с реальными последствиями. Когда первый кластер у нового клиента поднимается восемь минут — половина людей просто закрывают вкладку до того, как kubeconfig окажется у них в руках. Когда фермы CI поднимают кластер на каждом PR-е, каждые сэкономленные четыре минуты — это десятки человеко-часов в неделю.
Мы посмотрели на p95 и поняли, что у нас вообще-то не один пик долгого провижена, а два. И каждый — со своими причинами.
POST /clusters до первого 200 на /healthz у любого apiserver-инстанса. Регион AMS, m4.large ноды, 3 узла control plane.Метрики «до»
На пике лета мы сняли распределение времени подъёма по компонентам — и оно дало неожиданную картинку:
Главный сюрприз — VM-bootstrap. Мы думали, что узким местом будет etcd, и долго копали туда. На деле оказалось, что почти 40% времени уходило на загрузку и распаковку базового образа node-агента.
«Ваше узкое место — не там, где вы его ищете. Оно там, где вы его не измеряли.»
Подход и решения
Мы выбрали не идти за one-big-rewrite. Вместо этого: 12 недель, 9 итераций, каждая выкатывается за дверь и измеряется. Если итерация не даёт измеримого выигрыша — откатываем, разбираемся, идём дальше. Это дольше переписать на бумаге, но сильно безопаснее в продакшене с живыми клиентами.
Новая архитектура control plane
Главное изменение — мы убрали последовательность «сначала etcd, потом apiserver, потом ноды». Теперь это три параллельных трека, и control plane готов отвечать на запросы как только etcd собрал кворум, не дожидаясь join-а нод.
В коде это вылилось в довольно простой паттерн с errgroup — но детали распаралеливания оказались чувствительные:
// control-plane provisioner — упрощённый main loop func Provision(ctx context.Context, spec *ControlPlaneSpec) error { // 1. Параллельно поднимаем nodes и control-plane g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return bootstrapEtcd(ctx, spec) }) g.Go(func() error { return provisionNodes(ctx, spec) }) if err := g.Wait(); err != nil { return fmt.Errorf("bootstrap: %w", err) } // 2. apiserver уже знает про ноды через cache-watch return waitReady(ctx, spec, 90*time.Second) }
etcd-snapshotting и почему мы перестали его бояться
Главное психологическое препятствие — частые снапшоты etcd. У нас стоял 5m по дефолту, как почти у всех. Снапшот раз в 30 секунд интуитивно кажется «нагрузкой», от которой вырастет latency на write-операциях.
На практике, как только мы переехали на локальный NVMe и compaction в continuous-режиме, snapshot перестал быть видимым событием в метриках. Распределение latency не сдвинулось ни на p50, ни на p99.
# control-plane.yaml — упрощённая версия apiVersion: v1 kind: ControlPlane metadata: name: k8s-prod-eu spec: version: "1.31" replicas: 3 etcd: snapshotInterval: "30s" # было 5m compaction: "continuous" storage: nvme-local apiServer: cache: aggressive watchCacheSize: 10000
Что получилось
Если коротко — в три раза быстрее по медиане и в пять раз быстрее по p99. Подробнее:
| Этап | Было (медиана) | Стало (медиана) | Δ | Что изменили |
|---|---|---|---|---|
| VM-bootstrap | 2m 25s | 28s | −81% | Заранее прогретые образы, multi-arch манифест |
| etcd join | 1m 28s | 12s | −86% | NVMe + параллельный bootstrap |
| apiserver init | 1m 12s | 14s | −81% | Watch-cache prewarm, lazy reconcile |
| kubelet ready | 1m 08s | 28s | −59% | Параллельный pull, kubelet-config из metadata |
| first scheduling | 17s | 8s | −53% | Scheduler greedy-mode для bootstrap |
| Итого | 6m 30s | 1m 30s | −77% | — |
Цифры по неделям
Не всё стало хорошо сразу. Вот как менялась медиана по неделям, начиная с майского старта работ:
Видно, что в августе у нас было откатное событие: мы попробовали поменять snapshotter, нашли regression в чтении при compaction-е, выкатили обратно. Потеряли неделю — но избежали инцидента у клиентов.
Терминология, на всякий случай
watch-запросы вместо прямого чтения из etcd. Прогретый watch-cache даёт огромный выигрыш на старте.О чём мы жалеем
Главное — что не начали с метрик. Мы потратили почти три недели в начале на «оптимизации, которые казались очевидными»: подкручивали kernel-параметры, играли с pre-warmed pool. Если бы первой неделей мы прошлись по чистому профилированию, узкое место с базовым образом нашли бы сразу.
Вторая ошибка — мы изначально не сделали feature-flag для нового provisioner-а. Это сильно ограничило нас в первые месяцы: любой roll-out был «всё или ничего». Сейчас feature-flag есть, и мы можем катить по 5% трафика на новые версии controller manager-а. Жить стало сильно спокойнее.
Хронология проекта
Что дальше
Мы не считаем 1m 30s конечной точкой. Цель к январю — медиана 60 секунд для production-grade кластера и 30 секунд для dev-grade (без HA). Чтобы туда дойти, надо:
- Перевести кеш apiserver на shared mode между репликами — сейчас три инстанса греют кеш каждый отдельно.
- Сделать
StaticPlacementдля bootstrap-под — мы знаем, какие поды нужны на старте кластера, и можем их подготовить заранее. - Переписать health-checks так, чтобы готовность сообщалась не
healthy/not-healthy, а с честным процентом готовых компонентов — это снимет паузы в waitReady.
Если у вас есть свой опыт быстрого подъёма control plane — engineering@h3llo.cloud, нам интересно. Мы пишем технический подкаст «Облако наизнанку» — приходите гостем.
Скриншоты dashboard-а
Что говорят о результатах
Поднял тестовый k8s на h3llo за минуту с копейками. У AWS EKS на тех же параметрах — 11 минут. Не понимаю, как они так живут.
- 1. Все измерения сделаны на m4.large нодах, регион AMS. На GPU-нодах цифры другие — про них напишем отдельно.
- 2. Под «production-grade» здесь — кластер с HA control plane (3 узла), сетевыми политиками по умолчанию и подключённым мониторингом.
- 3. Бенчмарк AWS EKS из tweet-а Кости — публичные цифры самого Amazon, см. официальные SLA.
Комментарии · 38
Отличный разбор! Один вопрос: а как у вас устроены integration-тесты? Вы же не гоняете «поднять кластер за 90 секунд» на каждом PR — или гоняете?
Гоняем — но не на каждом PR, а раз в 2 часа на отдельной ферме. На PR-ах прогоняем lite-вариант (single-node без HA), он поднимается за 25 секунд. Раз в день делаем full e2e на 3 регионах. Если хочется — могу написать отдельный пост про CI у нас.
Continuous compaction в etcd — звучит дорого. У вас не было проблем с CPU на etcd-нодах?