Очереди сообщений и брокеры

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

Аудитория: архитекторы, техлиды, backend- и platform-разработчики.

Статус: устоявшаяся инфраструктура; канонический справочник — Грегор Хопе и Бобби Вульф, Enterprise Integration Patterns (2003); поколение брокеров — IBM MQ (1993), JMS (2001), AMQP/RabbitMQ (2007); поколение распределённых логов — Apache Kafka (2011), Pulsar (2016), NATS JetStream (2020).

Не путать с: событийно-ориентированной архитектурой (стиль проектирования; брокер — лишь транспорт для него), CQRS и Event Sourcing (паттерны работы с данными, которые могут опираться на брокера), ESB из SOA («умная шина» с бизнес-логикой; брокер — «глупый» транспорт).

Общее

Очередь сообщений (message queue) — механизм асинхронного обмена, при котором отправитель не передаёт данные получателю напрямую, а помещает сообщение — единицу данных с полезной нагрузкой и метаданными — в именованное хранилище, откуда получатель забирает его позже. Брокер сообщений (message broker) — инфраструктурный компонент, который принимает сообщения от отправителей, хранит их и доставляет получателям, реализуя очереди, маршрутизацию и гарантии доставки. Участников традиционно называют producer (издатель, отправитель) и consumer (потребитель, получатель); точка подключения — очередь или топик (канал в терминологии Enterprise Integration Patterns).

Ключевое отличие от вызова функции или HTTP-запроса — асинхронность и устранение временно́й связности. При синхронном вызове обе стороны обязаны быть доступны одновременно: получатель упал — отправитель стоит и ждёт тайм-аут. С очередью отправитель пишет сообщения, даже когда получатель недоступен, а получатель обрабатывает их, даже когда отправитель уже завершился. Обмен через брокера расшатывает связность сразу по нескольким осям: отправитель не знает адресов и количества получателей (пространственная связность), не зависит от их доступности и скорости (временна́я), а контракт сводится к схеме сообщения, а не к API каждого получателя. Подробнее об этом — в статье о событийно-ориентированной архитектуре; здесь брокер рассматривается как инфраструктура.

Зачем системе очередь — четыре базовые выгоды, каждая из которых самодостаточна:

  • Развязка (decoupling). Производители и потребители знают только схему сообщения и имя канала, но не друг друга. Новый потребитель подключается без правок у производителя; отказ или обновление одного компонента не касаются остальных. В микросервисах это главный способ сохранить автономность сервисов.
  • Буферизация пиковой нагрузки (load leveling). Производители могут создавать сообщения быстрее, чем потребители их обрабатывают, — и это нормально: брокер накапливает всплеск и отдаёт его потребителям с той скоростью, которую те выдерживают. Чёрная пятница: миллион заказов в час пишется в очередь, а обработчик закрытия чеков выбирает их стабильными ста тысячами в час. Пик сглаживается, потребители масштабируются под среднюю нагрузку, а не под пиковую.
  • Асинхронная обработка. Тяжёлые сопутствующие работы — уведомления, аналитика, генерация документов — снимаются с критического пути пользователя. Сервис принимает заказ, публикует событие за миллисекунды и отвечает клиенту; всё остальное происходит потом и параллельно.
  • Надёжная доставка. Брокер переживает отказ потребителя (сообщения ждут восстановления), отказ соединения (повторная доставка) и при настройке durability — собственный отказ узла (репликация сообщений). Временно недоступный участник превращается из аварии в задержку.

Когда очередь избыточна. В монолите вызов метода в одном процессе на порядки дешевле и надёжнее сетевого обмена: он не падает по сетевым причинам, не сериализует данные и не требует идемпотентности. Внутрипроцессная шина событий или обычный вызов закрывают те же потребности без брокера; очередь между частями одного приложения — почти всегда преждевременная сложность. Не помогает очередь и там, где нужен немедленный ответ пользователю: «показать корзину» — синхронный сценарий, и асинхронный обмен лишь добавит задержку. Наконец, маленькой системе из двух-трёх сервисов брокер даёт операционную цену (развернуть, мониторить, обновлять), непропорциональную выгоде. Практическое правило: очередь появляется, когда есть хотя бы два из трёх условий — несколько независимых потребителей одного потока данных, пиковая нагрузка, которую обработчики не выбирают синхронно, или требование доставить данные через границы, которые могут падать независимо.

