Несколько правил как организовать точки, очереди и как их правильно именовать, чтобы было удобно.
Каждая очередь должна представлять только один тип заданий. Не следует смешивать различные типы сообщений в одной очереди. И когда это правило соблюдается, мы можем четко назвать очередь с заданием, представленное им.
Избегайте повторной отправки сообщений в очередь. Если вы обнаружите, что ваш подписчик пытается повторно отправить какие-либо сообщения в другие очереди без реальной обработки, скорее всего что-то спроектировано не правильно. Маршрутизация — это ответственность точек обмена, а не очередей.
Поставщики не должны ничего знать об очередях. Одна из основных идей AMQP является разделение ответственности точек и очередей, так что поставщикам не нужно заботиться о том чтобы сообщение дошло до подписчика.
Предположим вы хотите спроектировать точки и очереди для «пользовательских» связанных событий записи. События записи будут запускаться в одном или нескольких приложениях, и эти сообщения будут использоваться некоторыми другими приложениями.
Первый вопрос, который обычно задают — это различные события одного объекта (объект " пользователь" в данном примере), следует ли использовать одну точку обмена для публикации всех трех событий или использовать 3 отдельных точки для каждого события? Или, короче говоря, одна точка обмена, или много?
Прежде чем ответить на этот вопрос, я хочу задать другой вопрос: действительно ли нам нужна отдельная точка для этого случая? Что если мы абстрагируем все 3 типа событий как событие «write», подтипы которого «created», «updated» и «deleted»?
Самое простое решение, создать очередь «user.write», и публиковать все сообщения событий записи пользователя в эту очередь через глобальную точку обмена.
Самое простое решение не может работать, когда есть второе приложение (имеющее другую логику обработки), которое хочет подписаться на любые сообщения, опубликованные в очереди. Когда подписаны несколько приложений, нам, по крайней мере, нужна одна точка с типом «fanout» с привязками к нескольким очередям. Таким образом, сообщения отправляются в точку, и она дублирует сообщения в каждую очередь. Каждая очередь представляет задание обработки для каждого приложения.
Второе решение отлично работает, если каждый подписчик действительно хочет обрабатывать все подтипы «user.write» событий. Например, если приложение подписчика предназначено для простого хранения журнала транзакций.
С другой стороны не очень хорошо, когда некоторые подписчики находятся за пределами вашей организации, и вы хотите уведомить их только о некоторых определенных событиях, например приложение app2 должно получить сообщение о создании пользователя и не должно знать о событиях обновления и удаления.
Чтобы решить проблему выше, мы должны извлечь событие «user.created» из типа «user.write». Точка обмена с типом «topic» может нам помочь. При публикации сообщений будем использовать user.created/user.updated/user.deleted как ключи роутинга в точке, так мы сможем поставить ключ связи «user.*» в очереди «user.write.app1» и ключ связи «user.created» в очереди «user.created.app2».
Тип «topic» точки обмена является более гибким в случае, если потенциально будет больше типов событий. Но если вы четко знаете точное количество событий, вы также можете использовать тип «direct» для повышения производительности.
Возвращаемся к вопросу «одна точка, или много?». Пока все решения используют только одну точку, работает нормально, ничего плохого. В каких ситуациях мы можем нуждаться в нескольких точках?
Давайте рассмотри пример когда помимо созданных, обновленных и удаленных событий, описанных выше, у нас есть другая группа событий: вход и выход — группа событий, описывающих «поведение пользователя», а не «запись данных». Для разных групп событий могут потребоваться совершенно разные стратегии маршрутизации и соглашения о ключах и именах очередей, для этого и нужны отдельные точки обмена.
Вольный перевод статьи RabbitMQ Exchange and Queue Design Trade-off.
| exchange | type | binding_queue | binding_key |
|-------------------------------------------------------|
| user.write | topic | user.created.app2 | user.created |
Понятия
AMQP (Advanced Message Queuing Protocol) — открытый протокол для передачи сообщений между компонентами системы.
Поставщик (Publishers/Producer) — программа, отправляющая сообщения.
Подписчик (Consumer) — программа, принимающая сообщения. Обычно подписчик находится в состоянии ожидания сообщений.
Очередь (Queue) — очередь сообщений.
Точка обмена (Exchange) — изначальная точка обмена очереди, которая занимается маршрутизацией.
Поставщик (Publishers/Producer) — программа, отправляющая сообщения.
Подписчик (Consumer) — программа, принимающая сообщения. Обычно подписчик находится в состоянии ожидания сообщений.
Очередь (Queue) — очередь сообщений.
Точка обмена (Exchange) — изначальная точка обмена очереди, которая занимается маршрутизацией.
Правило 1
Каждая очередь должна представлять только один тип заданий. Не следует смешивать различные типы сообщений в одной очереди. И когда это правило соблюдается, мы можем четко назвать очередь с заданием, представленное им.
Правило 2
Избегайте повторной отправки сообщений в очередь. Если вы обнаружите, что ваш подписчик пытается повторно отправить какие-либо сообщения в другие очереди без реальной обработки, скорее всего что-то спроектировано не правильно. Маршрутизация — это ответственность точек обмена, а не очередей.
Правило 3
Поставщики не должны ничего знать об очередях. Одна из основных идей AMQP является разделение ответственности точек и очередей, так что поставщикам не нужно заботиться о том чтобы сообщение дошло до подписчика.
Примеры и решения
Предположим вы хотите спроектировать точки и очереди для «пользовательских» связанных событий записи. События записи будут запускаться в одном или нескольких приложениях, и эти сообщения будут использоваться некоторыми другими приложениями.
| object | event |
|------------------|
| user | created |
| user | updated |
| user | deleted |
Первый вопрос, который обычно задают — это различные события одного объекта (объект " пользователь" в данном примере), следует ли использовать одну точку обмена для публикации всех трех событий или использовать 3 отдельных точки для каждого события? Или, короче говоря, одна точка обмена, или много?
Прежде чем ответить на этот вопрос, я хочу задать другой вопрос: действительно ли нам нужна отдельная точка для этого случая? Что если мы абстрагируем все 3 типа событий как событие «write», подтипы которого «created», «updated» и «deleted»?
| object | event | sub-type |
|-----------------------------|
| user | write | created |
| user | write | updated |
| user | write | deleted |
Решение 1
Самое простое решение, создать очередь «user.write», и публиковать все сообщения событий записи пользователя в эту очередь через глобальную точку обмена.
Решение 2
Самое простое решение не может работать, когда есть второе приложение (имеющее другую логику обработки), которое хочет подписаться на любые сообщения, опубликованные в очереди. Когда подписаны несколько приложений, нам, по крайней мере, нужна одна точка с типом «fanout» с привязками к нескольким очередям. Таким образом, сообщения отправляются в точку, и она дублирует сообщения в каждую очередь. Каждая очередь представляет задание обработки для каждого приложения.
| 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» событий. Например, если приложение подписчика предназначено для простого хранения журнала транзакций.
С другой стороны не очень хорошо, когда некоторые подписчики находятся за пределами вашей организации, и вы хотите уведомить их только о некоторых определенных событиях, например приложение app2 должно получить сообщение о создании пользователя и не должно знать о событиях обновления и удаления.
Решение 3
Чтобы решить проблему выше, мы должны извлечь событие «user.created» из типа «user.write». Точка обмена с типом «topic» может нам помочь. При публикации сообщений будем использовать user.created/user.updated/user.deleted как ключи роутинга в точке, так мы сможем поставить ключ связи «user.*» в очереди «user.write.app1» и ключ связи «user.created» в очереди «user.created.app2».
| 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
Тип «topic» точки обмена является более гибким в случае, если потенциально будет больше типов событий. Но если вы четко знаете точное количество событий, вы также можете использовать тип «direct» для повышения производительности.
| 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.* |
Вольный перевод статьи RabbitMQ Exchange and Queue Design Trade-off.