Архитектура “запрос-ответ” (Request-Response) и событийно-ориентированная архитектура (Event Driven Architecture)
Архитектура “запрос-ответ” (Request-Response) и событийно-ориентированная архитектура (Event Driven Architecture)

Неотъемлемым атрибутом таких компаний, как Uber, Twitter и LinkedIn, являются обновления в режиме реального времени: уведомления, сопровождающие вашу поездку, в Uber, твиты друзей и полезные советы отраслевых экспертов, которые можно получить и переслать в течении нескольких секунд. Как только информация попадает в сеть, она сразу становится доступной для всех вокруг. Пользователям всегда будет нравится эта простая и быстрая доступность информации - они всегда находятся в поиске подобных улучшений качества их жизни.

Чтобы удовлетворять постоянно растущие требования потребителей, многие компании вынуждены отказываться от традиционных структур “запрос-ответ” (Request-Response) в пользу событийно-ориентированных архитектур (Event Driven Architecture).

Бывший вице-президент AWS и по совместительству выдающийся инженер Тим Брэй (Tim Bray) во время своего выступления на AWS Re: Invent 2019 сказал: “[событийно-ориентированная архитектура] - правильный путь к построению Amazon.com, и любой другой способ будет неправильным”. Полагаю, одного этого заявления уже достаточно, чтобы вызвать интерес к событийно-ориентированной архитектуре.

Итак, что такое событийно-ориентированная архитектура? Прежде чем мы углубимся в эту тему, мы должны рассмотреть традиционную архитектуру “запрос-ответ”. “Запрос-ответ” зиждется на синхронных вызовах, инициирующих работу цепочки сервисов и ожидающих, что вызов вернется обратно с ответом, после чего может быть выполнен следующий запрос.

В приведенном выше примере мы узнаем монолитную архитектуру. А теперь представьте, что случится в подобном сетапе, если какой-нибудь сервис на сервере выйдет из строя? Запрос от клиента будет потерян. Для таких коммерческих сайтов, как Amazon, это будет означать потерю продаж на миллиарды долларов. В небольшой компании, которая обрабатывает меньший объем трафика, использование подобной монолитной архитектуры оправданно. Однако, как только компания начинает задумываться о масштабировании в будущем, эта модель не скупится на обилие узких мест.

И тут на сцену выходят событийно-ориентированные архитектуры. Теперь без лишних отлагательств давайте углубимся в них и разберемся со сценариями использования и перспективными инструментами.

Что из себя представляет событийно-ориентированная архитектура?

В отличие от систем “запрос-ответ”, в событийно-ориентированной архитектуре, запрашивающая сторона отправляет событие (обычно это сообщение с заголовком [header] и полезными данными [payload]) на слой распределения (distribution layer). Далее это сообщение может принять сервис, который прослушивает какой-нибудь конкретный топик (тему). Затем этот сервис может каким либо образом использовать полезные данные сообщения или передать это сообщение далее другому сервису.

Первоначальный отправитель этого события называется издателем/продюсер (publisher/producer), а принимающая сторона - потребителем/подписчиком (consumer/subscriber). Эта система позволяет сразу нескольким сервисам принимать одно и то же событие, если их каналы прослушивают один и тот же топик.

Система продюсера и потребителя
Система продюсера и потребителя

Давайте посмотрим на диаграмму выше. Топик в этой диаграмме - “slick car” (навороченный автомобиль). Потребитель (микросервис), который слушает топик “slick car”, получит сообщение, отправленное продюсером. Второй потребитель не получит это сообщение, так как слушает топик “classic car” (классический автомобиль). Слой распределения является посредником, который агрегирует сообщения, отправленные продюсером. Если потребитель вдруг сталкивается с какими-либо проблемами при получении сообщения, слой распределения может сохранить это сообщение, поместив его в очередь, и отправить его потребителю позже, когда он будет готов.

К Рождеству готовы
К Рождеству готовы