Историческая рамка. Очереди — старейшая инфраструктура распределённых систем: корпоративные брокеры появились раньше и HTTP, и микросервисов. IBM MQ (изначально MQSeries, 1993) обеспечивал гарантированную доставку транзакционных сообщений в банках; Microsoft MSMQ (1997) принёс очередь в Windows-стек; Java Message Service (2001) стандартизовал API для Java-мира. Книга Грегора Хопе (Gregor Hohpe) и Бобби Вульфа (Bobby Woolf) Enterprise Integration Patterns (2003) дала канонический словарь — канал сообщений, издатель, подписчик, конкурентный потребитель, dead letter channel, — которым до сих пор описывают любую очередь. Открытый протокол AMQP и брокер RabbitMQ (2007) сделали технологию общедоступной; Apache Kafka (LinkedIn, 2011; авторы — Джей Крепс, Неха Нархеде и Рао Джун) сменил парадигму с «брокера» на «распределённый лог»; Apache Pulsar (Yahoo, 2016) и NATS JetStream (2020) продолжили линию логов, а облачные сервисы (Amazon SQS/SNS, Google Pub/Sub, Azure Service Bus) сделали очередь коммунальной утилитой.

ГодВехаЗначение
1993IBM MQSeriesПромышленный брокер с гарантированной доставкой
1997Microsoft MSMQОчередь как часть платформы
2001Java Message Service (JMS)Стандартный API: Queue и Topic как модели первого класса
2003Hohpe и Woolf, Enterprise Integration PatternsСловарь паттернов обмена сообщениями
2007AMQP 0-9-1, RabbitMQОткрытый протокол и open-source брокер
2011Apache KafkaРаспределённый лог вместо классического брокера
2016Apache PulsarЛог с разделением хранения и обслуживания
2017Kafka 0.11 (KIP-98)Идемпотентный продюсер и транзакции
2020NATS JetStreamПерсистентный поток сообщений в NATS

Терминологическая оговорка, как и в статье об EDA: очередь — транспорт, а не архитектура. Брокер может возить что угодно — события, команды, документы, куски файлов, — и сам по себе не делает систему событийной или развязанной. Систему развязывает семантика обмена: кто кого знает, кто кого ждёт, что считается обязательством доставки. Очередь лишь даёт материал, из которого такая семантика строится.

Модели доставки

Две фундаментальные модели того, кто и сколько раз получает сообщение, зафиксированы ещё в JMS как Queue и Topic; в терминах EIP это point-to-point канал и publish-subscribe канал.

Очередь (queue, point-to-point). Сообщение попадает в очередь и достаётся ровно одному потребителю. Несколько потребителей могут слушать одну очередь — тогда они становятся конкурирующими потребителями (competing consumers): брокер раздаёт сообщения между ними, и каждое сообщение обрабатывает один — тот, кто освободился первым. Это встроенная балансировка нагрузки: добавление экземпляра потребителя увеличивает пропускную способность без какой-либо настройки. Сообщение покидает очередь после подтверждения обработки (ack) — это модель «задача из бэклога»: пул воркеров разбирает работу.

graph LR P1["Producer 1"] --> Q[("Queue")] P2["Producer 2"] --> Q Q --> C1["Consumer 1"] Q --> C2["Consumer 2"] Q --> C3["Consumer 3"]

Тема (topic, publish/subscribe). Сообщение публикуется в тему, и брокер доставляет его копию каждому подписчику — broadcast. Подписчики не конкурируют: каждый получает всё и обрабатывает по-своему. Один факт OrderPlaced одновременно уходит биллингу (выставить счёт), складу (зарезервировать), рассылке (письмо) и аналитике (записать в витрину). Подписчики — часто целые команды с независимыми темпами работы, и pub/sub снимает координацию между ними: новые подписчики добавляются, старые уходят, издатель не замечает ни тех, ни других.

