Очереди, потоки, события
RabbitMQ exchanges: слой маршрутизации, который решает, кому достанется копия
Платёжный сервис выходит в прод. Заказы идут, но очередь команды антифрода пуста — ноль сообщений, ни ошибок, ни исключений в логах. Producer спокойно публикует, брокер принимает. Через три дня замечают: новую очередь антифрода так и не забиндили к exchange. RabbitMQ маршрутизировал каждое сообщение в никуда и отбрасывал его беззвучно. Вызов publish всё это время возвращал успех, потому что по умолчанию exchange без подходящего binding тихо отбрасывает сообщение.
Ты публикуешь в exchange, а не в очередь
Первый сюрприз для всех, кто пришёл с моделью «очередь задач»: producer в RabbitMQ не может отправить сообщение в очередь. Он отправляет в exchange, и exchange решает — по своему типу, routing key сообщения и bindings, которые связывают его с очередями, — какие очереди (если вообще) получат копию. Binding — это правило: «exchange X, доставь в очередь Q, когда routing key совпадает с паттерном P». Одно сообщение может попасть в ноль очередей, в одну или в несколько; каждая совпавшая очередь получает свою независимую копию.
В этой непрямоте — весь смысл. Producer ничего не знает о консьюмерах, очередях и их количестве. Можно добавить нового консьюмера — ту самую очередь антифрода — создав очередь и забиндив её, без единого изменения в producer. Эта развязка и есть причина, почему RabbitMQ называют smart broker: логика маршрутизации живёт в брокере, а не в producer или консьюмере.
Четыре типа exchange — это четыре стратегии маршрутизации
У каждого exchange есть тип, и тип — это алгоритм маршрутизации. Реально используешь четыре:
| Тип | Маршрутизирует, когда… | Применять для |
|---|---|---|
direct | routing key в точности равен binding key | point-to-point маршрутизация задач (payments.refund → один пул воркеров) |
fanout | всегда — routing key игнорируется | broadcast: каждая забинженная очередь получает копию (инвалидация кэша, pub/sub) |
topic | routing key совпадает с точечным паттерном с * / # | селективная подписка (order.*.eu, logs.#) |
headers | атрибуты заголовков сообщения совпадают с аргументами binding | мульти-атрибутная маршрутизация, где плоского ключа мало |
Direct — точное совпадение: забиндь очередь Q с ключом payments.refund, и до неё дойдут только сообщения, чей routing key — ровно payments.refund. Fanout полностью игнорирует routing key и копирует каждое сообщение в каждую забинженную очередь — самый дешёвый и быстрый путь, идеально, когда N независимых консьюмеров хотят одно и то же событие. Topic — рабочая лошадка: routing keys — это слова через точку (order.created.eu), а паттерн binding использует * для совпадения ровно с одним словом и # для нуля или более. Так order.*.eu ловит order.created.eu, но не order.created.us; logs.# ловит всё под logs. Headers игнорирует routing key и матчит по мапе атрибутов заголовков, с x-match равным all или any — мощно, но медленнее и редко того стоит; topic key обычно выражает то же намерение дешевле.
Почему это работает
Topic exchange с binding key # ведёт себя ровно как fanout (матчит всё), а topic без wildcards — как direct. Иногда «просто используют topic для всего». Работает, но fanout и direct проще для рассуждения и чуть быстрее, потому что пропускают сопоставление паттернов, — бери конкретный тип, когда намерение конкретно.
Smart broker, dumb consumer — и почему это противоположность Kafka
Это и есть сеньорная рамка, которая решает, подходит ли RabbitMQ под твою задачу вообще. RabbitMQ пушит сообщения консьюмерам и удаляет каждое, как только его подтвердили (ack). Брокер умный (он маршрутизирует, отслеживает per-message ack, переотправляет при сбое); консьюмер относительно глупый (обрабатывает то, что ему дали, и подтверждает). Подтверждённое сообщение исчезло — нет лога, чтобы перемотать назад.
Kafka — зеркальное отражение: pull-модель dumb broker, smart consumer. Kafka — это append-only лог; брокер просто хранит упорядоченные записи, а консьюмеры сами отслеживают свой offset и тянут в своём темпе. При consume ничего не удаляется — сообщение лежит весь retention window, поэтому можно его replay-нуть, добавить новую consumer group, читающую с начала, или переобработать после фикса бага. Per-message ack-and-delete в RabbitMQ даёт тонкий контроль над workflow и сложную маршрутизацию; лог Kafka даёт replay и сырой throughput.
Разрыв в throughput большой и структурный. Классические очереди RabbitMQ достигают десятков тысяч сообщений/сек на очередь только в лёгкой, транзиентной конфигурации (без durability, без publisher confirms); включи durability, персистентность или quorum-очереди — и это падает существенно, часто до нескольких тысяч. Одна партиция Kafka держит порядка 1 миллиона сообщений/сек, потому что батчит последовательные записи в лог. Обратная сторона: RabbitMQ доставляет с субмиллисекундной p50-latency (пушит по мере поступления), тогда как батчинг Kafka задаёт структурный пол примерно в 5–15 мс p50. Так что выбор не «что лучше» — это сложная per-message маршрутизация и низкая latency (RabbitMQ) против replayable высокопроизводительных стримов (Kafka).
Нужно доставлять события заказов в 3 сервиса сегодня, ожидаешь добавить ещё позже, и часть заботится только о EU-заказах. Выбери топологию exchange.
Failure modes, которые поднимут тебя в 3 ночи
Три ошибки exchange-и-очередей вызывают большинство прод-инцидентов:
Unbound exchange → тихие drop’ы. Это и есть Hook. По умолчанию basic.publish не сообщает producer, была ли сообщение смаршрутизирована. Если ни один binding не совпал, брокер отбрасывает его, а publish всё равно возвращает успех. Фикс — флаг mandatory: выстави его, и несмаршрутизируемое сообщение вернётся producer через basic.return, а не исчезнет, — либо заведи catch-all очередь с #, чтобы ничего не терялось незаметно. Всегда алертуй на счётчик unroutable.
Prefetch (QoS) starvation и взрыв памяти. RabbitMQ пушит сообщения, а prefetch count (basic.qos) ограничивает, сколько неподтверждённых сообщений консьюмер может держать одновременно. Поставь слишком высоко — скажем, 1000 — и один медленный консьюмер хватает огромный backlog, пока быстрые простаивают, голодая; хуже того, эти unacked-сообщения остаются резидентно в памяти брокера, и если обработка застопорится, счётчик unacked растёт безгранично, пока узел не упрётся в memory watermark и не заблокирует всех publisher’ов. Сеньорный дефолт — низкий prefetch (начни с 1, тюнь вверх по метрикам), чтобы работа распределялась честно, а память оставалась ограниченной.
Poison message без DLX → бесконечный цикл переотправки. Консьюмер, который делает nack-with-requeue на сообщении, которое он никогда не сможет обработать (битый payload, баг, на котором он всегда падает), получает то же сообщение обратно, навсегда, пиня воркер. Фикс — dead-letter exchange (DLX): настрой очередь так, чтобы отклонённые или истёкшие сообщения dead-letter’ились, маршрутизируясь в отдельный exchange и карантинную очередь, которую можно осмотреть и переобработать. Без DLX одно poison-сообщение может застопорить весь пайплайн.
Durability и HA: classic, mirrored, quorum
Сообщение настолько в безопасности, насколько надёжна держащая его очередь. Классические очереди живут на одном узле; умрёт он — уйдут и сообщения очереди. Старым ответом были classic mirrored queues (реплики на других узлах), но их задепрекейтили и удалили в RabbitMQ 4.0 — они могли подтвердить сообщение до того, как оно безопасно реплицировано, рискуя потерей при failover. Современный ответ — quorum queues: реплицируемая очередь на Raft, которая подтверждает только когда большинство узлов имеет сообщение. Трейдофф конкретен — репликация на 3 узла держит примерно 30K сообщений/сек для сообщений в 1KB, ниже, чем нереплицируемая классическая очередь, в обмен на отсутствие потери данных, когда узел падает. Для всего, что нельзя позволить себе потерять, это та сделка, на которую идёт сеньор.
Producer публикует в topic exchange, но очередь только что добавленного консьюмера не получает ничего, при этом нигде нет ошибок. Какова наиболее вероятная причина?
Нужно, чтобы поздно подключившиеся консьюмеры могли replay-нуть события последней недели с начала. Какая система подходит и почему?
Расставь путь, который сообщение проходит от producer до консьюмера в RabbitMQ:
- 1 Producer публикует сообщение с routing key в именованный exchange
- 2 Exchange применяет правило своего типа к routing key против своих bindings
- 3 Каждый совпавший binding доставляет независимую копию в свою очередь
- 4 Брокер пушит сообщение консьюмеру, в пределах лимита prefetch (QoS)
- 5 Консьюмер подтверждает (ack); брокер удаляет эту копию из очереди
- 01Коллега уверен, что RabbitMQ и Kafka — взаимозаменяемые очереди сообщений. Объясни архитектурное различие и когда что подходит.
- 02Почему высокий prefetch count вызывает и несправедливость, и аварии, и какой безопасный дефолт?
Определяющий ход RabbitMQ в том, что producers публикуют в exchange, а не в очередь, и тип exchange плюс routing key сообщения плюс bindings решают, какие очереди получат независимую копию — возможно, никакие. Direct маршрутизирует по точному совпадению ключа, fanout вещает в каждую забинженную очередь, topic матчит точечные паттерны с * и #, а headers матчит атрибуты заголовков. Это модель smart-broker, dumb-consumer с push: брокер маршрутизирует, пушит, отслеживает per-message ack и удаляет по consume — противоположность pull-логу Kafka с dumb-broker, smart-consumer, который хранит сообщения для replay. Размен — богатство маршрутизации и субмиллисекундная latency при десятках тысяч сообщений/сек на очередь (RabbitMQ) против replay и примерно миллиона сообщений/сек на партицию (Kafka). Прод-сбои предсказуемы: unbound exchange тихо отбрасывает сообщения, пока ты не выставишь флаг mandatory или catch-all binding; слишком высокий prefetch голодит быстрых консьюмеров и растит unacked-память, пока publisher’ы не заблокируются; poison-сообщение без dead-letter exchange циклится вечно. А для durability classic mirrored queues исчезли начиная с RabbitMQ 4.0 — quorum queues реплицируют через Raft и подтверждают только по большинству, стоя throughput (около 30K сообщений/сек реплицировано на три узла), чтобы не терять данные при failover. Сперва выбери правильный тип exchange и bindings; всё ниже по потоку зависит от того, что маршрутизация верна.