RabbitMQ + Rebus: как настроить надёжную доставку сообщений между микросервисами
В этой статье про микросервисные коммуникации. Про, так называемые, best practice, то есть "лучшие практики". Другими словами, о том как настроить механизмы отправки и получения сообщений в микросервисной архитектуре.
Введение
Когда мы говорим о микросервисной архитектуре, рано или поздно встаёт вопрос: как сервисы общаются друг с другом? Есть два основных подхода, синхронный (synchronous, REST, gRPC) и асинхронный (asynchronous, message queue). Асинхронный подход кажется проще: отправили сообщение в очередь и забыли о нём. Но на деле всё сложнее.
Представьте такой сценарий: Ваш сервис заказов отправляет сообщение в RabbitMQ для уведомления сервиса доставки о новом заказе. В идеальном мире сообщение дошло бы сразу и безопасно. Но реальность беспощадна:
Первый сценарий: сервис отправил сообщение, но упал прямо после отправки. Было ли сообщение действительно отправлено в RabbitMQ? Кто знает.
Второй сценарий: сообщение успешно отправилось в очередь (queue), но сервис-получатель был в этот момент на перезагрузке. Сообщение заждалось в очереди, а потом сервис поднялся и обработал его. Хорошо. Но что если во время обработки произошла ошибка? Сообщение вернулось в очередь? Потеряется? Обработается дважды?
Третий сценарий, самый коварный: сообщение успешно обработалось, но отправка подтверждения (acknowledgement) в RabbitMQ упала. Очередь решила, что всё пошло не так, и переотправила сообщение. Теперь заказ создался дважды.
Вот тут и приходит на помощь надёжная доставка сообщений (reliable message delivery). И именно для этого многие выбирают Rebus поверх RabbitMQ — это библиотека, которая берёт на себя всю эту сложность.
В этой статье мы разберёмся, как настроить RabbitMQ и Rebus так, чтобы сообщения доходили туда, куда надо, и обрабатывались корректно, даже когда всё падает и ломается.
Почему Rebus поверх RabbitMQ
Вы можете спросить: зачем нужен Rebus, если есть встроенная библиотека RabbitMQ.Client? Отличный вопрос. Давайте разберёмся.
RabbitMQ.Client — это низкоуровневый драйвер (driver) для работы с брокером сообщений (message broker) RabbitMQ. Это просто транспорт: открыли соединение, отправили байты, получили ответ. Всё остальное — ваша забота. Retry-логика (retry logic), обработка ошибок (error handling), гарантии доставки (delivery guarantees) — вы пишете сами.
Rebus — это абстракция (abstraction) более высокого уровня поверх RabbitMQ.Client (и других транспортов, кстати). Это фреймворк для асинхронного обмена сообщениями (asynchronous messaging framework), который даёт вам:
Первое, встроенная поддержка retry-политик (retry policies). Если сообщение не обработалось, Rebus автоматически переотправит его через какое-то время, с экспоненциальной задержкой (exponential backoff). Вы не пишете цикл в коде — просто конфигурируете время между попытками.
Второе, управление очередями (queue management). Rebus сам создаст exchange и queue'ы в RabbitMQ нужной конфигурации. Вы не прописываете это вручную через RabbitMQ Management Console (Управление RabbitMQ).
Третье, поддержка saga паттерна (saga pattern). Если нужно оркестрировать длительный процесс (long-running process) с несколькими шагами, Rebus помогает координировать сообщения между сервисами.
Четвёртое, dead-letter queue (DLQ) из коробки. Сообщение, которое не смогло обработаться после N попыток, автоматически уходит в отдельную очередь для анализа.
Пятое, гибкость транспорта. Хотите завтра переключиться с RabbitMQ на Azure Service Bus или на папку на диске (для тестов)? Достаточно изменить конфиг — весь остальной код остаётся прежним.
Архитектурно это выглядит так:

