Общее#

Выбор лидера (Leader Election), это процесс, который позволяет некоторому кол-ву инстансов договориться о том, кто из них имеет приоритет. Данный процесс необходим приложениям для решения некоторых задач, таких как построение индекса на некотором датасете или фоновый процесс синхронизации данных или других задач, работу надо которыми в данный момент времени должен выполнять только один инстанс из всех работающих.

Процесс выбора мастера, равно как и само понятие мастера - интересная, но не самая приятная задача, которую стоит решать. К тому же в некоторых ситуациях ее можно исключить, изменив архитектуру приложения. Для наглядного примера можно представить, что в распределенной системе есть периодическая задача, которую система должна выполнять периодически, при этом только от имени одного из инстансов. Можно было бы использовать механизм выбора лидера, и тот из инстансов, который стал бы лидером - смог запустить периодическую задачу. Однако, если у нас используется Message Queue с подтверждением сообщений (прим. kafka), то можно использовать ее в режиме consumer group и некоторый инстанс, который отправляет в спец топик сигнал о старте этой самой периодической задачи. Если есть возможность избежать процесса выбора мастера - лучше его избежать.

Стоит так же отметить, что механизмов выбора мастера существует достаточно, но сегодня рассмотрим только тот, что позволяет использовать внешнюю систему - Consul от Hashicorp.

Hashicorp и Consul#

Hashicorp - технологическая компания из Калифорнии, которая занимается разработкой ПО вокруг клауда с кол-вом сотрудников более 1к. Сильная компания и отлично зарекомендовавшими себя продуктами (если говорить про open-source). Как компания - Hashicorp зарегистрирована с 2012, но в OSS сообществе стали известны после выпуска Vagrant. Hashicorp имеет несколько OSS продуктов, одним из которых является и Consul.

Consul изначально преподносился как инструмент для решения задач обнаружения сервисов (Service Discovery) через DNS и HTTP интерфейс и KV хранилище преимущественно для конфиг файлов или конфиг параметров.

Сейчас Consul имеет больше возможностей, и направление его развития на данный момент (помимо уже существующих SD и KV) - Service Mesh. Здесь мы рассмотрим только ту часть его возможностей, которая касается KV хранилища.

Определение проблемы#

Для подготовки решения и анализа его эффективности стоит для начала определить проблему, которую мы решаем. Помимо того, что нам необходимо выбрать один из инстансов как мастер - необходимо так же понять, как будет вести себя система при изменении кол-ва инстансов (скейлинг в большую/меньшую сторону), а так же то, как будет вести себя система при отказе текущего мастера. Причем текущий мастер может стать недоступным по нескольким причинам. Как минимум: сетевая недоступность, выключение (kill) процесса без graceful shutdown, а так же нормальная терминация сервиса с полным процессов graceful shutdown.

Особенности инсталляции Consul#

Consul - это распределенная система и она не была бы собой, если бы не была способна работать в условиях горизонтального масштабирования, нагрузки, работы в среде multi-datacenter. Для правильной работы, необходимо создать правильное окружение и использовать инструмент так, как задумавал его автор. Consul, как и все промышленные системы имеет ряд особеностей, которые необходимо учесть перед началом работы.

Consul, для синхронизации внутреннего состояния использует алгоритм достижения консенсуса под названием Raft. Для работы данного алгоритма системе необходимо иметь адреса всех соседних инстансов и иметь сетевой доступ друг до друга по этим адресам. Как работает данный механизм сейчас не важно, но важно то, что для достижения консенсуса в распределенной системе - количество инстансов в системе должно быть как минимум 3. Это обуславливается тем, что в результате работы алгоритма будет выбран мастер (опять leader election, но внутри самого Consul), а для выбора этого мастера - за него должны проголосовать большинство инстансов (нод) кластера. Большинство нод - это 50%+1 нода. Соответственно и минимальное количество нод равно 3, так как 50% от 3 = 1,5; округляем до меньшего целого = 1; прибавляем еще одну ноду для достижения большинства. В общем здесь все понятно. Однако, несмотря на то, что минимальное количество нод должно быть равно трем - в продакшн необходимо тащить минимум 4. Это строго необходимо, так как распределенная система имеет свойство отказывать. В случае отказа одной ноды, общее количество доступных нод будет равно двум, что не позволит корректно провести голосование большинства нод (болльшинства не может быть достигнуто). В этом нет ничего сложного, а процесс запуска отлично описан в документации и на Docker Hub.

Еще одна особенность данной системы - то, что в отличие от (например) Postgres, где существует много инстансов вашего приложения и сервер СУБД, в которому подключаются клиент вашего приложения, в Consul существует Consul Server и Consul Agent. Consul Server - это основные ноды кластера Consul, которые хранят данные и обеспечивают гарантии, предоставляемые системой. Они связываются между собой и синхронизируют состояние через Raft. Consul Agent - это (условный) прокси, который по принципу sidecar должен находиться рядом с каждым инстансом приложения, которое хочет работать с Consul.

Еще раз, но другими словами - каждый инстанс вашего приложения обязан подключаться только к своему агенту, а уже агент будет подключаться к кластеру Consul Server. Несмотря на кажущееся неудобство такой инсталляции - это невероятно облегчает работу с сохранением гарантий системы, так как именно агент берет на себя задачу поиска мастер сервера в кластере Consul, ретраи, кэш DNS и KV а так же healthcheck (одна из фичей Consul).

И отдельный момент про локальную разработку. Для того, что бы создать минимальный сетап, необходимо запустить всего один Consul Agent в режиме разработки.

Consul: KV и Session#

Consul имеет простое KV хранилище, в которое по некоторому ключу можно записать любое значение. Так же имеется расширение этого хранилища, которое позволяет захватывать некоторый ключ для реализации меаханизма рекомендательных блокировок.

