Практически каждое успешное бизнес-приложение рано или поздно вступает в фазу, когда требуется горизонтальное масштабирование. Во многих случаях можно просто запустить новый экземпляр и уменьшить среднюю нагрузку. Но бывают и менее тривиальные случаи, когда мы должны обеспечить, чтобы разные ноды знали друг о друге и аккуратно распределяли рабочую нагрузку.
Так удачно получилось, что erlang, который мы выбрали за приятный синтаксис и хайп вокруг, имеет первоклассную поддержку для распределенных систем. В теории это звучит вообще тривиально:
Передача сообщений между процессами на разных узлах, а также между ссылками и мониторами прозрачна […]
На практике все немного сложнее. Распределенный erlang был разработан, когда «контейнер» означал такой большой железный ящик для перевозки, а «докер» был просто синонимом портового грузчика. В IP4 было много незанятых адресов, в сетевых разрывах — были, как правило, виноваты перегрызшие кабель крысы, а среднее время безотказной работы производственной системы измерялось десятилетиями.
Теперь мы все невообразимо самодостаточны, упакованы, и запускаем распределенный erlang в среде, где динамические IP-адреса раздаются по принципу великой случайности, а узлы могут появляться и исчезать по желанию левой пятки планировщика. Чтобы избежать вороха шаблонного кода в каждом проекте, запускающем распределенный erlang, для борьбы с враждебной средой, требуется помощь.
Примечание: я в курсе что есть libcluster
. Он действительно крутой, у него больше тысячи звезд, автор — известен в сообществе, и все такое. Если вам достаточно предлагаемых этим пакетом способов создания и поддержки кластера — я рад за вас. Мне, к сожалению, нужно гораздо больше. Я хочу управлять настройкой в деталях и не быть сторонним зрителем в театре переформирования кластера.
Требования
Что было нужно лично мне, так это библиотека, которая возьмет на себя управление кластером и будет обладать следующими свойствами:
- прозрачная работа как с жестко закодированным списком узлов, так и с динамическим обнаружением через сервисы erlang;
- полнофункциональный колбэк при каждом изменении топологии (узел туда, узел сюда, нестабильность сети, сплиты);
- прозрачный интерфейс для запуска кластера с длинными и короткими именами, как и с
:nonode@nohost
; - поддержка докера из коробки, без необходимости писать инфраструктурный код.
Последнее означает, что после того, как я протестировал приложение локально в :nonode@nohost
, или в искуственно-распределенной среде при помощи test_cluster_task
, я хочу просто запустить docker-compose up --scale my_app=3
и увидеть, как оно выполняет три экземпляра в докере без каких-либо изменений кода. Я также хочу, чтобы зависимые приложения, например mnesia
— когда топология изменяется, за кулисами перестраивали кластер наживую без какого-либо дополнительного пинка со стороны приложения.
Cloister не задумывался как библиотека, способная на все: от поддержки кластера до приготовления кофе. Это не серебряная пуля, стремящаяся охватить все возможные случаи, или быть академически полным решением в том смысле, который теоретики от CS вкладывают в этот термин. Эта библиотека призвана служить очень четкой цели, но выполнять свой не слишком большой объем работ идеально. Эта цель будет заключаться в обеспечении полной прозрачности между локальной средой разработки и распределенной эластичной средой, полной враждебных контейнеров.
Выбранный подход
Cloister предполагается запускать как приложение, хотя опытные пользователи могут работать со сборкой и поддержкой (assembly and maintainance) кластера вручную, непосредственно запустив Cloister.Manager
в дереве супервизоров целевого приложения.
При запуске в качестве приложения, библиотека полагаемся на config
, откуда вычитывает следующие основные значения:
config :cloister,
otp_app: :my_app,
sentry: :"cloister.local", # or ~w|n1@foo n2@bar|a
consensus: 3, # number of nodes to consider
# the cluster is up
listener: MyApp.Listener # listener to be called when
# the ring has changed
Параметры выше означает дословно следующее: Cloister используется для OTP приложения :my_app
, использует erlang service discovery для подключения узлов, трех по меньшей мере, и MyApp.Listener
модуль (имплементирующий @behaviour Cloister.Listener
) настроен получать уведомления об изменениях топологии. Подробное описание полной конфигурации можно найти в документации.
При такой конфигурации, приложение Cloister будет запускаться поэтапно, откладывая процесс старта главного приложения до достижения консенсуса (три узла подключены и соединены, как в приведенном выше примере.) Это предосталяет главному приложению возможность предположить, что когда оно запустилось, кластер уже доступен. При каждом изменении топологии (их будет много, потому что узлы запускаются не полностью синхронно), будет вызван обработчик MyApp.Listener.on_state_change/2
. В большинстве случаев мы выполняем действие, когда получаем сообщение с состоянием %Cloister.Monitor{status: :up}
, что означает: «алё, кластер собран».
В большинстве случаев установка consensus: 3
является оптимальной, потому что даже если мы ожидаем, что подключится больше узлов, обратный вызов будет проходить через status: :rehashing
→ status: :up
на любом новом добавленном или удаленном узле.
При запуске в режиме разработки, достаточно просто выставить consensus: 1
и Cloister радостно проскочит ожидание сборки кластера, увидев :nonode@nohost
, или :node@host
, или :node@host.domain
— в зависимости от того, как был настроен узел (:none | :shortnames | :longnames
).
Управление распределенными приложениями
Распределенные приложения не в вакууме обычно включают распределенные же зависимости, типа mnesia
. Нам легко обрабатывать их переконфигурацию из того же обратного вызова on_state_change/2
. Вот, например, подробное описание того, как перенастроить mnesia
на лету в документации Cloister.
Главным преимуществом использования Cloister является то, что он выполняет все необходимые операции по перестройке кластера после изменения топологии под капотом. Приложение просто запускается в уже подготовленной распределенной среде, со всеми подключенными узлами, независимо от того, знаем ли мы IP-адреса и, следовательно, имена узлов заранее, или они были динамически назначены/изменены. Это требует ровно никаких специальных настроек конфигурации докера и с точки зрения разработчика приложений, нет никакой разницы между запуском в распределенной среде или в локальной на :nonode@nohost
. Подробнее про это можно почитать в документации.
Несмотря на то, что сложная обработка изменений топологии возможна через собственную реализацию MyApp.Listener
, всегда могут быть пограничные случаи, когда эти ограничения библиотеки и предвзятый подход к конфигурации окажутся краеугольным камнем на пути внедрения. Это нормально, просто возьмите вышеупомянутый libcluster
, который является более универсальным, или даже обработайте кластер низкого уровня самостоятельно. Цель этой библиотеки кода состоит не в том, чтобы охватить все возможные сценарии, а в том, чтобы использовать наиболее распространенный сценарий без ненужной боли и громоздких копи-пастов.
Примечание: на этом месте в оригинале была фраза «Happy clustering!», и Яндекс, которым я перевожу (не самому же по словарям лазить), предложил мне вариант «Счастливого скопления!». Лучшего перевода, пожалуй, особенно в свете текущей геополитической ситуации — и представить невозможно.