Rebus берёт на себя всю боль, связанную с надёжностью (reliability), оставляя вам заботу только о бизнес-логике (business logic).
Базовая настройка
Перейдём от теории к практике. Разберём, как подключить Rebus к RabbitMQ в проекте на ASP.NET Core.
Первым делом добавляем нужные NuGet-пакеты (NuGet packages): Rebus, Rebus.RabbitMq и Rebus.ServiceProvider (последний нужен для интеграции с DI-контейнером (dependency injection container) ASP.NET Core).
Дальше настраиваем Rebus в Program.cs или в Startup.cs, в зависимости от того, какой подход вы используете в проекте:
Конфигурация начинается с вызова services.AddRebus(...), куда передаётся конфигуратор (configurator). Внутри него указываем несколько ключевых блоков.
Транспорт (transport): здесь мы говорим Rebus использовать RabbitMQ и передаём строку подключения (connection string), а также имя основной очереди (input queue) — это очередь, в которую будут падать входящие сообщения для конкретного сервиса.
Маршрутизация (routing): указываем, каким образом Rebus должен определять, куда отправлять сообщение того или иного типа. Обычно используется простая маршрутизация по типу сообщения (type-based routing) — например, все сообщения типа OrderCreated уходят в очередь сервиса доставки.
Сериализация (serialization): по умолчанию Rebus использует JSON, но это настраивается, если нужен другой формат.
После настройки конфигуратора вызываем AddRebusHandler (или похожий метод, в зависимости от версии) для регистрации обработчиков сообщений (message handlers) — классов, реализующих интерфейс IHandleMessages<T>.
На стороне RabbitMQ, когда сервис запускается, Rebus автоматически создаёт нужный exchange и очередь (queue), если их ещё не существует. Не нужно вручную заходить в RabbitMQ Management Console и создавать их.
Для проверки, что всё работает, достаточно отправить тестовое сообщение через IBus.Send(...) или IBus.Publish(...) (в зависимости от того, точка-точка это или публикация события), и посмотреть, что обработчик на другой стороне его получил.
Дальше в статье мы усложним эту базовую настройку и добавим гарантии доставки, о которых говорили во введении.
Гарантии доставки
Теперь переходим к самому важному — как сделать так, чтобы сообщения не терялись и обрабатывались корректно, даже когда что-то идёт не так.
At-least-once delivery
Rebus по умолчанию работает по модели at-least-once delivery (доставка хотя бы один раз). Это означает следующее: Rebus гарантирует, что сообщение будет доставлено и обработано минимум один раз, но не гарантирует, что оно будет обработано ровно один раз (exactly-once). Разница принципиальная.
Как это работает технически: подписчик (subscriber) получает сообщение из очереди, но не удаляет его сразу. Сначала выполняется обработчик (handler). Если обработка прошла успешно, Rebus отправляет подтверждение (acknowledgement, или ack) в RabbitMQ, и только после этого сообщение удаляется из очереди. Если обработчик упал с исключением (exception) или сервис перезапустился до отправки подтверждения, RabbitMQ считает, что сообщение не обработано, и возвращает его в очередь для повторной попытки.

