Перейти к основному содержимому

Service Discovery при помощи координационных нод YDB

· 7 мин. чтения

Когда приложению нужен service discovery, первое привычное решение — взять отдельный инструмент: Consul, ZooKeeper или аналогичный сервис. Но иногда координационный механизм уже есть в стеке, и тогда отдельный сервис оказывается лишней инфраструктурной сущностью.

Ниже разберём такой сценарий на примере YDB Coordination Node. Задача не в том, чтобы заменить любой service discovery на YDB, а в том, чтобы показать конкретный класс задач, где координационная нода может работать как простой реестр живых инстансов и их конфигурации.

Представим мультитенантное приложение, в котором пользователи могут регистрировать адреса вебхуков. Основная бизнес-логика живёт в группе машин App: она принимает события, определяет нужного пользователя, выбирает зарегистрированный webhook endpoint и отправляет туда HTTP-запрос.

Для большинства пользователей исходящий трафик может идти через общий пул инфраструктуры. Но премиальные пользователи попросили отдельную возможность: чтобы все запросы к их webhook-адресам уходили только с заранее известных выделенных IP-адресов. Тогда они смогут добавить эти IP в свои firewall allowlists и не открывать доступ для всего интернета.

Наивное решение — поднимать отдельную виртуальную машину под каждый выделенный IP или под каждого премиального клиента. На небольших объёмах это работает, но быстро становится экономически невыгодным: ресурсы простаивают, количество машин растёт вместе с числом клиентов, а операционная сложность начинает жить своей жизнью.

Поэтому вводится отдельный тип машин, который будем называть Border. Это пограничные ВМ, через которые уходит внешний трафик к webhook-получателям. На одной такой машине может быть настроено несколько выделенных IP-адресов, а разные пользователи могут быть привязаны к разным адресам или наборам адресов. Таким образом, Border выступает как общий, но управляемый слой egress-инфраструктуры: он позволяет переиспользовать вычислительные ресурсы, сохраняя для клиентов предсказуемые исходящие IP.

Остаётся ключевая проблема: App не может просто один раз получить список Border-машин и считать его постоянным. Количество Border может динамически меняться: группа может масштабироваться, отдельные машины могут перезапускаться во время релиза, временно выпадать из работы или заменяться новыми инстансами.

Итого слой App должен в каждый момент времени знать не просто список Border, а актуальную карту: какой выделенный IP жив, на каком Border он доступен и можно ли через него сейчас отправлять трафик. В дальнейшем мы будем рассматривать именно этот сценарий: как организовать взаимодействие между App и Border так, чтобы маршрутизация webhook-запросов оставалась корректной, устойчивой к изменениям инфраструктуры и при этом не требовала неэффективной схемы «одна ВМ — один IP».

Общая идея решения

В качестве механизма service discovery и распространения конфигурации используется координационный слой на базе YDB Coordination Node. Вместо отдельного сервиса (например, Consul или ZooKeeper) мы используем уже доступный компонент YDB как «живую» распределённую шину конфигурации.

В этой модели Border-инстансы публикуют своё состояние: набор IP-адресов и их статусы. App-инстансы регулярно считывают это состояние и строят локальное представление для маршрутизации запросов.

Ключевая идея — хранить состояние Border не в виде статической конфигурации, а как набор эфемерных владельцев (owners) семафора. Важно, что семафор здесь используется не для взаимного исключения в классическом смысле, а как реестр живых участников, привязанный к сессиям.

Каждый Border занимает слот в семафоре и прикладывает к нему JSON payload со своей конфигурацией. Если инстанс умирает, его сессия истекает, слот автоматически освобождается, и система сама приходит в консистентное состояние без дополнительной логики очистки.

Архитектура и роли

Система состоит из трёх логических компонентов: координационного узла, Border-инстансов и App-инстансов.

Каждый Border при старте:

  • устанавливает сессию с coordination node,
  • захватывает слот в семафоре borders-registry,
  • публикует payload вида {border_id, ips[]}.

App-инстансы не подписываются на изменения напрямую, а используют polling (раз в ~500 мс), вызывая DescribeSemaphore. На основе списка владельцев (owners) они собирают актуальную карту:

IP → [border_id, status]

Этот словарь хранится локально в памяти и обновляется атомарно (через pointer swap), что позволяет избежать гонок и блокировок при обработке запросов.

Поток распространения изменений

Когда оператор меняет статус IP на Border, изменение проходит через coordination node и становится доступным всем App-инстансам с минимальной задержкой.

Важно, что Border не «патчит» состояние, а полностью перепубликует его через re-acquire. Это делает модель проще:

  • нет частичных обновлений,
  • нет рассинхронизации,
  • всегда есть единый источник истины — payload владельца слота.

Задержка распространения определяется интервалом polling’а App и обычно составляет до 1–2 секунд. Это не модель реального времени, зато она простая, предсказуемая и не требует держать постоянный канал подписки между App и coordination node.

Маршрутизация запросов

При обработке запроса App использует локальный snapshot (view), не обращаясь к coordination node в runtime. Это критично для latency и устойчивости.

Алгоритм выбора намеренно простой (например, лексикографически минимальный border_id), но может быть расширен:

  • round-robin,
  • consistent hashing,
  • weighted routing.

Главное — решение принимается полностью локально, без сетевых вызовов.

Обработка отказов (ключевое свойство)

Одно из главных преимуществ использования coordination node — автоматическое удаление «мертвых» Border.

Если Border падает:

  • его сессия истекает (обычно ≤ 5–10 секунд),
  • слот в семафоре освобождается,
  • App перестаёт видеть этот Border при следующем poll.

Это даёт важное свойство: система самовосстанавливается без явного deregistration. Нет необходимости реализовывать heartbeat, TTL-таблицы или cleanup-задачи — всё это уже встроено в механизм coordination.

Ограничения

У такого подхода есть границы применимости. Он хорошо подходит для небольшого и среднего числа Border-инстансов, когда payload остаётся компактным, а задержка обновления в одну-две секунды допустима для продукта.

Если нужны мгновенные реакции на изменения, сложные политики выбора маршрута или очень большое количество участников, придётся внимательнее смотреть на стоимость опроса, размер payload и лимиты семафора. Интервал опроса и таймаут сессии тоже становятся частью продуктового поведения: некоторое время App может работать со слегка устаревшим представлением о мире.

Это не делает решение плохим. Просто это не универсальная замена специализированному discovery-сервису, а практичный вариант для случая, когда в системе уже есть YDB и нужна простая координация живых инстансов с небольшим объёмом конфигурации.

Вывод

Таким образом, YDB Coordination Node используется как простой и надёжный механизм service discovery и распространения конфигурации. Border-инстансы публикуют своё состояние через семафор, а App-инстансы периодически считывают его и строят локальную модель маршрутизации.

В исходном сценарии это даёт ровно то, что нужно: App видит только живые Border, клиенты получают предсказуемые исходящие IP, а в инфраструктуре не появляется отдельный Consul или ZooKeeper только ради этой задачи. При этом логика остаётся достаточно простой: вся схема укладывается в несколько операций — acquire, describe и автоматическое управление жизненным циклом через сессии.

Полный код примера на Go доступен в репозитории.