graph LR P["Publisher"] --> T[("Topic")] T --> S1["Subscriber: биллинг"] T --> S2["Subscriber: склад"] T --> S3["Subscriber: рассылка"]

Различие удобно фиксировать вопросом: «если добавить второго потребителя, удвоится ли работа?». В модели queue — нет: сообщение уйдёт то одному, то другому, суммарная работа прежняя. В модели pub/sub — да: каждый подписчик выполняет свою работу над каждой копией. Отсюда и разные сценарии: queue — распределение работы (обработка заданий, письма, картинки), pub/sub — уведомление о фактах (доменные события, EDA).

Гибриды. Чистые модели в современных системах комбинируются, и часто одна реализует другую поверх себя:

  • RabbitMQ — очередь первична, pub/sub строится из обменников (exchange): продюсер публикует сообщение в exchange, тот по правилам маршрутизации (routing key, binding) раскладывает копии в несколько очередей — по одной на каждого подписчика. Каждый подписчик со своей очередью получает всё; внутри же каждой очереди работают конкурирующие потребители. Две модели складываются в композицию.
  • Kafka — лог первичен, обе модели строятся над ним через consumer groups (подробно — в разделе об упорядоченности): одна группа на партицию даёт семантику queue (сообщение партиции читает один член группы), разные группы — семантику pub/sub (каждая группа читает весь поток независимо).
  • Облачные пары — Amazon SNS+SQS: тема SNS раздаёт копии в очереди SQS подписчиков, объединяя обе модели явным образом.

Выбор модели — выбор семантики, а не технологии: почти каждый брокер умеет обе. Ошибка модели видна сразу: конкурентная очередь там, где каждый факт нужен всем, приводит к «диспетчеру», который вручную перенаправляет сообщения в несколько мест; pub/sub там, где нужно распределить работу, — к N-кратному дублированию обработки.

Брокер vs распределённый лог

За фасадом «брокера сообщений» скрываются два принципиально разных устройства, и большинство проектных споров «RabbitMQ или Kafka» — на самом деле спор двух архитектур хранилища сообщений.

Брокер (RabbitMQ, ActiveMQ, Amazon SQS) хранит сообщение как единицу работы. Жизненный цикл: сообщение принято → маршрутизировано в очередь → выдано потребителю → подтверждено (ack) → удалено. Брокер «умный»: маршрутизация по правилам (exchanges, routing keys, заголовки), приоритеты сообщений, TTL, отложенная доставка, богатые ack/nack-семантики. Взаимодействие поштучное: каждый consumer получает отдельное сообщение и отвечает на него. Плата — сообщение живёт, пока не обработано: после ack его больше нет, перечитать нельзя; глубокая история потока не хранится.

graph LR P["Producer"] -->|"сообщение"| B[("Broker")] B -->|"выдача"| C["Consumer"] C -->|"ack"| B B -.->|"после ack — удалено"| X["хранилище пусто"]

Распределённый лог (Kafka, Pulsar, NATS JetStream) хранит поток как журнал (append-only log): сообщения дописываются в конец упорядоченной записи и не удаляются при чтении. Потребитель читает журнал со своей позиции — offset — и сам двигает её вперёд; никакое чтение не меняет данные. Журнал живёт по политике ретеншена (retention): по времени (данные за 7 дней) или размеру. Отсюда уникальные свойства: повторное чтение (consumer может отмотать offset и перечитать поток — для исправления бага, перестройки проекции, восстановления после сбоя), много независимых читателей одного потока (каждая consumer group — своя позиция), а сам поток — это долговременная запись фактов, на которой строятся CQRS-проекции и Event Sourcing, стриминговая аналитика и репликация данных. Плата — хранение растёт с потоком, маршрутизация бедна (данных из сообщения в очередь-«на-потребителя» не разложишь), а семантика работы ближе к чтению файла, чем к «получил задачу — подтвердил».

graph LR P1["Producer 1"] --> L[("Log: append-only")] P2["Producer 2"] --> L L --> G1["Group A, offset 42"] L --> G2["Group B, offset 17"] G1 -.->|"replay: сброс offset"| L

