Существует целая категория проблем в распределенных вычислениях, которые требуют, чтобы один клиент среди множества одноранговых узлов в сети координировал поведение всех других клиентов. Это распространено в системах, отвечающих за обработку очередей работы, которые подключаются к нижестоящим системам, которые не поддерживают одновременные подключения. Или в системах, где работа распараллелена определенным образом.

Например:

  • Система, которая отслеживает расположение файла по SFTP, может иметь один узел мониторинга, который загружает большой файл, а затем распределяет подмножество строк в этом файле на другие узлы для параллельной обработки.
  • Системы, в которых открыто прямое сокетное соединение для потоковой передачи работы вниз, которая затем распределяется или ставится в очередь другим клиентам для обработки.
  • Все, что требует поддержки одного соединения с сервером или другим ресурсом.

Этот паттерн, в котором один из участников является «координатором», популярен в видеоиграх, где запуск многопользовательских серверов зачастую непомерно дорог. Гораздо дешевле предоставить клиентам один сервер для поиска партнеров, а затем разместить логику многопользовательской игры на одном из узлов.

Проблема с этими архитектурами — и их слабость — возникает, если координирующий клиент — лидер — умирает и не может восстановиться. Без координирующего клиента вся система перестанет обрабатывать сообщения и впадет в состояние «зомби».

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

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

Такой шаблон является мощным, потому что он делает масштабирование сети очень простым — просто добавьте больше клиентов (или узлов) в сеть и разверните то же самое программное обеспечение, которое работает везде, на новых.

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

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

Существует несколько способов реализации выборов лидера в сети:

  • Гонка за получением распределенной блокировки общего ресурса (например, записи в базе данных или файла, доступного по сети, например сегмента хранилища BLOB-объектов S3/Azure).
  • Ранжирование клиентов по идентификаторам клиентов
  • Внедрение алгоритма передачи сообщений, при котором одноранговые узлы передают сообщения о выборах по каждому узлу в сети (см. Алгоритм кольца)

У каждого подхода есть плюсы и минусы. Как и большинство архитектурных вариантов, выбранный вами метод будет зависеть от остальной архитектуры вашего приложения.

Если вы имеете дело с обработкой файлов или подобными системами, вы, вероятно, настроите своих клиентов на гонку за распределенной блокировкой. Часто это вариант по умолчанию, и его легко реализовать. Реализуйте поток/повторяющуюся задачу, предназначенную для блокировки известного местоположения файла. Любой клиент, успешно заблокировавший файл, избирается лидером и начинает координировать действия. Если этот клиент умирает, блокировка снимается, и другой одноранговый узел получает блокировку и продолжает работу.

В одноранговых сетях на основе браузера этот подход менее жизнеспособен. Браузерные приложения обычно являются однопоточными и не имеют доступа к общим хранилищам с возможностью записи. Это делает ранжирование клиентов более естественным для этих сред, и его можно реализовать с помощью очень небольшого кода с помощью Ably SDK.

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

Хотя это и не приложение, которое вы когда-либо продавали, оно похоже по архитектуре на одноранговое голосование, одноранговые игры или любое другое приложение, которое начинается, когда присутствует один клиент, и заканчивается, когда последний клиент уходит. .

Демо — приложение для подсчета голосов с выбором лидера

Для запуска демо вам понадобится ключ Ably API. Если вы еще не зарегистрированы, вы можете зарегистрироваться сейчас для получения бесплатной учетной записи Ably. Если у вас есть аккаунт Ably:

  1. Войти в панель управления приложения.
  2. В разделе "Ваши приложения" нажмите "Управление приложением" для любого приложения, которое вы хотите использовать для этого руководства, или создайте новое с помощью кнопки "Создать новое приложение". .
  3. Перейдите на вкладку "Ключи API".
  4. Скопируйте секретное значение "API Key" из вашего корневого ключа, мы будем использовать его позже при создании нашего приложения.

Мы используем Ably Presence API и TypeScript для создания этой демонстрации. Presence API уведомляет участников канала Ably, когда клиент присоединяется к каналу или покидает его. Клиенты могут устанавливать, читать и обновлять свои presence data (строка, которой может быть присвоено любое значение и которая используется для хранения информации о клиенте).

Мы создаем класс, который является оберткой для Ably JavaScript SDK, и подписывается на presence события SDK. Если лидер уже не выбран, когда клиент присоединяется к каналу или покидает его, лидером избирается клиент с наименьшим clientId. Когда Ably предоставляет данные о присутствии, мы знаем, что, несмотря на то, что эта логика выполняется на каждом клиенте в сети, порядок идентификаторов clientId одинаков для каждого узла.

Начнем с определения класса Swarm в файле Swarm.ts и некоторых переменных-членов.

Мы также сохраняем идентификатор, экземпляр Ably SDK и канал, к которому мы подключены, как свойства объекта.

Затем мы определяем некоторые свойства для хранения обратных вызовов. Когда происходят события подключения и выборов, мы вызываем соответствующий обратный вызов, чтобы позволить приложению инициировать соответствующее действие:

Конструктор принимает channelName для подключения и генерирует идентификатор для этого клиента. В этой реализации мы создаем новый экземпляр Ably и подключаемся к предоставленному channelName, передавая сгенерированный clientId.

Обратите внимание, что в реальном приложении из соображений безопасности следует использовать аутентификацию с помощью токена, а не использовать ключ API непосредственно в коде.

После того, как мы создали экземпляр Ably, мы собираемся подключить и сохранить экземпляр канала в this.channel:

Метод connect будет подписываться на событие presence в channel, передавая обратный вызов onSwarmPresenceChanged, который будет вызываться при возникновении события. После подписки мы войдем в presence с состоянием «подключено» и вызовем обратный вызов onConnection:

Обратный вызов onPresenceChanged будет вызываться, когда клиент присоединяется к каналу или покидает его. Всякий раз, когда это происходит, мы извлекаем последний полный набор присутствия из экземпляра канала (который кэшируется), запускаем процесс выбора и, наконец, вызываем обратный вызов onSwarmPresenceChanged.

Логика выбора реализована в ensureLeaderElected. Мы используем массив участников, чтобы определить, является ли клиент лидером. Во-первых, он проверяет массив members, переданный в функцию, и, если уже есть лидер (кто-то с данными о присутствии «лидера»), он остановится и вернется раньше. Лидер уже есть, так что больше ничего делать не нужно.

Если лидера нет, мы сортируем массив участников по алфавиту, используя свойство clientId каждого PresenceMessage:

Если первый элемент в массиве members является текущим клиентом, мы обновляем его данные presence до "leader" и вызываем обратный вызов onElection. При обновлении данных о присутствии этот клиент будет отмечен как ведущий, а вызывающий код использует обратный вызов onElection для соответствующего запуска логики приложения.

Помните, что этот код работает для каждого клиента в рое, но только первый клиент, отсортированный по clientId, выберет себе лидера.

Это демонстрационное приложение использует Модули ES и Vite в качестве опыта разработки. Приложение состоит из четырех файлов:

./app/Swarm.ts
./app/index.html 
./app/script.ts 
./app/style.css

Мы уже работали с Swarm.ts, поэтому теперь мы создадим index.html:

Обратите внимание, что ссылка на ./script.ts использует type="module" в теге скрипта. Модули ES позволяют браузеру импортировать script.ts в качестве модуля во внешнем интерфейсе. (Это означает, что мы можем использовать собственные операторы импорта браузера в коде JavaScript).

Пользовательский интерфейс этой демонстрации очень прост — пара абзацев, отображающих текущие браузеры ClientId и является ли этот клиент лидером.

Он также отображает текущее значение счетчика и список всех клиентов в рое.

script.ts

Вся логика приложения находится в script.ts.

Мы начинаем с импорта зависимостей и использования document.getElementById для получения ссылки на элементы client-id, counter-value, Leader и Present-members.

Здесь следует отметить две интересные вещи:

  • мы используем import для импорта как типов Ably SDK, так и нашего класса ./Swarm из более раннего. Vite гарантирует, что это работает нормально.
  • мы инициализируем переменную с именем counter в 0. Это состояние приложения.

Класс Swarm подключится к some-channel-name и инициализирует пользовательский интерфейс текущим ClientId и значением "false" для leaderUi:

Теперь, когда пользовательский интерфейс настроен, нам нужно прикрепить некоторые функции к нашим обратным вызовам Swarm.

Мы назначаем функцию обратного вызова для onSwarmPresenceChanged. Когда пользователи присоединяются к каналу или покидают его, он обновляет пользовательский интерфейс строковой версией массива участников.

Мы также назначаем функцию обратного вызова для onElection. Это обновит пользовательский интерфейс, чтобы показать, что выбранный клиент является лидером.

Кроме того, когда лидер избирается лидером, создается интервал, который увеличивает переменную counter каждые пять секунд. По мере увеличения счетчика сообщение публикуется для всех других одноранговых узлов в рое с обновленным значением счетчика.

Чтобы убедиться, что значение counter обновляется в пользовательском интерфейсе, мы предоставляем обратный вызов onConnection, который подписывается на сообщения с именем "message".

Когда приходит сообщение, мы сначала обновляем counter значением из тела сообщения, а затем устанавливаем обновленное значение innerText из counterUi.

Наконец, мы подключаемся к рою:

Стоит отметить, что обратный вызов onConnection выполняется как для клиентов leader, так и для клиентов follower. Если вы наблюдательны, то заметите, что значение counter обновляется в обоих случаях. Для этого примера не имеет значения, что значение счетчика обновляется дважды на лидере и один раз на ведомых.

Попробуйте

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

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

Вы можете клонировать здесь репозиторий для запуска приложения. Для запуска этой демонстрации вам понадобятся node и npm.

npm install 
npm run start

Затем откройте в браузере http://localhost:8000.

Вам нужно будет отредактировать Swarm.ts, чтобы предоставить собственный ключ API Ably для работы примера приложения.

Дальнейшее чтение

У Microsoft есть отличные рекомендации по различным способам реализации шаблона выбора лидера в их Каталоге облачных шаблонов и практик.

Первоначально опубликовано на https://ably.com 3 марта 2022 г.