Exchange, очередь и binding
Разбираемся, почему продюсер в RabbitMQ физически не может положить сообщение в очередь — и почему это не недоработка, а главная идея брокера.
Exchange (обменник) — точка входа сообщений в брокер. Сам он ничего не хранит: его работа — сверить
routing keyсообщения со связками (binding) и положить копию сообщения в каждую подошедшую очередь.
Если вы приходите в RabbitMQ из мира Redis-списков, то первый рефлекс — искать метод «положить в очередь orders». Такого метода нет. В API есть basic_publish(exchange=..., routing_key=...) — и слова «queue» в нём нет вообще. Продюсер отдаёт сообщение обменнику и говорит: «вот ключ, разбирайся сам». Куда оно поедет — решает конфигурация брокера, а не код отправителя.
Зачем такая прослойка
Представьте сервис оплаты. Он публикует событие «платёж прошёл». Сегодня это событие нужно ровно одному потребителю — сервису писем, чтобы отправить чек. Через месяц приходит аналитика: «нам тоже нужны платежи». Ещё через месяц — склад: «по оплаченным заказам надо резервировать товар».
Если бы продюсер писал прямо в очередь, каждый новый потребитель означал бы правку и деплой сервиса оплаты. С обменником не так: вы создаёте новую очередь, привязываете её к тому же exchange — и она немедленно начинает получать копии тех же сообщений. Код продюсера не тронут. Он вообще не знает, сколько у него читателей: ноль, один или семь.
Отсюда практическое правило: продюсер знает про exchange, консюмер знает про свою очередь, и друг про друга они не знают ничего. Это и есть развязка, ради которой брокер ставят.
Три сущности и одна связка
| Сущность | Что делает | Хранит сообщения? |
exchange | принимает публикации и маршрутизирует их | нет, никогда |
queue | копит сообщения до прихода потребителя | да, это единственное хранилище |
binding | правило «exchange → queue при таком ключе» | нет, это просто правило |
Binding — самая недооценённая штука в этом списке. Формально это тройка: имя обменника, имя очереди и binding key — образец, с которым сравнивается routing key сообщения. Одна очередь может иметь сколько угодно binding'ов к одному и тому же exchange, и это нормальный приём: «хочу и payment.succeeded, и payment.refunded».
Полный пример: продюсер
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
# 1. Обменник — точка входа сообщений
channel.exchange_declare(exchange="payments", exchange_type="direct", durable=True)
# 2. Очередь — место, где сообщения ждут потребителя
channel.queue_declare(queue="email.payments", durable=True)
# 3. Связка: сообщения с ключом payment.succeeded попадут в эту очередь
channel.queue_bind(
exchange="payments",
queue="email.payments",
routing_key="payment.succeeded",
)
# 4. Публикуем — в exchange, а не в очередь!
channel.basic_publish(
exchange="payments",
routing_key="payment.succeeded",
body=b'{"order_id": 4821, "amount": 1990}',
properties=pika.BasicProperties(
delivery_mode=2, # сообщение переживёт рестарт брокера
content_type="application/json",
),
)
print("Отправлено")
connection.close()Построчный разбор
exchange_declare— создаёт обменник, если его нет. Операция идемпотентна: повторный вызов с теми же параметрами ничего не сломает. Типdirectозначает «routing key должен точно совпасть с binding key».queue_declare— то же самое для очереди.durable=Trueговорит: описание очереди сохранить на диск, чтобы после перезапуска брокера она не исчезла.queue_bind— та самая связка. Без неё сообщение доедет до обменника и тихо исчезнет: класть его будет некуда.basic_publish— обратите внимание, чтоrouting_keyздесь конкретный (payment.succeeded), а не шаблон. Шаблоны бывают только в binding'ах.delivery_mode=2— пометить сообщение как persistent. Про это ниже, это классическая ловушка.
Полный пример: консюмер
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
# Консюмер объявляет ровно ту же топологию — на всякий случай
channel.exchange_declare(exchange="payments", exchange_type="direct", durable=True)
channel.queue_declare(queue="email.payments", durable=True)
channel.queue_bind(
exchange="payments",
queue="email.payments",
routing_key="payment.succeeded",
)
# Не давать нам новое сообщение, пока не подтвердили предыдущее
channel.basic_qos(prefetch_count=1)
def on_message(ch, method, properties, body):
print("Отправляю чек:", body.decode())
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(queue="email.payments", on_message_callback=on_message)
print("Жду сообщения. Ctrl+C для выхода.")
channel.start_consuming()Почему консюмер повторяет exchange_declare и queue_bind? Потому что порядок запуска сервисов в реальной жизни непредсказуем. Если каждая сторона объявляет топологию, которая ей нужна, система собирается сама, кто бы ни стартовал первым. Приём называется «декларативная топология», и он на порядок надёжнее, чем «настроим руками в админке и будем надеяться».
Как это работает
Путь сообщения выглядит так:
producer --publish(exchange, routing_key)--> [exchange]
|
сверка routing_key с binding key
|
+--------------------------+--------------------------+
v v v
[queue email] [queue analytics] (нет совпадений)
| | |
consumer 1 consumer 2 сообщение отброшеноКлючевые следствия:
- Обменник не буферизует. Маршрутизация происходит синхронно, в момент публикации. Если подходящих очередей нет — сообщение просто исчезает. Никакой ошибки продюсер по умолчанию не получит.
- Копия на каждую очередь, но одна. Если сообщение подошло трём очередям, каждая получит свою копию. Если одна очередь подошла по двум разным binding'ам — она всё равно получит ровно одну копию, брокер не дублирует.
- Пустое имя обменника — это не «без обменника».
exchange=""— это default exchange, встроенный direct-обменник, к которому каждая очередь автоматически привязана по своему имени. Именно поэтому в туториалах «hello world» работаетbasic_publish(exchange="", routing_key="hello")— routing key совпадает с именем очереди. Удобно для примеров, вредно для архитектуры: продюсер снова знает имена очередей. - Долговечность собирается из трёх кубиков. Пережить рестарт брокера сообщение сможет, только если
durable=Trueу очереди иdelivery_mode=2у самого сообщения. Забыли одно — потеряли всё, что лежало в очереди.
Частые ошибки
- Опубликовали раньше, чем создали очередь. Классика: продюсер стартовал первым, отправил тысячу событий, потом поднялся консюмер и создал очередь — пусто. Все сообщения были unroutable и отброшены. Лечится декларацией топологии на обеих сторонах или отдельным шагом миграции топологии при деплое.
- Забыли
queue_bind. Очередь есть, обменник есть, сообщения уходят — а очередь пустая. Проверяйте вкладку Bindings в management-плагине: пустой список binding'ов означает, что очередь никогда ничего не получит (кроме публикаций через default exchange). - Ждут ошибку при неудачной маршрутизации. По умолчанию её нет. Хотите знать, что сообщение никуда не доехало — публикуйте с
mandatory=Trueи обрабатывайте возврат, либо настройте обменнику alternate-exchange, куда стекается всё «бездомное». - Пересоздали очередь с другими параметрами.
queue_declareидемпотентен только при полном совпадении аргументов. Изменилиdurableили добавили TTL — брокер ответит406 PRECONDITION_FAILEDи закроет канал. Существующую очередь нельзя «доредактировать»: только удалить и создать заново. - Считают, что из обменника можно читать. Нельзя. Нет сообщений в обменнике, нет метода «посмотреть, что там». Всё, что вы видите в UI на вкладке Exchanges, — это счётчики и binding'и.
- Случайные
exclusive=Trueиauto_delete=True. Первый привязывает очередь к соединению (упал клиент — исчезла очередь), второй удаляет очередь, когда отписался последний потребитель. Для рабочих очередей это почти всегда не то, что вам нужно.
Итоги
- Продюсер публикует в exchange с routing key; про очереди он не знает вообще.
- Exchange ничего не хранит — он маршрутизирует здесь и сейчас; хранит только очередь.
- Binding = правило «exchange → queue при таком ключе». Нет binding'а — нет доставки.
- Сообщение без подходящей очереди отбрасывается молча. Нужен контроль —
mandatoryили alternate-exchange. - Топологию объявляют обе стороны — тогда порядок запуска сервисов перестаёт быть проблемой.
- Долговечность = durable-очередь +
delivery_mode=2. Половина не работает.