Полтора года планировщик Vlone работал по простой схеме: на каждый запрос о запуске инстанса он опрашивал все узлы региона, собирал их свободные ресурсы и выбирал лучший. Схема честная, понятная и линейно деградирующая.
Где она сломалась
К весне 2026 года в крупнейшем регионе стало больше 900 узлов. Опрос занимал 35–45 мс в медиане и до 210 мс в хвосте. Для деплоя это незаметно, для холодного старта — половина бюджета задержки.
Мы платили за точность выбора временем, которого у холодного старта нет.
Хуже, что точность оказалась иллюзорной. Между опросом и запуском проходило время, за которое состояние узла успевало измениться. В пиковые часы до 4% размещений попадали на узел, который к моменту старта уже был занят, и инстанс переезжал — ещё плюс 300 мс.
Что мы сделали
Заменили опрос на подписку: узлы сами шлют дельты своего состояния в региональный агрегатор раз в 250 мс или при значимом изменении. Планировщик держит актуальный снимок в памяти и выбирает из него.
// Выбор узла: два случайных кандидата, побеждает менее загруженный.
// Power of two choices даёт почти оптимальное распределение
// без глобальной синхронизации.
func (s *Scheduler) pick(req Request) (*Node, error) {
candidates := s.snapshot.Eligible(req.Region, req.Size)
if len(candidates) == 0 {
return nil, ErrNoCapacity
}
a := candidates[rand.IntN(len(candidates))]
b := candidates[rand.IntN(len(candidates))]
if a.Load() <= b.Load() {
return a, nil
}
return b, nil
}Приём известен как «power of two choices». Он не даёт идеального размещения, но отклонение от оптимума логарифмическое, а стоимость выбора — константная.
Результат
| Метрика | Было | Стало |
|---|---|---|
| Выбор узла, p50 | 38 мс | 2.9 мс |
| Выбор узла, p99 | 212 мс | 11 мс |
| Холодный старт Go, p50 | 390 мс | 147 мс |
| Переезды при старте | 4.1% | 0.6% |
Чего это стоило
Агрегатор — новая точка отказа. Мы держим по три реплики на регион и переключаемся на старый опрос, если снимок устарел больше чем на две секунды. За четыре месяца fallback срабатывал дважды, оба раза при обновлении самого агрегатора.
Второй компромисс — память. Снимок на 900 узлов занимает около 40 МБ на реплику. Мы посчитали это приемлемым: альтернативой был бы кеш с инвалидацией, а это тот же опрос, только с более сложными багами.