Сравнение по существенным осям:

ОсьБрокер (RabbitMQ, ActiveMQ, SQS)Распределённый лог (Kafka, Pulsar, JetStream)
Единица храненияОтдельное сообщениеЗапись в упорядоченном журнале (партиции)
После обработкиУдаляется (ack)Остаётся до истечения ретеншена
Повторное чтениеНет — сообщения больше нетДа — любой consumer может отмотать offset
Независимые читателиОтдельная очередь (копия) на каждогоConsumer groups: своя позиция у каждой
МаршрутизацияБогатая (exchange, binding, приоритеты, TTL)Минимальная (топик + ключ партиции)
ПорядокВ пределах очередиВ пределах партиции
Управление потокомPush: брокер сам раздаёт (prefetch)Pull: потребители читают в своём темпе
ХранениеПропорционально глубине очередиПропорционально всему потоку за ретеншен
Типичные сценарииЗадачи, RPC-подобные команды, уведомления, сложная маршрутизацияПотоки событий, аналитика, CDC, CQRS/ES, replay
Профиль нагрузкиНизкая задержка на сообщение, метрики — глубина очередиВысокий последовательный throughput, метрики — лаг потребителей

Критерий выбора сводится к одному вопросу: нужно ли сообщение после того, как его обработали? Если да — перечитать, перестроить потребителей, дать нескольким независимым командам читать один факт — это лог. Если нет: сообщение — временная единица работы, которую обработали и забыли, а нужна богатая маршрутизация и поштучная семантика — это брокер. На практике зрелые ландшафты используют оба: лог для потока доменных событий, брокер для внутренних задач и интеграций; задача архитектора — не выбрать «правильный продукт», а понять, какое устройство хранилища соответствует каждой задаче.

Гарантии и надёжность

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

Семантики доставки. Что происходит с сообщением при сбое — на стыке «брокер ↔ потребитель» — определяет три классических варианта:

  • At-most-once (не более одного раза) — доставка без гарантии: продюсер отправил и забыл, потребитель подтверждает до обработки. Сообщение может быть потеряно (сбой при передаче или до обработки), но не может быть обработано дважды. Применимо там, где потеря терпима: телеметрия, метрики, «устаревает быстро» данные.
  • At-least-once (по меньшей мере один раз) — гарантия доставки ценой возможных дублей: подтверждение посылается после успешной обработки, и любой сбой (обработчик умер, ack не дошёл, сеть порвалась между обработкой и ack) приводит к повторной выдаче сообщения. Основной режим всех продакшен-конфигураций. Обратная сторона — одно и то же сообщение может быть доставлено и обработано дважды: двойное письмо, двойное списание, дубль в базе.
  • Exactly-once (ровно один раз) — сквозная гарантия «обработано ровно один раз» без потерь и дублей. Строго говоря, сквозная exactly-once доставка между произвольными системами не реализуема: сбой может случиться между любыми двумя шагами распределённой операции, а атомарности «обработка + ack + внешние сайд-эффекты» между разнородными системами нет. Что существует — точно ограниченные конструкции: внутри Kafka транзакции и идемпотентный продюсер (KIP-98, 2017) дают «ровно один раз в потоке Kafka»; атомарность «прочитал-обработал-записал» достижима там, где чтение и запись живут в одной транзакционной системе.

Практическая формула, которую стоит запомнить: exactly-once на практике = at-least-once + идемпотентный потребитель. Поскольку повторы неизбежны, потребитель обязан безопасно обрабатывать дублирование — и тогда семантика «ровно один раз» достигается на уровне эффекта, а не доставки. Идемпотентность строится явным ключом дедупликации (messageId/eventId в таблице обработанных), естественными ключами домена («платёж P-10427 уже проведён») или upsert-записями вместо insert. Проектировать идемпотентность дешевле и надёжнее, чем верить рекламным гарантиям транспорта.

