Comments 21
создавать connection на каждый SendMessage есть сомнительная идея
Однобокий и технически неграмотный туториал. После прочтения возникает вопросов больше, чем ответов. От таких статей больше вреда, чем пользы. В официальном мануале итак почти все разжевано доступным языком https://www.rabbitmq.com/documentation.html
Первый вопрос, а зачем вообще нужен брокер сообщений? Я давно делаю приложения на ASP.NET (классика ASP + EF + WebApi / Razor), но не понимаю в каком случае брокера нужно использовать. Было бы классно увидеть это в начале статьи.
У нас RabbitMQ используется в качестве событийной шины для общения между сервисами. Сервис А генерирует событие, оно кладётся в "exchange", который раскидывает сообщение по подключённым очередям сервисов B, C, D и т.д., которые в нужном им темпе извлекают события и как-то их обрабатывают. Сервис может таким образом реагировать на свои же события, т.е. получаем асинхронную обработку. С помощью очереди можно ещё реализовать асинхронные команды. Тут плюс в том, что очередь персистируется, и если сервис упал и не успел сделать ACK, то произойдёт retry (т.к. останется в очереди), что полезно для отказустойчивости, плюс там есть свои плюшки для мониторинга и работы именно с очередями... Но есть и свои минусы - если память кончается, то он заблочит все соединения (начнут висеть без ответа) и обработчики должны быть идемпотентными из-за at least once delivery. Если инстанс приложения один и объёмы небольшие, можно обрабатывать события сразу в том же запросе/транзакции.
Если упал процесс обработчика, то теряется контекст (теряется событие), и перебор циклом не поможет, так как мы не знаем, с чем работать. Конечно, можно сохранять события и в БД, но тогда нам придется самим переизобретать то, что уже умеет RabbitMQ.
>> Если упал процесс
Какой именно процесс? producer или consumer?
Давайте представим, что успал producer.
С RabbitMQ: MQ сервер не получит сообщения, оно не встанет в очередь и не дойдет до получателя.
Без RabbitMQ: сообщение так же не дойдет.
Давайте представим, что упал consumer.
С RabbitMQ: RabbitMQ сохранит сообщение в очереди и доставит его, когда consumer будет готов.
Без RabbitMQ: оно всё равно дойдет при следующем "try", ведь контекст сохранен в producer'е. Но есть одно НО, если во время retry producer упадет, мы потеряем сообщение. (гол в пользу Rabbit)
Давайте представим, что упал сам RabbitMQ.
С RabbitMQ: consumer не получит сообщения
Без RabbitMQ: ситуация невозможна (гол против Rabbit)
Итого счёт 1:1.
Но "если брюки выглядят одинаково, зачем платить больше?" ©
Я не вижу реальной выгоды от Рэббита, хотя может я что-то упустил в своем анализе?
Я имел в виду падение именно консьюмера (подкорректировал оригинальное сообщение) - сообщение тогда потеряно, т.е. гол в пользу RabbitMQ.
Что касается продьюсера, то верно замечено, что если мы упали после сохранения в БД, но до добавления события в очередь RabbitMQ, то мы тоже потеряем событие (имею в виду полное падение продьюсера, где retry невозможен). Для этого используется идиома transactional outbox - информация о событии создаётся и сохраняется в локальную БД в той же транзакции, что и бизнесовая операция на запись. Т.е если мы упали, то откатится вся транзакция сразу (ни изменений, ни событий). А если транзакция успешно закоммичена, то наличие события в БД гарантирует успешность/транзакционную целостность. Далее отдельный поток извлекает такие события из БД и кладёт в RabbitMQ с ретраем - т.е. мы гарантируем, что событие будет доставлено, в том числе если временно отвалится брокер. Плюс брокера в том, что это отдельный сервис, через который можно подключить по отдельной очереди на каждый сервис (для параллельной обработки), и он имеет из коробки протестированные временем инструменты для менеджмента таких очередей. Обычно польза чувствуется только если проект придерживается микросервисной архитектуры, где сервисы изолированы и с собственными БД, иначе можно, конечно, обойтись и без RabbitMQ, т.к. в монолите у нас и так есть доступ к персистированным событиям.
Также замечено верно, что RabbitMQ в таком случае становится узким горлышком (если он упал, то встанет вся обработка), но с таким же успехом может упасть и БД монолита. Тут главное, как по мне - возможность продолжить работу после падения как ни в чём не бывало (что позволяет персистирование очередей) и сам механизм exchanges/queue, который есть из коробки. Конечно, тут магии нет, и можно написать свой сервис событий, но можно использовать и готовый инструмент, проверенный временем.
Что-то я совсем запутался. Сначала вы пишете, что: "Конечно, можно сохранять события и в БД, но тогда нам придется самим переизобретать то, что уже умеет RabbitMQ."
Затем, отвечая на мои возражения вы пишете про: "идиома transactional outbox - информация о событии создаётся и сохраняется в локальную БД" ... "Далее отдельный поток извлекает такие события из БД и кладёт в RabbitMQ".
Разве это не то самое переизобретение из комента выше?
Т.е было:
producer ➡ rabbitmq ➡ consumer
стало
producer ➡ DB ➡ inter-producer ➡ rabbitmq ➡ consumer
Точка отказа сместилась, но осталась, система усложнилась. Непонимаю где профит? Мы можем добавлять промежуточные звенья в систему, но это не сделает её лучше.
Разве это не то самое переизобретение из комента выше?
В локальной БД хранится только outbox сервиса, без доп. логики. Если у нас N сервисов, заинтересованных в событии, то нам нужно N инбоксов, для каждого сервиса. В той же БД их невозможно хранить (т.к. БД разные), и на каждое обработанное событие для каждого сервиса также нужно поддерживать состояние (ack или не-ack). Профит в чём: 1) отказ продьюсера (его самого или его БД) не останавливает обработку, т.к. консьюмеры независимы и работают со своими копиями сообщений 2) гарантирует целостность данных 3) уменьшается связность сервисов, т.к. продьюсер не знает своих обработчиков. RabbitMQ довольно стабильная штука, и больше вероятности упасть не ему, а какому-то кастомному сервису одной из команд, напр. после неудачного релиза (а их овердохрена), поэтому и выносится это на редко изменяемый стабильный внешний сервис. Как я уже сказал, это полезно в основном при микросервисной архитектуре для повышения масштабируемости и отказоустойчивости, иначе для более простых проектов это явный оверинджиниринг.
система усложнилась
У нас всё ещё сложнее :) Каждый аккаунт это отдельная организация с тысячами пользователей (tenants), и чтобы избежать ситуаций, где одна большая организация забивает очередь событий 9000-ми событий и останавливает обработку для более мелких (т.к. они будут ждать своей очереди), у нас есть ещё одна прослойка, которая диспатчит события "справедливо", т.е. распределяет их порциями, чтобы у каждого аккаунта обработка продвигалась равномерно.
Это всё придумано не просто так "чтобы было" и является эволюционным решением конкретных проблем бизнеса (до этого был простой монолит без RabbitMQ, но перестало вывозить); если вам в ваших проектах профита нет, то не знаю, зачем вам доказывать профит :) Он есть, но решается для конкретных задач. Можно жить и без брокеров, но иметь в виду как инструмент всё же стоит :)
Брокер сообщений нужен для: буферизации запросов, выполнения параллельной обработки запросов, обеспечения слабой связанности между микросервисами, реализации событийного взаимодействия, реализации асинхронных операций.
Я один заметил что RabbitMqListener код не верный? А точнее создается коньсюмер к нему прилепывается листенер, но т.к коньсюмер никуда не присвается (в локальной области видимости) и не вызывается эвейтер , получается по консьюмеру произойдет диспос при выходе из метода ?
Не лучше ли переложить все это на Masstransit или его аналоги?
Метод отправки слишком захардкоженный. Зачастую передаются объекты, соотвествено и сам метод отправки должен быть универсальным.
Столько много тирад над "Зачем нужен брокер сообщений", но всего пара постов про асинхронные операции, что по моему скромному мнению является основным преимуществом перед синхронными REST и gRPC.
RabbitMQ в ASP.NET Core. Быстрый старт