Это очень похоже на то, как работает современная почтовая система. Когда бабушка (продюсер) отправляет посылку (сообщение) внуку (потребителю), ей нужно указать адрес (топик). Когда посылка отправлена, сначала она попадает в почтовое отделение (слой распределения). По прибытии в пункт назначения, если семья внука готова ее принять, посылка может быть доставлена ​​немедленно. В противном случае почтовое отделение может держать посылку у себя до тех пор, пока конечные адресаты не будут готовы ее получить.

Подобный механизм удержания гарантирует, что сообщения никогда не будут потеряны - они будут поставлены в очередь для доставки, как только потребитель будет готов к их получению. Это ключевое различие между событийно-ориентированной архитектурой и архитектурой “запрос-ответ”.

Зачем использовать событийно-ориентированную архитектуру?

Переход на событийно-ориентированную архитектуру дает множество преимуществ. Но, в частности, можно выделить четыре основных преимущества: вывод в реальном времени, простая масштабируемость, отказоустойчивость и высокая доступность.

Вывод в реальном времени

Когда клиентом триггерит событие, данные, отправляемые в этом событии, обычно имеют формат JSON или Avro. Когда эти данные отправляются на слой распределения, сообщение сериализуется в байтовый формат, поскольку слой распределения не заботит содержимое сообщения, его единственная задача - передать сообщение соответствующему потребителю. Этот процесс сериализации делает доставку сообщений чрезвычайно быстрой, что результирует в более высокой пропускной способности потоков данных. Это позволяет обновлять информацию в реальном времени.

Легкая масштабируемость

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

Это значительно упрощает подключение различных микросервисов и сокращает время разработки, что результирует в более высокой скорости масштабирования.

Отказоустойчивость и высокая доступность

Одним из недостатков использования архитектуры “запрос-ответ” является наличие так называемой единой точки отказа. Если сервис выходит из строя хотя бы на какое-то время, то весь рабочий поток может быть нарушен.

Именно здесь событийно-ориентированная архитектура наиболее ярко проявляет свое преимущество. Это связано с тем, что слои распределения могут иметь несколько серверов, выделенных для хранения сообщений, и могут горизонтально масштабироваться по мере необходимости. И затем, даже если одна из систем выйдет из строя, другие сервера смогут перехватывать и ретранслировать сообщения.

Более того, с помощью механизма постановки в очередь сообщения могут быть сохранены, когда сервис-потребителя отключается, и вновь отправлены, как только потребитель снова начинает прослушивать сообщения.

С чего мне начать?

Ниже приведены известные инструменты, которые вы можете использовать в производственной среде, чтобы начать свой победный путь с событийно-ориентированной архитектурой.

RabbitMQ

RabbitMQ - это продукт с открытым исходным кодом, разработанный Pivotal Software. Примечательной особенностью RabbitMQ является его способность давать сообщениям приоритеты, чтобы потребители могли получать конкретные сообщения быстрее.

Вы можете узнать больше о настройке RabbitMQ на их сайте.

Kafka

Kafka - еще один фреймворк с открытым исходным кодом, использующий потоковую обработку. Он славится тем, что имеет высокую пропускную способность и низкую задержку, что позволяет эффективно обрабатывать потоки данных в реальном времени.

Вы можете узнать больше о настройке Kafka на его сайте.

Заключение

Событийно-ориентированные архитектуры набирают популярность не просто так. Масштабируемость при создании изолированных микросервисов - это парадигма, к достижению которой постоянно стремятся многие предприятия, а событийно-ориентированная архитектура построена с оглядкой на это. Благодаря инновационным инструментам, таким как Kafka и RabbitMQ, событийно-ориентированные архитектуры становятся более гибкими, универсальными и надежными, что удовлетворяет множество актуальных бизнес-запросов.