Durability. Переживание отказов настраивается на трёх уровнях. Сообщение может быть помечено персистентным (delivery mode persistent в RabbitMQ) — записываться на диск, а не жить в памяти. Очередь/подписка должны быть durability-устойчивыми — переживать рестарт брокера. Сам брокер реплицируется: quorum-очереди RabbitMQ копируются на несколько узлов кластера; в Kafka партиция реплицируется, а запись подтверждается при min.insync.replicas — записи, подтверждённые с недостаточным числом синхронных реплик, рискуют потерей при отказе. Классическая ошибка — персистентное сообщение в transient-очередь: механизм durability работает только когда все звенья настроены согласованно.

Ack, nack, reject. Ручное подтверждение — основа at-least-once: потребитель получает сообщение, обрабатывает и только потом подтверждает. Если обработка невозможна, потребитель явно отказывается: nack с requeue: true возвращает сообщение в очередь (переобработать позже — например, временная ошибка зависимости), nack/reject без requeue выводит сообщение из очереди навсегда — либо в dead-letter, либо в никуда. Молчание тоже сигнал: закрытое соединение с неподтверждёнными сообщениями брокер трактует как отказ потребителя и выдаёт сообщения повторно. Отсюда правило: авто-ack допустим только для терпимых к потере потоков; бизнес-сообщения подтверждаются вручную и только после завершения обработки, включая запись в собственную базу.

Dead-letter queue и retry-политики. Что делать с сообщением, которое раз за разом не обрабатывается? Обрабатывать его вечно — значит заблокировать очередь (см. poison messages ниже); выбросить — потерять данные. Промежуточное решение — dead-letter queue (DLQ, в терминах EIP — dead letter channel): отдельная очередь, куда брокер или приложение перемещает сообщения после исчерпания попыток. DLQ — не мусорка, а рабочий инструмент: сообщения в ней видны, их считают, мониторят и разбирают — исправив баг, переобрабатывают вручную или пакетно.

Retry-политики выстраиваются лестницей: немедленный ретрай (сбой сети — повторить сразу; недорого, но бесполезен против систематической ошибки), отложенный ретрай с backoff (экспоненциальная задержка между попытками, тех же принципов, что retry в отказоустойчивости; в RabbitMQ реализуется цепочками retry-очередей с TTL, в Kafka — retry-топиками), и потолок попыток — после N неудач сообщение уходит в DLQ. Пауза между повторами даёт отказавшей зависимости время восстановиться; без неё очередь превращается в машину генерации бесполезной нагрузки.

Poison messages. «Ядовитое» сообщение — то, которое стабильно валит или зависает обработчик: некорректная схема, неизвестный формат, данные, нарушающие инварианты, или полезная нагрузка, на которой обработчик паникует. Отличается от временного сбоя тем, что повторение не помогает никогда. Без DLQ и потолка попыток poison message с requeue: true циркулирует «выдан → упал → возвращён → выдан» — либо навсегда, либо до смерти очереди; с конкурирующими потребителями способна положить весь пул воркеров. Контрмеры — валидация схемы на входе, потолок попыток с DLQ и алерт на глубину DLQ: сообщение, попавшее туда, — это, как правило, инцидент либо баг, требующий разбора, а не фон.

Упорядоченность и партиционирование

Порядок обработки сообщений — требование, которое формулируется легко («обрабатывайте в порядке поступления»), а в распределённой системе исполняется дорого. Глобальный порядок на всём потоке требует единственной упорядоченной точки — одной очереди с одним потребителем, — что противоречит и масштабированию, и отказоустойчивости. Поэтому все практические системы гарантируют порядок локально: в пределах одной очереди или одной партиции.

Партиционирование. Поток (топик) делится на партиции — независимые упорядоченные последовательности; Kafka хранит партицию как отдельный лог, RabbitMQ партиций не имеет (их роль играют отдельные очереди). Сообщение попадает в партицию по ключу партиционирования: продюсер вычисляет хэш ключа (orderId, customerId) и записывает сообщение в соответствующую партицию. Отсюда центральное свойство: все сообщения с одним ключом попадают в одну партицию и упорядочены между собой; между ключами порядка нет. Для доменной логики этого обычно достаточно: важен не глобальный порядок всех заказов, а порядок событий конкретного заказа. Выбор ключа — архитектурное решение: ключ должен группировать то, что обязано обрабатываться последовательно, и одновременно равномерно распределять нагрузку; «горячий» ключ (клиент-гигант) концентрирует трафик в одной партиции и ломает масштабирование.

