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

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

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

Возьмем довольно распространенный сценарий, когда у вас есть кэш в памяти на узлах. (Узлом может быть сервер без операционной системы, виртуальная машина, модуль Kubernetes, что угодно, это просто некоторая вычислительная мощность, на которой работает ваш сервис). Я имею в виду, что мы не используем распределенный кеш, такой как как Redis, Memcached, но некоторые данные хранятся непосредственно в оперативной памяти узла, так что все узлы поддерживают свои изолированные локальные копии определенных данных. Теперь выполняется вызов API для обновления этих конкретных данных, узел, который получил этот вызов API, принял запрос и сделал необходимые обновления в базе данных. Теперь этот принимающий узел знает, что кеш должен быть аннулирован, и он может легко сделать локальный кеш недействительным. Но как быть с остальными узлами? Они также должны аннулировать устаревшие данные в кеше, верно? Но как эти узлы узнают, что это за сигнал? Я предполагаю, что в этот момент принимающему узлу нужен какой-то способ передать сообщения о недействительности всем другим узлам, чтобы они также обновили свой локальный кеш.

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

Возможные решения

Посмотрев на проблему, я мог придумать 3 вида решений. Среди них два были немного ясными, а один был полностью экспериментальным.

  1. Решение для обмена сообщениями Fanout на основе очереди сообщений с использованием таких технологий, как Kafka, RabbitMQ и т. д.
  2. Простое решение pub/sub с использованием Redis.
  3. Решение, основанное на протоколе сплетен.

О первых двух у меня было какое-то размытое представление, но 3-й был для меня полностью теоретическим.

Обмен Fanout на основе очереди сообщений

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

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

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

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

Использование Кафки

Создание разветвления Kafka может быть таким. Вам просто нужно поместить все узлы в разные группы потребителей, и таким образом всякий раз, когда какое-либо сообщение публикуется в теме, все узлы будут получать сообщение отдельно. Но вы должны убедиться, что во всей группе потребителей есть только один потребитель/один узел. Это работает так: когда вы размещаете сообщение на любую тему, его читают разные потребители. А Kafka отслеживает последнее сообщение, которое читал какой-либо конкретный потребитель, чтобы ни одно сообщение не использовалось несколько раз. Итак, теперь это отслеживание осуществляется на основе группы потребителей. Таким образом, если в одной группе потребителей есть 3 потребителя и одно сообщение с идентификатором «message-11100», как только один из этих 3 потребителей прочитает это сообщение, это сообщение будет помечено как прочитанное для этой конкретной группы потребителей и двух других. узлы не получат это сообщение. Но загвоздка в том, что, поскольку это отслеживание выполняется на основе группы потребителей, если существуют другие группы потребителей, один из узлов в этих группах потребителей по-прежнему имеет право читать «сообщение-11100». Таким образом, мы воспользуемся этим свойством и поместим только 1 узел в каждую группу потребителей, чтобы все узлы теперь могли читать одно и то же сообщение, и они больше не были конкурирующими потребителями.

Я знаю, что это может быть немного ошеломляющим, если вы не знаете Кафку, так что не стесняйтесь больше об основах Кафки здесь.

Использование RabbitMQ

RabbitMQ имеет встроенную поддержку разветвленных обменов. Когда сообщение, отправленное на разветвленный обмен, фактически транслируется во все подключенные очереди. Таким образом, эта установка будет довольно простой. Сначала мы создаем разветвленный обмен и создаем одну очередь на узел на этом разветвленном обмене. Итак, теперь настройка становится такой: у нас есть разветвленный обмен, а именно fan_ex_1, и у нас есть пары узлов и очередей, у нас есть q_1, на который подписан node_1, у нас есть q_2, на который подписан node_2, и так далее. Итак, когда какой-либо узел помещает сообщение на fan_ex_1, затем q_1, q_2…. q_n все получают это сообщение, что означает, что все узлы также получают это сообщение. То же самое еще раз, если вы мало что знаете о RabbitMQ, не стесняйтесь читать больше здесь.

Pub/Sub с использованием Redis

Redis, вероятно, является швейцарским ножом распределенных систем, и неудивительно, что мы можем использовать Redis pub/sub для создания простой системы, подобной шине сообщений.

Это снова будет только на похожих линиях. У нас есть издатель, который публикует сообщение, узел-производитель для нас. Издатель публикует это сообщение в канале Redis, который здесь напоминает шину сообщений, которая похожа на непрерывный поток данных. Кроме того, у нас есть потребители, которые подписаны на этот канал, поэтому всякий раз, когда в этом канале появляется новое сообщение, все потребители получают обратный вызов, после чего они могут обработать это сообщение. Как видите, узел-производитель отправляет уведомление, и, поскольку все остальные узлы подписаны на этот конкретный канал уведомлений, все получают пинг об этом новом сообщении. Это, вероятно, самое простое решение среди всех, но если вы хотите узнать больше о Redis pub/sub, ознакомьтесь с их документом здесь.

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

Протокол сплетен

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

Это самый интересный, самый сложный и самый трудоемкий вариант, поэтому я хотел построить его больше всего 😝

Итак, сплетни — это сетевой протокол/алгоритм, в котором несколько узлов образуют кластер сплетен, и все узлы в кластере знают обо всех других узлах, периодически обмениваясь heartbeat/метаданными между собой. И когда нам нужно распространить сообщение на все узлы в кластере, узел-производитель случайным образом выберет узел для передачи сообщения. А затем принимающий узел снова отправит это сообщение какому-то другому случайному узлу, и так будет продолжаться. Следовательно, сообщение в конечном итоге достигает всех других узлов. Популярные технологии, такие как Cassandra, Consul используют Gossip для формирования кластеров. Подробнее о протоколе сплетен здесь.

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

Несмотря на то, что Gossip не подходит для этого конкретного варианта использования, он по-прежнему является отличным выбором для межузловой связи в целом.

Выбор дизайна

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

Подход, основанный на слухах, был неуместен, поскольку не соответствовал варианту использования. Итак, подход на основе очереди или на основе Redis? Redis был самым простым и требовал наименьшей работы инфраструктуры для моей системы. Поэтому я выбрал решение Redis pub/sub. Но если вы уже интегрировали Kafka/RabbitMQ в свои сервисы и у вас есть команда, которая хорошо разбирается в этих технологиях, то подход на основе очередей может быть для вас лучшим выбором. И если бы мне дали все время в мире, чтобы построить это, я бы, вероятно, выбрал решение Fanout, так как это было бы немного более устойчивым и надежным решением.

На сегодня все, спасибо за прочтение!

Меня зовут Аритра Дас, я разработчик, и мне очень нравится создавать сложные распределенные системы. Не стесняйтесь обращаться ко мне в Linkedin или Twitter по любым вопросам, связанным с технологиями.

Удачного обучения…