Полтора года планировщик 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». Он не даёт идеального размещения, но отклонение от оптимума логарифмическое, а стоимость выбора — константная.

Результат

МетрикаБылоСтало
Выбор узла, p5038 мс2.9 мс
Выбор узла, p99212 мс11 мс
Холодный старт Go, p50390 мс147 мс
Переезды при старте4.1%0.6%

Чего это стоило

Агрегатор — новая точка отказа. Мы держим по три реплики на регион и переключаемся на старый опрос, если снимок устарел больше чем на две секунды. За четыре месяца fallback срабатывал дважды, оба раза при обновлении самого агрегатора.

Второй компромисс — память. Снимок на 900 узлов занимает около 40 МБ на реплику. Мы посчитали это приемлемым: альтернативой был бы кеш с инвалидацией, а это тот же опрос, только с более сложными багами.