Engineering · долгий разбор

Как мы переписали control plane Kubernetes — и стали поднимать кластер за 90 секунд

Раньше у нас уходило 6–8 минут от «нажал кнопку» до working API. Это разбор того, что мы переписали в провижене, etcd-snapshotting и образах нод — и какие тупики прошли по дороге.

ИС
Илья Самойленко
Lead, Platform · h3llo cloud
·12 ноября 2025·18 минут чтения#kubernetes#etcd#postmortem
k8sk8sk8sk8sk8sk8sk8sk8sk8sk8sk8sk8sk8sk8scontrol planeapiserver · scheduler · etcd · controller-mgr90s

Шесть с половиной минут. Столько в среднем уходило у нас весной 2025-го от «пользователь нажал создать кластер» до момента, когда apiserver отвечает 200 на /healthz. Это нормальная цифра по индустрии. И именно поэтому она нас не устраивала.

В этом посте я расскажу, что мы переделали в control plane h3llo cloud, какие компромиссы выбрали и где ошиблись — потому что ошибок, как обычно, было больше, чем я бы хотел писать в публичный блог. Если коротко: сейчас медиана подъёма production-grade кластера — 1 минута 30 секунд, а 95-й перцентиль — около двух минут. Дальше — как именно.

было
6m 30s
медиана подъёма
стало
1m 30s
−77% · быстрее в 4.3×
p95
2m 04s
было 11m 12s

Что нас не устраивало

Долгий подъём кластера — это не «неудобно для пользователя». Это конкретная боль с реальными последствиями. Когда первый кластер у нового клиента поднимается восемь минут — половина людей просто закрывают вкладку до того, как kubeconfig окажется у них в руках. Когда фермы CI поднимают кластер на каждом PR-е, каждые сэкономленные четыре минуты — это десятки человеко-часов в неделю.

Мы посмотрели на p95 и поняли, что у нас вообще-то не один пик долгого провижена, а два. И каждый — со своими причинами.

i
Кратко про методику
Все цифры в этом посте — медианы по 30-дневному окну, измеряем от первого POST /clusters до первого 200 на /healthz у любого apiserver-инстанса. Регион AMS, m4.large ноды, 3 узла control plane.

Метрики «до»

На пике лета мы сняли распределение времени подъёма по компонентам — и оно дало неожиданную картинку:

Из чего состояли 6m 30s
Медианное время по этапу, июль 2025
VM-bootstrap
145s
etcd join
88s
apiserver init
72s
kubelet ready
68s
first scheduling
17s
медианная длительность этапа

Главный сюрприз — VM-bootstrap. Мы думали, что узким местом будет etcd, и долго копали туда. На деле оказалось, что почти 40% времени уходило на загрузку и распаковку базового образа node-агента.

«Ваше узкое место — не там, где вы его ищете. Оно там, где вы его не измеряли.»

старая SRE-мудрость, которую мы каждый раз учим заново

Подход и решения

Мы выбрали не идти за one-big-rewrite. Вместо этого: 12 недель, 9 итераций, каждая выкатывается за дверь и измеряется. Если итерация не даёт измеримого выигрыша — откатываем, разбираемся, идём дальше. Это дольше переписать на бумаге, но сильно безопаснее в продакшене с живыми клиентами.

Новая архитектура control plane

Главное изменение — мы убрали последовательность «сначала etcd, потом apiserver, потом ноды». Теперь это три параллельных трека, и control plane готов отвечать на запросы как только etcd собрал кворум, не дожидаясь join-а нод.

kubectl / APIEdge gatewayавторизация · rate-limitapiserver pool3× active, watch-cacheschedulercontroller-mgrleader-electedetcd clusternvme · snapshot 30s3 voters · 0 learners
Control plane h3llo. Edge gateway держит ddos-защиту и rate-limiting; etcd живёт на локальных NVMe; apiserver-pool — три active-инстанса.

В коде это вылилось в довольно простой паттерн с errgroup — но детали распаралеливания оказались чувствительные:

provisioner/main.goGo
// 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.yamlYAML
# 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
!
Это работает только на локальных NVMe
Мы пробовали тот же конфиг на сетевых дисках — write-amplification от continuous compaction съел всё, что мы выиграли. Snapshot 30s + remote storage = плохая идея.

Что получилось

Если коротко — в три раза быстрее по медиане и в пять раз быстрее по p99. Подробнее:

ЭтапБыло (медиана)Стало (медиана)ΔЧто изменили
VM-bootstrap2m 25s28s−81%Заранее прогретые образы, multi-arch манифест
etcd join1m 28s12s−86%NVMe + параллельный bootstrap
apiserver init1m 12s14s−81%Watch-cache prewarm, lazy reconcile
kubelet ready1m 08s28s−59%Параллельный pull, kubelet-config из metadata
first scheduling17s8s−53%Scheduler greedy-mode для bootstrap
Итого6m 30s1m 30s−77%

Цифры по неделям

Не всё стало хорошо сразу. Вот как менялась медиана по неделям, начиная с майского старта работ:

8m6m4m2m0майиюньиюльавгсенокт1m 30sтекущая медиана

Видно, что в августе у нас было откатное событие: мы попробовали поменять snapshotter, нашли regression в чтении при compaction-е, выкатили обратно. Потеряли неделю — но избежали инцидента у клиентов.

Один из неочевидных побочных эффектов
Поскольку apiserver теперь готов отвечать раньше нод, GitOps-операторы (ArgoCD, Flux) у клиентов начали стартовать reconcile-цикл сразу, а не ждать «полного» кластера. Это сэкономило ещё ~40 секунд в реальных сценариях деплоя приложений.

Терминология, на всякий случай

control plane
Управляющий слой Kubernetes — apiserver, scheduler, controller-manager, etcd. Не путать с node plane (рабочими нодами).
watch-cache
Кэш у apiserver, через который отдаются watch-запросы вместо прямого чтения из etcd. Прогретый watch-cache даёт огромный выигрыш на старте.
snapshot interval
Период, с которым etcd делает консистентный снимок состояния. По умолчанию у k8s — 5 минут; чем чаще — тем быстрее восстановление, тем выше I/O.
compaction
Удаление устаревших ревизий из etcd. В обычном режиме — пакетно по таймеру; в continuous — постоянно фоном по ревизиям.

О чём мы жалеем

Главное — что не начали с метрик. Мы потратили почти три недели в начале на «оптимизации, которые казались очевидными»: подкручивали kernel-параметры, играли с pre-warmed pool. Если бы первой неделей мы прошлись по чистому профилированию, узкое место с базовым образом нашли бы сразу.

Вторая ошибка — мы изначально не сделали feature-flag для нового provisioner-а. Это сильно ограничило нас в первые месяцы: любой roll-out был «всё или ничего». Сейчас feature-flag есть, и мы можем катить по 5% трафика на новые версии controller manager-а. Жить стало сильно спокойнее.

Хронология проекта

12 мая 2025
Старт. Первое профилирование
Получили честные цифры по этапам. Поняли, что узкое место — VM-bootstrap, а не etcd.
3 июня
Pre-warmed образы и multi-arch манифесты
Самый дешёвый и самый эффективный шаг проекта. −60% времени на bootstrap за две недели.
28 июня
NVMe + новый etcd-конфиг
Перешли на локальные NVMe. Включили continuous compaction. Snapshot interval 30s стал нормой.
14 августа
Откат snapshotter-а
Нашли regression на чтении в continuous compaction, откатили. Обновили e2e-тесты.
19 сентября
Параллельный provisioner
Главный архитектурный сдвиг. apiserver и ноды поднимаются одновременно, не последовательно.
25 октября
Медиана 1m 30s закреплена за prod
Финальный rollout. Feature-flag убран. Все новые кластеры теперь на новой схеме.
×
Мы могли сломать прод
Во время отката snapshotter-а в августе у нас был один четырёхминутный read-impact на двух кластерах в Амстердаме. К счастью, мы заметили это до того, как клиенты подняли тикет. Теперь любой rollout etcd-конфигурации идёт через canary 5% → 25% → 100% с автоматическим rollback по latency-budget.

Что дальше

Мы не считаем 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-а

Дашборды h3llo platform: время подъёма, etcd write-latency, apiserver QPS — то, что мы смотрим каждое утро.

Что говорят о результатах

КГ
Костя Г. · CTO Lazyboard
@konst_g

Поднял тестовый k8s на h3llo за минуту с копейками. У AWS EKS на тех же параметрах — 11 минут. Не понимаю, как они так живут.

♥ 412↻ 8714:32 · 26 окт 2025
ИС
Илья Самойленко
Lead, Platform · h3llo cloud
Делает control plane на h3llo. До этого — 6 лет Yandex Cloud, до этого — большой и сложный k8s в e-commerce. Считает, что etcd — главный ингредиент любой облачной кухни.
  1. 1. Все измерения сделаны на m4.large нодах, регион AMS. На GPU-нодах цифры другие — про них напишем отдельно.
  2. 2. Под «production-grade» здесь — кластер с HA control plane (3 узла), сетевыми политиками по умолчанию и подключённым мониторингом.
  3. 3. Бенчмарк AWS EKS из tweet-а Кости — публичные цифры самого Amazon, см. официальные SLA.

Комментарии · 38

МК
Михаил К.12 нояб · 16:42

Отличный разбор! Один вопрос: а как у вас устроены integration-тесты? Вы же не гоняете «поднять кластер за 90 секунд» на каждом PR — или гоняете?

ИС
Илья Самойленко автор12 нояб · 17:08

Гоняем — но не на каждом PR, а раз в 2 часа на отдельной ферме. На PR-ах прогоняем lite-вариант (single-node без HA), он поднимается за 25 секунд. Раз в день делаем full e2e на 3 регионах. Если хочется — могу написать отдельный пост про CI у нас.

АП
Артём Панов12 нояб · 18:15

Continuous compaction в etcd — звучит дорого. У вас не было проблем с CPU на etcd-нодах?

+
показать ещё 35 комментариев →