Отсюда следует важный вывод: возможны дубли (duplicates). Если обработчик выполнил бизнес-логику (например, создал заказ в базе данных), но упал до отправки подтверждения, то сообщение придёт снова, и обработчик выполнится второй раз. К этому мы вернёмся в разделе про идемпотентность (пункт 5).
Retry-политики и экспоненциальная задержка
Когда обработка сообщения завершается ошибкой, Rebus не сразу сдаётся. Настраивается количество попыток (retry count) и стратегия задержки между ними (backoff strategy). Например, можно настроить пять попыток с экспоненциальным увеличением интервала: 1 секунда, 2 секунды, 4 секунды, 8 секунд, 16 секунд. Это защищает от ситуации, когда временная проблема (например, недоступность базы данных на пару секунд) не приводит к немедленной потере сообщения.
Настройка retry-политики (retry policy) в Rebus делается через конфигуратор при регистрации Rebus, где указывается максимальное количество попыток (MaxDeliveryAttempts) и другие параметры.
Dead-letter queue (DLQ)
Что происходит, если сообщение не смогло обработаться после всех попыток? Оно не исчезает бесследно, а перемещается в специальную очередь — dead-letter queue (очередь недоставленных сообщений). Это своего рода "кладбище" сообщений, которые Rebus не смог доставить успешно.
DLQ — критически важный инструмент для диагностики (diagnostics). Вместо того чтобы сообщение просто терялось, вы можете:
Первое, посмотреть, какие сообщения не обработались и почему (обычно вместе с сообщением сохраняется информация об ошибке).
Второе, вручную переотправить (replay) сообщение после исправления проблемы.
Третье, настроить мониторинг (monitoring) на количество сообщений в DLQ — если оно растёт, это сигнал, что что-то в системе сломалось.
В следующем разделе разберёмся, как обрабатывать дубли сообщений на стороне подписчика — то есть, как сделать обработку идемпотентной.
Идемпотентность на стороне подписчика
Как мы выяснили в предыдущем разделе, at-least-once delivery означает, что дубли сообщений — это не баг, а нормальное, ожидаемое поведение системы. Значит, обработчик сообщений должен уметь корректно справляться с повторной доставкой одного и того же сообщения. Это свойство называется идемпотентностью (idempotency).
Что такое идемпотентная обработка
Идемпотентная операция — это операция, повторное выполнение которой с теми же входными данными не приводит к нежелательным побочным эффектам (side effects). Если сообщение "создать заказ №123" обработалось дважды, результат должен быть таким же, как если бы оно обработалось один раз: один заказ, а не два.
Дедупликация по идентификатору сообщения
Самый распространённый подход — дедупликация (deduplication) по уникальному идентификатору сообщения (message id). У каждого сообщения в Rebus есть уникальный идентификатор, который можно получить через контекст обработки сообщения (message context).
Логика следующая: перед обработкой сообщения проверяем, не обрабатывали ли мы уже сообщение с таким message id. Если обрабатывали, просто игнорируем повторное сообщение и подтверждаем его получение (acknowledge), не выполняя бизнес-логику заново. Если не обрабатывали, выполняем логику и сохраняем идентификатор сообщения как обработанный.
Реализация с EF Core и PostgreSQL
Раз уж мы используем EF Core и PostgreSQL, реализовать дедупликацию можно через отдельную таблицу, например ProcessedMessages, с полями: идентификатор сообщения (message id), дата обработки (processed at), и, возможно, тип сообщения (message type) для удобства диагностики.
Важный нюанс: проверку "обработано ли уже сообщение" и сохранение бизнес-данных (например, создание заказа) нужно выполнять в одной транзакции базы данных (database transaction). Иначе возможна гонка (race condition): между проверкой и сохранением может проскочить дубль сообщения, обработанный параллельно.
Альтернативный подход: идемпотентность на уровне бизнес-логики
Иногда дедупликация по message id избыточна, если бизнес-логика сама по себе идемпотентна. Например, операция "установить статус заказа в Delivered" идемпотентна по своей природе: сколько раз её ни выполни, результат один и тот же. В таких случаях отдельная таблица для дедупликации не нужна — достаточно использовать UPSERT-подход (upsert, insert-or-update) или проверку текущего состояния перед изменением.
Какой подход выбрать, зависит от природы операции: для создания сущностей (create-операций) обычно нужна дедупликация по message id, а для операций изменения состояния (update-операций) часто достаточно идемпотентности на уровне бизнес-логики.
В следующем разделе поговорим о паттерне Outbox — как решить обратную проблему: когда транзакция в базе данных прошла успешно, но сообщение не отправилось.
Паттерн Outbox
В пятом разделе мы разобрались, как справляться с дублями сообщений. Теперь разберём обратную, не менее коварную проблему: что если транзакция в базе данных прошла успешно, а сообщение в RabbitMQ отправить не удалось? Или наоборот — сообщение отправилось, а транзакция откатилась?
Проблема двух систем
Представьте типичный сценарий: сервис заказов создаёт запись о заказе в PostgreSQL и сразу отправляет сообщение OrderCreated в RabbitMQ через Rebus. Это две разные операции с двумя разными системами, и они не могут быть атомарными (atomic) вместе. Возможны варианты:
Первый: транзакция в базе данных закоммитилась (committed), но сервис упал до отправки сообщения. Заказ создан, но сервис доставки никогда не узнает об этом.
Второй: сообщение успешно отправилось, но транзакция в базе данных откатилась (rolled back) из-за ошибки. Сервис доставки получит уведомление о заказе, которого на самом деле не существует.
Как Outbox решает эту проблему
Идея паттерна Outbox (outbox pattern) простая: вместо того чтобы отправлять сообщение напрямую в RabbitMQ, мы сохраняем его в отдельную таблицу в той же базе данных, в той же транзакции, что и основные бизнес-данные. Например, таблица OutboxMessages с полями: идентификатор, тип сообщения, содержимое (payload) в формате JSON, дата создания, статус отправки (отправлено/не отправлено).
Поскольку запись в таблицу заказов и запись в OutboxMessages происходят в одной транзакции EF Core, они либо обе применятся, либо обе откатятся. Атомарность (atomicity) обеспечена — но уже не между базой данных и RabbitMQ, а внутри самой базы данных, что тривиально с точки зрения ACID-транзакций.