Consumer groups. Механизм Kafka, объединяющий упорядоченность с параллелизмом. Потребители объединяются в группу, и брокер назначает каждой партиции ровно одного потребителя группы: параллелизм равен числу партиций, порядок внутри партиции сохраняется, потому что читает её один член группы. Группа ведёт собственный offset по каждой партиции — и фактически является распределённой очередью поверх лога. Несколько групп читают один топик независимо: биллинг своей группой на offset 100, аналитика догоняет на 40. При изменении состава группы (упал потребитель, добавился новый) происходит rebalance — перераспределение партиций; это плановое, но заметное событие: на время rebalance потребление останавливается, а при неудачных настройках может начаться «пляска» (flapping) — череда rebalance-ей из-за медленных потребителей, не успевающих подтверждать heartbeat.

Prefetch. Механизм RabbitMQ для той же задачи — упорядочить параллелизм потребителя. Потребитель может держать несколько неподтверждённых сообщений одновременно; prefetch (QoS) задаёт этот потолок. Маленький prefetch — строже порядок и меньше throughput (брокер выдаёт по одному); большой — выше производительность, но сообщения, выданные одному потребителю, обрабатываются в его внутренней очереди и при nack возвращаются в очередь, теряя относительный порядок с уже обработанными. Prefetch подбирают измерением: слишком маленький душит throughput, слишком большой ломает порядок и честное распределение между конкурирующими потребителями.

Компромисс «порядок против параллелизма» формулируется одной таблицей решений:

ТребованиеКонфигурацияЦена
Строгий порядок всего потокаОдна очередь/партиция, один потребительНикакого параллелизма; throughput одного обработчика
Порядок в пределах сущностиПартиционирование по ключу сущности + по потребителю на партициюЗависимость от равномерности ключей; горячие ключи
Максимальный параллелизмМного партиций, свободное распределениеПорядок только между соседними сообщениями партиции
Повторная обработка с порядкомОдин потребитель, последовательный offsetВосстановление в реальном времени невозможно

Практический вывод: порядок — это не «фича брокера», а результат трёх согласованных решений: как партиционируется поток (ключ), как масштабируются потребители (группа/prefetch) и что делает потребитель с нарушением порядка (окно переупорядочивания, версионирование событий либо безразличие к порядку). Нарушенный порядок — источник самых коварных багов асинхронных систем: «отмена пришла раньше заказа», «списание по второй версии клиента при уже отклонённой первой». Если порядок критичен, семантика сообщений обязана нести защиту: версионность, метки времени, идемпотентные переходы состояний — а не полагаться на транспорт.

Плюсы

Развязка компонентов. Производители и потребители не знают друг о друге ничего, кроме схемы сообщения и имени канала: добавление, удаление и отказ потребителя не касаются производителя. Это фундамент автономии микросервисов и главный механизм событийной интеграции.

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

Надёжность доставки. At-least-once с durability переживает отказы потребителя, соединения и узла брокера: сообщение не исчезает, а ждёт. Временная недоступность любого участника становится задержкой, а не потерей данных.

Независимое масштабирование. Медленный потребитель добавляет экземпляры, не трогая быстрых продюсеров; конкурирующие потребители и consumer groups дают балансировку нагрузки «из коробки» — без балансировщиков и реестров.

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

Журнал фактов (для логов). Распределённый лог сохраняет поток: события можно перечитать, перестроить проекции, восстановить потребителя с нуля, подключить нового читателя к истории. Поток сообщений превращается в аудит и источник данных для CQRS и Event Sourcing и стриминговой аналитики.

Наблюдаемость потока. Брокер — естественная точка измерений: глубина очередей, лаг потребителей, скорость публикации, доля ошибок. Поток сообщений виден целиком (подробнее — в статье о наблюдаемости), чего нет в клубке синхронных вызовов.

Минусы

