
Архитектура — это всегда баланс между контролем и гибкостью. Микросервисы (MSA) хороши тем, что чётко разделяют логику, дают независимое масштабирование и удобны в отладке. Каждый сервис сам за себя, отвечает за конкретную зону ответственности и общается с другими через API — обычно REST или gRPC. Вроде бы идеальная схема, но со временем возникает проблема: сервисов становится всё больше, а их связи усложняются. Появляется скрытая зависимость — если один сервис меняет контракт API, все клиенты должны адаптироваться. Итог: чем больше микросервисов, тем сильнее их взаимозависимость, что фактически превращает систему в «распределённый монолит».
Хороший пример — Amazon. Компания начинала с монолитного приложения, но со временем перешла к микросервисной архитектуре. Это позволило командам работать независимо, но привело к новым проблемам — например, сложностям с управлением версиями API и зависимостями между сервисами.
Теперь посмотрим на событийно-ориентированную архитектуру (EDA). Здесь сервисы не дергают друг друга напрямую, а просто публикуют события. Например, сервис регистрации отправляет событие «пользователь зарегистрирован», а уже подписчики решают, как на это реагировать: один создаст профиль в базе, другой отправит email, третий добавит запись в CRM. Сервису, который породил событие, вообще всё равно, кто его обработает — он просто выдал сигнал в шину сообщений (Kafka, RabbitMQ, Amazon EventBridge и т. д.).
На практике это даёт кучу плюсов. Во-первых, такая архитектура проще масштабируется: нагрузка распределяется, а новые подписчики могут подключаться без изменений в исходном сервисе. Во-вторых, отказоустойчивость выше: если один обработчик упадёт, события никуда не пропадут и будут обработаны позже. В-третьих, это отлично подходит для асинхронных задач, например, в e-commerce — заказ оформлен, а дальше система сама разруливает отправку уведомлений, списание денег и логистику без ожидания ответа от каждого сервиса.
Такую модель использует Netflix. Их система логирования и мониторинга построена на EDA: все сервисы стриминга, рекомендации, биллинга и аналитики обмениваются событиями через Kafka, что позволяет динамически масштабировать нагрузку.
Но у EDA тоже хватает минусов. Во-первых, дебаг. Если в микросервисах можно просто вызвать API и сразу увидеть результат, то тут события путешествуют по системе непредсказуемо. Откуда взялся баг? Какой сервис не обработал событие? Где оно вообще потерялось? Чтобы не утонуть в хаосе, приходится внедрять трассировку, ставить мониторинг, логировать каждую стадию обработки. Uber, например, использует OpenTelemetry и Jaeger для распределённого трейсинга, чтобы отслеживать движение событий между сотнями сервисов.
Во-вторых, сложность управления событиями. В реальной системе одно событие может триггерить десятки других процессов, а некоторые события могут дублироваться из-за проблем с сетью или задержек в обработке. Это значит, что нужно продумывать механику дедупликации и идемпотентности, чтобы случайно не создать дубликаты заказов или платежей. Airbnb столкнулись с этой проблемой при масштабировании системы бронирования — пришлось внедрять строгий контроль за повторной обработкой событий.
Так что же выбрать?
Если система требует строгой синхронности и предсказуемости — микросервисы. Если важна гибкость, отказоустойчивость и независимость компонентов — события.
Но идеальный вариант — это их комбинация. Критичные запросы (например, авторизация, платежи) лучше делать через API, чтобы избежать задержек и дублирования. А для второстепенных процессов (уведомления, аналитика, интеграции с внешними системами) можно использовать брокер сообщений.
Например, в маркетплейсе Amazon заказ можно оформить через REST API, но обработка идёт через событийную архитектуру:
Пользователь оформил заказ → сервис заказов сохранил данные в базе.
Событие «заказ создан» отправилось в шину.
Сервис платежей подписан на это событие → списал деньги.
Сервис логистики подписан → передал информацию на склад.
Сервис уведомлений подписан → отправил email клиенту.
Таким образом, каждое действие логически отделено от других, но вся система работает как единое целое.
Вывод простой: чем больше система, тем полезнее события, но полностью отказываться от API тоже не стоит. Главное — не упрощать архитектуру в ущерб масштабируемости.
Комментарии (19)
suburg
18.02.2025 15:35Не понял почему в заголовке ИЛИ.
Микросервисная архитектура - это с моей точки зрения про структуру системы, статика.
Событийно-ориентированная - про порядок взаимодействия, динамика.
Одно не исключает другого, эти вещи разного порядка.
Звучит как "вам яблоко большое или красное"
Arm79
18.02.2025 15:35Событийная модель и микросервисы полностью перпендикулярны друг другу. Между ними нельзя ставить ИЛИ.
Кстати, реактивная модель прекрасно работает с событиями.
lnkiseleva
18.02.2025 15:35Открываем рандомную книгу про архитектуру приложений и увидим примерно следующее, что архитектура приложения зависит от объема нагрузки и ее характера. То есть заложить сразу и предсказать, что вот это решение будет у нас хорошо работать в дальнейшем, довольно сложно. Плюс постоянно появляются новые подходы и инструменты. Понятно, что архитектурные изменения в работающем большом приложении это долго и дорого, но по мере развития приложения в какой-то момент дорабатывать придется.
AAnisimov
18.02.2025 15:35Заголовок кликюейтный.
Сравнение теплого и мягкого. Управление логикой и организацию противопоставить - сильная заявка.
Давайте противопоставим оркестрацию и монолит?
nin-jin
Не сохранил, ибо кончилось место на диске - заказ потерялся.
Отправилось, но не принялось, так как из-за повышенной нагрузки на сеть произошёл таймаут.
Не списал, так как банковский сервис, через который он работает, в данный момент не доступен.
Не передал, так как склад сейчас оффлайн из-за перебоев с интернетом.
Не отправил, так как в момент отправки уведомления был перезагруен.
А вот как бы всё это выглядело в реактивной архитектуре:
Пользователь оформил заказ → данные уже сохранены у него в локальной базе.
Локальная база в конечном счёте синхронизируется с серверами.
Сервис платежей видит неоплаченный заказ → пытается списать деньги, пока не получится.
Склад видит оплаченный, но не доставленный заказ → готовит его к выдаче.
Сервис уведомлений видя разницу между тем, что пользователь уже видел, и тем, что ещё нет → отправляет push уведомление.
vitrilo
А можно гдето почитать об этом подходе? На запрос Реактивная Архитектора - выдает очень общий список пожеланий к работе системы - Reactive Manifesto.
Конкретнее
как на шаге #3 сервис видя что в базе есть запись уверен что только он один ее прочитал и потом записал? Что если есть несколько экземпляров сервиса #3.
Сервис #3 мониторит БД SQL запросом? Тоесть PULL, или он получает извещение о том что строка в БД изменилась - PUSH?
Заранее спасибо
nin-jin
Разве что тут. Манифест тот о чём-то другом.
Каждый экземпляр отвечает за свой диапазон идентификаторов и никак не влияет на остальные экземпляры.
Многие СУБД предоставляют "живые запросы", что позволяет получать пуши при изменении в базе.
pkokoshnikov
События в бд не надёжно. Только репликация и полинг даст хорошую гарантию.
nin-jin
Вполне допускаю, что существуют СУБД, где это не надёжно. Ко многим это прикручивали костылями в последний момент.
Mr_Cheater
А что в данном случае имеется в виду под термином «локальная база данных»?
nin-jin
БД на машите пользователя, не зависящая от доступности серверов.
Mr_Cheater
Т.е. какой-нибудь localStorage?
nin-jin
Какой-нибудь IndexedDB или даже File.
haaji
Не сохранены - у пользователя кончилось место. Что раз в 1000 вероятнее, чем кончившееся место на выделенном сервисе, обмазанном мониторингом.
Вся описанная схема с eventual consistency подойдет разве что для написания и синхронизации заметок, для задачи покупки - это отвратительный UX.
nin-jin
Если у пользователя настолько закончилось место, то он даже не дойдёт до оформления заказа.
Да-да, расскажите мне про прекрасный UX с потерей заказа из-за недоступности сервера.
MasterDoramu
Это все очень просто лечится.
Делаешь master-slaves бд.
Делаешь транзакционные запросы в симфонии между событиями.
При сбое одного запроса и нескольких безуспешных траев - откатываешь события обратно. Выводишь юзеру ошибку, что мол так и так, к сожалению обработать платёж не вышло.
Хотя с платежкой наверное лучше отдельный оркестратор сделать - для стабильности, но это как редкие исключения должно быть, так как он все таки отдельные сценарии контролит.
Профит.
nin-jin
Человек в поезде едет и связь постоянно рвётся. До вашей симфонии запросы даже не доходят с первого раза. А пользователь устал уже 10 раз вручную перезапускать оформление заказа. Его вообще не должно волновать где что временно не доступно: "у вас там что-то сломалось? ОК, я подожду, напишите как всё починится и заказ будет оформлен"
totsamiynixon
Могу ошибаться, но возникает такое чувство, что Вы считаете, что если сообщение не было обработано сервисом (проблемы с сетью или железом), то оно не будет обработано им после восстановления к штатному режиму работы.
Гарантию доставки в шину обмена сообщений обеспечивает transactional outbox. Сами шины обмена сообщений гарантируют at least once delivery. Когда сервис вернётся в рабочий режим он продолжит работу с той точки, где он закончил, даже если он упал где-то в середине обработки сообщения. Для этого и нужна идемпотентность, чтобы убедиться, что повторная обработка сообщения на приведет к сбоям в логике работы приложения.
Так что все будет в порядке, и результат будет такой же, каким Вы себе его представляете в Вашей интерпретации Реактивной Архитектуре.
nin-jin
Это называется "материализация событий" - превращение событий в записи базы данных, за которыми можно наблюдать, идемпотентно синхронизировать и вот это вот всё. Чем раньше вы откажетесь от событий, тем меньше нужно будет костылей с persistent transactional outbox, persistent message queue и тп.