Дальше в дело вступает отдельный фоновый процесс (background process, например, IHostedService или BackgroundService в ASP.NET Core), который периодически читает неотправленные записи из OutboxMessages, публикует их через Rebus в RabbitMQ, и только после успешной отправки помечает запись как отправленную (или удаляет её).
Если фоновый процесс упадёт до того, как пометит сообщение отправленным, при следующем запуске он просто отправит его снова. Это возвращает нас к идемпотентности на стороне подписчика — но зато мы гарантированно не теряем сообщения.
Паттерн Inbox
Outbox решает проблему на стороне отправителя. Но есть симметричная проблема на стороне получателя: что если сервис-подписчик получил сообщение, начал обрабатывать бизнес-логику, записал что-то в свою базу данных — а затем упал до отправки подтверждения (acknowledgement) в RabbitMQ? Сообщение вернётся в очередь и придёт снова, и мы опять упрёмся в дубли, о которых говорили в разделе про идемпотентность.
Паттерн Inbox (inbox pattern) — это, по сути, тот же принцип, что и Outbox, только в обратную сторону. При получении сообщения сервис-подписчик сначала сохраняет его в таблицу InboxMessages (в той же транзакции, что и бизнес-данные) вместе с признаком "обработано". Если запись с таким идентификатором сообщения уже есть в таблице, значит, сообщение уже обрабатывалось, и повторную обработку можно спокойно пропустить.