Рекомендательные блокировки - тип блокировок, которые не ограничивают другие процессы в доступе на чтение или модификацию заблокированных данных, а только собщают о том, что данный объект сейчас находится во владении некоторого процесса (заблокированы тем процессом).

Расширение KV хранилища, которое позволяет управлять рекомендательными блокировками в Consul называется сессии ( Sessions). Это механизм, который позволяет зарегистрировать сессию (по сути - некоторая строчка в локальном хранилище Consul Agent), и от ее имени блокировать ключи в KV хранилище или использовать специальные объекты блокировок.

Сессия особенно отличается от других сущностей в Consul тем, что имееет параметры, такие как TTL (Time-To-Live) самой сессии, а так же то, что сессия живет только на созданном агенте. Созданная сессия с TTL=10sec будет удалена автоматически по истечении 10 секунд. Так же сессию можно продлить. Запрос на продление сбросит таймер жизни сессии и начнет новый отсчет до момента истечения TTL. То есть, созданная ранее сессия с TTL 10 секунд, при обновлении будет жить еще 10 секунд. Таким образом сессию можно бесконечно продлевать, но быть уверенным в том, что в случае каких-либо проблем она будет уничтожена автоматически.

Consul: Рекомендации по построению Leader Election#

Рекомендация от Consul по построению механизма выбора лидера на клиентской стороне выглядят след образом:

Все клиенты одного сервиса должны договориться (захардкодить) о названии сессии и ключа в KV хранилище, через который будут договариваться о выбранном лидере. Хорошим выбором имени сессии для нашей задачи будет services/my-service/leader. Точно такое же имя стоит выбрать и для ключа в KV хранилище. Для удобства, далее название сессии будет упоминаться как SessionName, а название ключа как Key.

Значением ключа Key будет всегда некоторый идентификатор клиента(инстанса) нашего приложения. Если мы работаем в Kubernetes, то для этого можно использовать имя пода или адрес хоста, а может быть у вас есть свои идентификаторы инстансов. Тем не менее, если идентификатора инстанса нет - можно обратиться к своему локальному агенту (помните - у каждого инстанса свой локальный агент) и получить имя агента. Я бы рекомендовал использовать именно его, даже если есть некоторый свой идентификатор. Далее он будет упоминаться как ClientID.

При старте клиента (здесь и далее подразумеваются все клиенты), необходимо в первую очередь создать новую сессию с именем SessionName. При создании необходимо указать время жизни сессии. Например 10s. Этим мы перестрахуемся на случай неожиданного останова нашего приложения. После создания сессии Consul вернет нам идентификатор созданной сессии - SessionID. Храним его и при необходимости обновляем. Он нам потребуется далее.

Сразу после создания сессии, необходимо запустить фоновый процесс обновления сессии (renew). Этот процесс должен работать бесконечно, а периодичность его должна быть не более чем TTL созданной сессии. Этим параметром можно поиграться на больших инсталляциях и крутить его от TTL/2 до ~TTL/1.2. Если нет точного понимания сколько ставить - ставьте 50% от TTL. То есть в нашем случае 5s. Думаю, здесь все понятно - иначе, если мы попытаемся обновить сессию после окончания срока ее жизни то получим условный NotFound, так как сессии больше нет.

Процесс обновления сессии может по каким-то причинам не обновить ее в очередной момент. В таком случае необходимо создать новую сессию с точно такими же параметрами и начать процесс обновления уже этой сессии.

Клиент только что запустился и подготовился к работе.

На данный момент он не знает о том, что происходило до момента его запуска и кто сейчас лидер. Первым делом клиент проверяет наличие ключа Key в хранилище Consul. Если ключ существует - стоит проверить его данные. Проверяем значение, которое хранится по ключу. Если в значении находится идентификатор ClientID, который принадлежит нам - это означает, что мы и есть лидер. Такое может произойти, например, после неожиданного перезапуска нашего текушего клиента.

Если ключ не существует - значит мастер еще не выбран. Необходимо начать процедуру блокировки. Одной командой создаем в KV ключ Key с телом, в котором находится наш идентификатор ClientID. В параметрах запроса на создание ключа есть так же поле Session, в которое необходимо положить наш текущий SessionID.

Если мы были первыми - мы получим успешный ответ от Consul, что будет означать, что мы успешно захватили лидерство. Если произошла ошибка - значит кто-то успел это сделать до нас и сейчас мы не в статусе лидера.

В идеальном мире, где все работает без ошибок - больше ничего делать не нужно. Мы (или кто-то другой) является лидером и задача выбора этого лидера для нашей системы решена. Но, на практике такого не бывает и мы продолжаем.

Вне зависимости от результата создания ключа, клиент обязан подписаться на его обновление. Обновление ключа может произойти, если наш обновлятор сессии не успеет обновить ее и сессия будет уничтожена. В этом случае ключ будет обновлен самим Consul (а именно - будет удалена связь ключа с сессией, которая его блокировала). Так же нашу сессию может убить руками оператор (человек) в интерфейсе Consul, что повлечет точно такое же обновление ключа.

При каждом новом изменении ключа для нас важны след его свойства:

  • Значение свойства Session.
    • Если оно пустое - начинаем процедуру блокировки, как описано выше.
    • Если оно равно текущему идентификатору сессии SessionID:
      • Убедимся, что идентификатор клиента ClientID точно такой же, что и в значении ключа Key. Если это так - то мы мастер, помним об этом.

В ином случае кто-то другой является мастером, нам это не важно. Заново подписываемся на обновление ключа. У нас есть алгоритм выбора мастера.

Практическая часть будет в след статье.