Но этот мощный инструмент имеет и свою цену: высокую кривую обучения и достаточно сложную первоначальную настройку. Однако после создания такой структуры преимущества очевидны, поскольку данные передаются с меньшей задержкой и большей пропускной способностью. Так почему бы не использовать событийно-ориентированную архитектуру?

Спасибо, что нашли время прочитать эту статью!


Материал подготовлен в рамках курса «Microservice Architecture». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Комментарии (17)


  1. EvilBeaver
    16.12.2021 17:55
    +2

    А сколько классных глюков несет эта архитектура (как и любая другая распределенная), когда порядок сообщений сбивается, или когда оно доставлено не во все места, куда должно... там столько веселья кроется, что синхронные проблемы "сервис недоступен" покажутся раем.


    1. OlegZH
      16.12.2021 18:39

      А можно поподробнее написать про порядок сообщений? Можете ли Вы набросать пару-тройку реальных примеров?


      1. EvilBeaver
        17.12.2021 10:50

        Порядок сообщений возникает, когда например у вас на основании каких-то статусов заказов что-то происходит в бизнесе. Клиент создал заказ, клиент поменял заказ, клиент отменил заказ, клиент оплатил заказ.

        Так вот, оплата может быть как до отмены, так и после. Причем это ПОСЛЕ может случиться и в реальном мире (действительно сначала отменил, а потом оплатил) так и из-за сбоев в сервисах. На каждый из случав нужна разная реакция бизнеса. Если в event-driven логика завязана на порядок сообщений - вас ждут проблемы. Это не масштабируется.

        Еще пример: есть одна очередь, в ней порядкозависимые события. Если у нас один воркер ее разгребает - все хорошо. но вот он перестал справляться. Мы не можем добавить еще один на эту же очередь, поскольку теперь какое сообщение попадет в обработку быстрее №1 или №2? Пусть воркер А почему-то долго обрабатывает сообщение №1 (база тупит, сеть залагала, мало ли). Воркер Б получает сообщение 2, которое не может наступить раньше, чем сообщение 1. Но про сообщение 1 то воркер не знает ничего. И о том как оно обработано тоже (транзакция воркера А еще не завершена).

        Придется масштабировать не воркерами, а очередями. Например была 1 очередь на все магазины, стало 20 очередей по числу магазинов. Ну и вообще, ухищрений и велосипедов связанных с обеспечением идемпотентности, порядка, throttling - очень много возникает.


        1. aa0ndrey
          17.12.2021 14:36

          Если мы, например, говорим про kafka, то для масштабирования есть partition. В качестве ключа партиции указываем id заказа. Тогда заказы обрабатываются параллельно, но для одного заказа порядок всегда фиксированный. Это позволяет машстабироваться линейно.


    1. AlexSpaizNet
      16.12.2021 19:20
      +3

      Вы правы. Уже 4 года варюсь в message driven/event driven и система все еще продолжает удивлять. От простых ритраев которые спасают и могут и проблемы сделать если нет idempotency, до спайков которые убивают http сервисы или базу потому что воркеры скейлятся намного быстрее и нужно делать circuit breaker и рейт лимиты.

      Про параллельную обработку это вообще отдельная тема...

      От построения тупого POC до системы которая будет реально работать как часы, как от земли до луны.

      Последний раз 3 дня копал почему у нас воркер который работает с раббитом крашится, и месседж остается все время в head и все это повторяется и вообще ничего не может процесситься.

      Готовлю статью на медиуме на эту тему. Оказалось для long running CPU bound job драйвер раббита для NODEJS не может отправить heartbeat и раббит закрывает соединение =)))

      P.S.

      Но справедливости ради нужно сказать, все таки эта архитектура необходима если вам важны данные и важно их процессить, и важно иметь возможность управлять нагрузкой и разделать ворклоады. Если же вы готовы терять реквесты если сервис лежит, и вам не важно что один клиент может повлиять но всех остальных, то может оно вам и не надо...


      1. EvilBeaver
        17.12.2021 11:03

        heartbeat-ы в кролике тоже нам попили крови, ага. Мне больше нравится слово "пульс", т.к. печатать меньше :) Нужно в потоке обработки что-нибудь дернуть в кролике, какой-нибудь статус проверить, тогда отошлется пульс. А если забрал сообщение и долго процессишь, то драйвер порвет соединение. Большая часть клиентов кролика построена на либе rabbitmq-c, а она не имеет метода автоматического асинхронного "пульсирования". Отсюда поведение в ноде, питоне и даже 1С - такое


    1. nin-jin
      17.12.2021 11:54
      -1

      Любая неидемпотентность - источник множества граблей. Поэтому стоит использовать delta-based архитектуру, а не event-driven.


      1. mayorovp
        17.12.2021 12:46

        И как же неидемпонентные дельты позволят бороться с неидемпотентностью?


        1. nin-jin
          17.12.2021 14:23
          -1

          Очень просто - они идемпотентны. А то, о чём вы подумали, - это события с патчами.


          1. AlexSpaizNet
            17.12.2021 16:44

            Вы о CRDT?


            1. nin-jin
              17.12.2021 18:17

              Если нужны двунаправленные информационные потоки, то да.


  1. megahertz
    17.12.2021 00:09

    Главное фронтендерам про это не рассказывать.

    Ожидание: модули системы независимы друг от друга и общаются между собой через redux.

    Реальность: зависимости никуда не делись, но теперь они неявные, для понимания недостаточно посмотреть код, нужно отдельно документировать. Отладка превращается в ад.


    1. napa3um
      17.12.2021 04:00
      -1

      Redux DevTools для хрома пробовали?


    1. AlexSpaizNet
      17.12.2021 11:12

      В принципе на бэкенде тоже самое и это кстати важно понимать. Если нет трейсинга, документации/ADR, то понять бизнес будет сложно. Теряется общая картина. И вот сидишь ты и смотришь на все эти месседжи и не понимаешь а какие вообще бизнес процессы тут протекают и что с этим всем делать.

      Особенный ад это когда евенты в виде крада... reservationUpdated, listingEdited. Легче застрелиться чем поддерживать такую систему.


    1. aa0ndrey
      17.12.2021 14:53

      Подскажите, а чем вызвано усложнение отладки?

      Если, например, без событий, то у вас есть условно класс A1, который вызывает метод класса B1. С помощью IDE вы переходите в класс B1.

      А если у вас отправляется событие, то у вас класс A1 отправляет событие E1. Класс B1 обрабатывает событие E1. То есть по классу события E1 или по названию топика вы можете найти класс B1.


      1. megahertz
        17.12.2021 17:31

        В простом случае, там где такая архитектура вообщем-то и не нужна, при условии строгой типизации просмотреть конечно не сложно. Но даже в этом случае ctrl + click при классическом подходе гораздо удобнее. В реальной же системе на событие может быть много подписчиков, обрабатывают они их с разной скоростью а потом шлют другие события. Иногда что-то где-то теряется, отправляется дважды и т.п. Таким образом, сложность возрастает, предсказуемость снижается. Должна быть достаточно веская причина чтобы пойти на такие компромисы.


        1. aa0ndrey
          17.12.2021 17:59
          +1

          Если правильно понимаю, то что-то где-то потеряться может, если используется внешний брокер и есть вызовы по сети. Защиту от потерь обычно на себя берет брокер сообщений. На принимающей стороне для этого можно сделать универсальное решение с идемпотентностью.

          Тут два момента:

          1. Решение защищающее от потерь сообщений не влияет на сложность поиска обработчиков. То есть код от этого решения не должен становиться запутанее.

          2. Блокирующие вызовы по сети не имеют защиты от потерь. Это обычно приводят в качестве преимуществ и обычно это и является веским основанием использовать очереди.

          Если где-то возможна нежелательная гонка или возникает сложность в понимании процесса обработки, то в этом случае можно управлять процессом явно. см: https://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html