По сути, Inbox — это более формализованный и надёжный вариант дедупликации по message id, о которой мы говорили в разделе 5, но с гарантией атомарности за счёт единой транзакции с бизнес-данными.
Outbox + Inbox вместе
Когда оба паттерна используются вместе, получается сквозная гарантия доставки и обработки: отправитель гарантированно не теряет сообщение (Outbox), а получатель гарантированно обрабатывает его ровно один раз с точки зрения бизнес-эффекта (Inbox), даже если технически сообщение доставляется несколько раз (at-least-once).
Эта комбинация — один из самых надёжных способов построить обмен сообщениями (messaging) между микросервисами, но она добавляет сложность (complexity) и требует фонового процесса для Outbox. Стоит ли она того — зависит от критичности бизнес-процесса. Для уведомлений, где потеря одного сообщения не критична, это может быть избыточно. Для финансовых операций или заказов — почти всегда оправдано.
Мониторинг доставки
Настроить надёжную доставку сообщений — это только половина дела. Вторая половина — понимать, что происходит с сообщениями в реальном времени, не дожидаясь, пока пользователи начнут жаловаться на пропавшие заказы. Здесь на помощь приходят Prometheus и ELK.
Что важно мониторить
Первое, размер очередей (queue size). RabbitMQ отдаёт метрики о количестве сообщений в каждой очереди через свой встроенный плагин мониторинга (RabbitMQ Prometheus plugin). Если очередь начинает расти — это сигнал, что подписчик не успевает обрабатывать сообщения или вовсе не работает.
Второе, размер dead-letter queue. Как мы обсуждали в разделе про гарантии доставки, рост DLQ — прямой индикатор проблем в системе. Стоит настроить алерт (alert) в Prometheus/Alertmanager на превышение порогового значения (threshold), например, больше 10 сообщений в DLQ за 5 минут.
Третье, время обработки сообщения (processing time). Полезно замерять, сколько времени проходит с момента получения сообщения обработчиком до отправки подтверждения. Резкий рост этого показателя часто указывает на проблему с внешней зависимостью (external dependency) — например, база данных стала отвечать медленнее.
Четвёртое, количество retry (retry count). Если сообщения массово уходят на повторную обработку, это стоит увидеть на графике раньше, чем сообщения начнут попадать в DLQ.
Как это выглядит на практике
Rebus поддерживает интеграцию с системами метрик через middleware (промежуточное ПО), куда можно добавить свой код для инкрементирования счётчиков (counters) Prometheus при разных событиях: получено сообщение, обработано успешно, обработка с ошибкой, отправлено в DLQ.
Дальше эти метрики визуализируются через Grafana (если она есть в вашем стеке) в виде дашборда (dashboard): графики размера очередей, DLQ, retry, времени обработки — всё в одном месте.
Логирование в ELK
Помимо метрик, критически важно логировать сам факт обработки сообщения — как минимум, идентификатор сообщения, тип, результат обработки (успех/ошибка) и, если ошибка, её текст и стек вызовов (stack trace). Эти логи стекаются в Elasticsearch через Logstash или Filebeat, и в Kibana можно быстро найти историю конкретного сообщения по его идентификатору — что особенно полезно, когда нужно разобраться, почему конкретный заказ не дошёл до сервиса доставки.

Комбинация метрик (что происходит сейчас, в среднем) и логов (что случилось с конкретным сообщением) даёт полную картину надёжности системы обмена сообщениями.
Итоги
Мы прошли путь от постановки проблемы до полноценной картины надёжной доставки сообщений между микросервисами с помощью RabbitMQ и Rebus. Подведём итог в виде чек-листа, который можно использовать как отправную точку при проектировании своей системы обмена сообщениями.
Чек-лист надёжной доставки
- Используйте Rebus (или аналогичный фреймворк) вместо низкоуровневого RabbitMQ.Client — это избавит от необходимости писать retry-логику, управление очередями и обработку ошибок вручную.
- Настройте разумные retry-политики с экспоненциальной задержкой (exponential backoff), чтобы временные сбои не приводили к немедленной потере сообщений.
- Обязательно мониторьте dead-letter queue — это ваш ранний индикатор системных проблем.
- Реализуйте идемпотентность на стороне подписчика. At-least-once delivery означает, что дубли — это норма, а не исключение.
- Для критичных бизнес-процессов (финансы, заказы) используйте паттерны Outbox и Inbox вместе — это даёт сквозную гарантию доставки и обработки без потерь и дублей на уровне бизнес-логики.
- Настройте мониторинг через Prometheus (метрики) и ELK (логи), чтобы видеть проблемы с доставкой до того, как о них сообщат пользователи.
Заключение
Надёжная доставка сообщений — это не разовая настройка, а комплекс решений, каждое из которых закрывает свою часть проблемы. Rebus снимает значительную часть инфраструктурной сложности, но архитектурные решения — идемпотентность, Outbox, Inbox, мониторинг — остаются на вашей стороне. Инвестиции в эти механизмы окупаются в первую же ночь, когда что-то в системе упадёт, а данные при этом не потеряются.
Полезные ссылки:
Мои видео
Boosty.to | YouTube | Yandex.Дзен | RuTube | VK Video
Еще вопросы и еще ответы
На странице Вопросы и ответы (FAQ) есть другие ответы на другие вопросы. Простые и сложные, на разные темы.