Подтвердить что ты не робот

RabbitMQ/AMQP - Лучшая практика Очередь/Тема Дизайн в архитектуре MicroService

Мы думаем о внедрении подхода AMQP для нашей инфраструктуры микросервиса (хореография). У нас есть несколько услуг, скажем, обслуживание клиентов, обслуживание пользователей, сервис статей и т.д. Мы планируем внедрить RabbitMQ в качестве нашей центральной системы обмена сообщениями.

Я ищу лучшие практики для разработки системы по темам/очередям и т.д. Один из вариантов - создать очередь сообщений для каждого отдельного события, которое может произойти в нашей системе, например:

user-service.user.deleted
user-service.user.updated
user-service.user.created
...

Я думаю, что это неправильный подход для создания сотен очередей сообщений, не так ли?

Я хотел бы использовать Spring и эти красивые аннотации, например, например:

  @RabbitListener(queues="user-service.user.deleted")
  public void handleEvent(UserDeletedEvent event){...

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

Мой второй вопрос: если я хочу слушать в очереди, которая раньше не была создана, я получу исключение в RabbitMQ. Я знаю, что могу "объявить" очередь с помощью AmqpAdmin, но должен ли я делать это для каждой очереди моих сотен в каждом микросервисе, так как всегда может случиться, что очередь не была создана до сих пор?

4b9b3361

Ответ 1

Я обычно считаю, что лучше всего иметь обмены, сгруппированные по типам объектов/типам обмена.

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

в одном сценарии, имеет смысл иметь обмен на событие, как вы указали. вы можете создать следующие обмены

| exchange     | type   |
|-----------------------|
| user.deleted | fanout |
| user.created | fanout |
| user.updated | fanout |

это соответствовало бы шаблону pub/sub" для всех слушателей, не заботясь о том, что слушает.

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

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

| exchange     | type   |
|-----------------------|
| user         | topic  |

С помощью такой настройки вы можете использовать клавиши маршрутизации для публикации определенных сообщений в определенных очередях. Например, вы можете опубликовать user.event.created в качестве ключа маршрутизации и провести маршрут с определенной очередью для конкретного пользователя.

| exchange     | type   | routing key        | queue              |
|-----------------------------------------------------------------|
| user         | topic  | user.event.created | user-created-queue |
| user         | topic  | user.event.updated | user-updated-queue |
| user         | topic  | user.event.deleted | user-deleted-queue |
| user         | topic  | user.cmd.create    | user-create-queue  |

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

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

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

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

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

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

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

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

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

Ответ 2

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

Имена очередей должны быть указаны после того, что сделает потребитель, подключенный к очереди. Какова цель операции этой очереди. Предположите, что вы хотите отправить электронное письмо пользователю, когда создается их учетная запись (когда сообщение с ключом маршрутизации user.event.created отправлено с использованием ответа Derick выше). Вы должны создать имя очереди sendNewUserEmail (или что-то в этом направлении, в том стиле, который вам подходит). Это означает, что легко просмотреть и точно знать, что делает эта очередь.

Почему это важно? Итак, теперь у вас есть другой ключ маршрутизации user.cmd.create. Скажем, это событие отправляется, когда другой пользователь создает учетную запись для кого-то другого (например, членов команды). Вы по-прежнему хотите отправить электронное письмо этому пользователю, так что вы создаете привязку для отправки этих сообщений в очередь sendNewUserEmail.

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

Ответ 3

Прежде чем ответить на "один обмен или много?" вопрос. Я действительно хочу задать другой вопрос: действительно ли нам нужен специальный обмен для этого случая?

Различные типы событий объекта настолько естественны, чтобы соответствовать различным типам сообщений, которые должны публиковаться, но иногда это не обязательно. Что, если мы отрисуем все три типа событий как событие "записи", подтипы которых "созданы", "обновлены" и "удалены"?

| object | event   | sub-type |
|-----------------------------|
| user   | write   | created  |
| user   | write   | updated  |
| user   | write   | deleted  |

Решение 1

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

| queue      | app  |
|-------------------|
| user.write | app1 |

Решение 2

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

| queue           | subscriber  |
|-------------------------------|
| user.write.app1 | app1        |
| user.write.app2 | app2        |

| exchange   | type   | binding_queue   |
|---------------------------------------|
| user.write | fanout | user.write.app1 |
| user.write | fanout | user.write.app2 |

Это второе решение прекрасно работает, если каждый абонент заботится и хочет обрабатывать все подтипы событий "user.write" или, по крайней мере, подвергать все эти события подтипа каждому подписчику, это не проблема. Например, если приложение-подписчик просто хранит журнал изменений; или хотя абонент обрабатывает только user.created, это нормально, чтобы сообщить ему о том, когда происходит user.updated или user.deleted. Это становится менее изящным, когда некоторые подписчики являются внешними из вашей организации, и вы хотите только уведомить их о некоторых конкретных событиях подтипа. Например, если app2 только хочет обрабатывать user.created и он не должен обладать знаниями о пользователе. Обновлено или вообще не указано.

Решение 3

Чтобы решить проблему выше, нам нужно извлечь концепцию user.created из "user.write" . Может быть полезен тип обмена "тема". При публикации сообщений вы можете использовать user.created/user.updated/user.deleted как ключи маршрутизации, чтобы мы могли установить ключ привязки "user.write.app1" в очередь "user. *" И ключ привязки "user.created.app2" очередь будет "user.created".

| queue             | subscriber  |
|---------------------------------|
| user.write.app1   | app1        |
| user.created.app2 | app2        |

| exchange   | type  | binding_queue     | binding_key  |
|-------------------------------------------------------|
| user.write | topic | user.write.app1   | user.*       |
| user.write | topic | user.created.app2 | user.created |

Решение 4

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

| queue             | subscriber  |
|---------------------------------|
| user.write.app1   | app1        |
| user.created.app2 | app2        |

| exchange   | type   | binding_queue    | binding_key   |
|--------------------------------------------------------|
| user.write | direct | user.write.app1   | user.created |
| user.write | direct | user.write.app1   | user.updated |
| user.write | direct | user.write.app1   | user.deleted |
| user.write | direct | user.created.app2 | user.created |

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

Решение 5

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

| queue              | subscriber  |
|----------------------------------|
| user.write.app1    | app1        |
| user.created.app2  | app2        |
| user.behavior.app3 | app3        |

| exchange      | type  | binding_queue      | binding_key     |
|--------------------------------------------------------------|
| user.write    | topic | user.write.app1    | user.*          |
| user.write    | topic | user.created.app2  | user.created    |
| user.behavior | topic | user.behavior.app3 | user.*          |

Другие решения

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

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

Я также создал сообщение в блоге, собрав эту проблему, решения и другие связанные с этим соображения.