Операционная сложность. Брокер — полноценная распределённая система: кластер, репликация, брокерские метрики, версии протоколов и клиентов, ёмкостное планирование. Ей нужны выделенное сопровождение и дежурства; для малой системы цена непропорциональна выгоде.

Итоговая согласованность. Асинхронность означает запаздывание: данные потребителя отстают от данных производителя на миллисекунды, секунды или минуты при отставании (лаг). Сценарий «прочитать свою запись немедленно» через очередь не решается — требуется синхронный запрос или иная маршрутизация, и это надо проектировать явно.

Дубликаты как норма. At-least-once делает повторы не исключением, а фоновым режимом: каждый потребитель обязан быть идемпотентным. Идемпотентность — это дополнительный код, хранилище обработанных идентификаторов, тесты на дублях; «просто включить exactly-once» не работает (см. раздел о гарантиях).

Отладка и трассировка. Стек-трейса между производителем и потребителем не существует: цепочка рассеяна по журналам разных сервисов. Без сквозного correlationId/eventId, протаскиваемого через все топики, и метрик лагов разбор инцидентов превращается в археологию.

Порядок — отдельная инженерия. Гарантии порядка локальны (партиция/очередь), а их композиция с параллелизмом требует осознанных решений: ключи партиционирования, размер групп, prefetch, поведение при rebalance. Ошибки порядка проявляются редко и при специфических тайминга — худший класс багов для воспроизведения.

Стоимость хранения. Распределённые логи хранят весь поток за ретеншен; на больших трафиках это терабайты и заметная статья расходов. Сокращение ретеншена ограничивает окно replay — компромисс, который нужно поддерживать осознанно.

Концентрация зависимости. Брокер, на который завязаны все сервисы, — точка концентрации отказов: его доступность и ёмкость становятся пределом для всей системы. Готовность брокера требует собственных механизмов отказоустойчивости — репликации, кворумов, мониторинга.

Типичные ошибки

Очередь как RPC. Запрос кладётся в очередь, вызывающий ждёт ответа из очереди ответов, коррелируя по ID: формально асинхронно, фактически — синхронный вызов со всеми его проблемами (временна́я связность, ожидание, тайм-ауты), но ещё и с overhead брокера и сложной корреляцией. Если вызывающему нужен немедленный ответ — это сценарий для синхронного API (см. интеграционные паттерны), а не для очереди; очередь — для работы, которую можно отложить.

Отсутствие DLQ. Очередь без dead-letter и потолка попыток — очередь, которую одно ядовитое сообщение способно остановить: циркуляция «выдан → упал → requeue» блокирует хвост, а без алертов никто не узнает. DLQ с мониторингом глубины — не опция, а обязательный контур любой продакшен-очереди.

Неограниченный ретрай. Повторы без backoff и потолка превращают временную ошибку зависимости в шторм повторов, который сам добивает восстанавливающуюся систему (см. retry storm в статье об отказоустойчивости). Правильная лестница: пара немедленных повторов, затем отложенные с экспоненциальным backoff, затем DLQ.

Игнор идемпотентности. «Брокер гарантирует доставку, значит всё надёжно» — забыта вторая половина гарантии: at-least-once означает повторы, и неидемпотентный потребитель на повторе выполняет эффект дважды — двойное письмо, двойное списание, дубль записи. Идемпотентность проектируется до первого инцидента: ключ дедупликации, естественные ключи, upsert.

Очередь как база данных. Очередь используется для хранения и выборки данных: «прочитать последнее, но не удалять», «вернуться к сообщению за прошлую среду», «отфильтровать по полю». Очередь и лог — не хранилища произвольного доступа: первый не умеет перечитывать, второй — произвольно запрашивать (но умеет перечитывать). Для данных — база данных; для потока фактов — лог с осознанным ретеншеном.

Мониторинг сервисов без мониторинга потока. Дашборды показывают здоровье сервисов, но не потока: лаг потребителей растёт, DLQ наполняется, партиция отстала после rebalance — и никто не видит, пока пользователи не заметят «данные запаздывают». Метрики брокера (глубина, лаг, inflight, DLQ) — полноправная часть наблюдаемости системы.

Связанные статьи