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

Событийная архитектура

Монолитная архитектура

До появления современной программной архитектуры приложения создавались как единое развертывание, называемое монолитом.

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

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

Монолитная архитектура

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

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

И, возможно, это событийная архитектура.

Событийная архитектура

В такой архитектуре ключевым элементом информации, на котором основывается система, является событие.

Событие  —  это момент времени, которым обозначается факт того, что что-то произошло. С событием могут быть связаны данные  —  его метаданные, которыми описывается произошедшее, однако важно само событие.

Как исторический факт, события неизменны: это запись о том, что и когда произошло, ее нельзя изменить.

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

Будучи неизменными, события обрабатываются в любое время после их генерирования. То есть система, откуда создаваемое в ней событие отправляется, эта система-отправитель «не знает», в какой другой системе-получателе оно обработается и когда. Нет ей дела и до того, сколько у события получателей или что они с ним делают.

Налицо полное разделение отправителей и получателей.

Событийная архитектура

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

Разделение получателей

На схеме также показано три получателя события. Будучи полученным одним из них, оно не удаляется*, а остается в очереди для двух других. События получаются каждым получателем одно за другим, подробнее об этом позже.

*События условно не удаляются, зато очищаются старые, что обусловлено затратами ресурсов. В приложении этим проектным решением определяется время, в течение которого события остаются доступными.

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

То есть отделены не только отправители от получателей, но и получатели друг от друга.

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

Масштабирование получателей

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

Масштабирование получателей

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

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

Расширение функций

Другая проблема монолита заключается в том, что добавлять или изменять функции становится очень сложно. Как бы не сломать чего. И это при наличии автоматизированного регрессионного тестирования.

Расширение набора функций

Снова приходится кстати событийная архитектура.

На этой схеме изменяется функция 3, и это не сказывается на других.

И добавляется функция 5 опять же в полной уверенности, что это не скажется на имеющихся функциях.

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

Брокер событий

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

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

Модель «издатель-подписчик»

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

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

Это называется архитектурой «издатель-подписчик».

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

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

Итак, нужна ли событийная архитектура?

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

Аргументы против

Казалось бы, событийная архитектура  —  это панацея от всех проблем. Все отделено, масштабируется и сопровождается.

Но на практике не такая это и панацея.

  • Сложность: при добавлении брокера в архитектуру появляется еще один компонент, который нужно настраивать и сопровождать.
  • Надежность: с брокером в системе появляется и значимая точка отказа, отказ брокера чреват отказом всей системы.
  • Скрытые зависимости: казалось бы, все теперь отделено, зависимостей нет, но это не так. Если отправителем прекратится публикация одного события в пользу другого или изменятся связанные с событием метаданные, придется изменить все остальные функциональные компоненты.
  • Согласованность данных: события обрабатываются разными частями системы с разной скоростью, это чревато несогласованностью частей системы. Только когда события прекращаются, система становится «окончательно согласованной», чего на самом деле никогда не случается.
  • Асинхронный ответ: там, где часть системы нуждается в синхронном ответе, например в качестве реакции на действие, которое необходимо выполнить сейчас, она блокируется с расходованием ресурсов в течение длительного времени.
  • Запрашивание данных: несогласованность данных оборачивается не только проблемой при запрашивании данных в различных функциях, но и серьезными трудностями при выполнении запросов к нескольким базам данных.
  • Отладка: когда что-то не так, выяснить, что произошло и почему, тоже становится сложнее. Хотя последовательность событий восстанавливается, попытки воспроизвести проблему сильно затрудняются состояниями гонки.
  • Тестирование: для тестирования системы теперь требуется объединить все функции, в том числе брокер событий. И приложить немало усилий.
  • Антипаттерны: имеются решения, при создании которых преимущества использования событийно-ориентированных архитектур фактически сведены на нет. Эти решения называются антипаттернами.
  • Упорядочение событий: на первый взгляд кажется, что события просто получаются получателями и до неупорядоченных или потерянных событий и производительности им нет дела. На самом деле все это необходимо учитывать.
  • Дублирование данных: у каждой функции свой «взгляд на мир»; если этот взгляд поврежден, требуется немало усилий по исправлению данных во всех функциях.
  • Уязвимость безопасности: все бизнес-данные теперь перемещаются в одной системе и становятся уязвимыми для атак.

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

Например, для стартапов, у которых нет необходимости в крупномасштабной обработке данных и имеется относительно простая проблемная область, монолитная архитектура  —  оптимальный выбор.

Зачем вообще тогда событийная архитектура?

Зачем вообще выбирать событийную архитектуру со столькими трудностями?

У ее широкой популярности должны быть веские причины.

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

Большинство обозначенных выше проблем преодолеваются:

  • Сложность: использованием проверенных технологических решений, таких как Kafka.
  • Надежность: использованием проверенных технологических решений высокой доступности, таких как Kafka.
  • Скрытые зависимости: внедрением в проекты с событиями концепции контроля версий и работой с обратной совместимостью на одну или две версии, то есть n–1, n–2.
  • Согласованность данных: определением наличия проблемы и, если она имеется, применением синхронного решения, например API-интерфейсов.
  • Асинхронный ответ: так же применением синхронного решения, если проблема имеется.
  • Запрашивание данных: в некоторых целях использованием хранилища данных, которым получаются все события и создается согласованное текущее состояние системы. А там, где в функциях требуются данные,  —  созданием данных, необходимых для запрашивания в собственной базе данных, но без разрастания данных.
  • Отладка: внедрением инструментов для мониторинга и управления событийными и распределенными системами, такими как Zipkin и Jaeger.
  • Тестирование: созданием фреймворка для работы над сегментом системы в целом.
  • Антипаттерны: пониманием антипаттернов, и их избеганием.
  • Упорядочение событий: встраиванием в микросервисы фреймворка для управления неупорядоченными и дублированными сообщениями, выбором проектных решений на основе требований предметной области.
  • Дублирование данных: применением механизмов повторной синхронизации распределенных хранилищ данных и разработкой инструментария для исправления данных.
  • Уязвимость безопасности: реализацией в брокере требуемого уровня контроля доступа ко всем событиям, в том числе отправляемым, получаемым и находящимся в постоянном хранилище.

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

Сложности миграции монолитов

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

На этом этапе при тщательном планировании и проектировании необходимо учитывать ряд факторов.

1. Распределение монолита

Рик Пейдж однажды написал: «Надежда  —  это не стратегия».

Разбить монолит на части и надеяться склеить их брокером событий тоже не стратегия.

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

2. Синхронистичность в асинхронном мире

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

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

3. Дублирование данных  —  повсюду

В монолитном мире у каждой функции имеется доступ ко всем данным. В рамках внутреннего проектирования объекты предметной области отделяются друг от друга сервисным слоем, а внутренними API-интерфейсами предоставляется неограниченный доступ.

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

4. Организация событий

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

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

При этом важно не забыть концепцию, согласно которой отправитель не должен ничего «знать» о получателе. Так создается простая и гибкая система, с которой из архитектуры получается максимум преимуществ.

5. Оптимизация дизайна

Отправитель не должен ничего «знать» о получателе. Но, если принять это как неизменную политику, в итоге получится система, которую сложно контролировать и развивать. Ведь бизнес-логика для той или иной сферы или области распределяется и, возможно, даже дублируется.

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

6. Проектирование по наихудшему варианту

И вот все готово, от брокера получателям предоставляется корректно упорядоченный, непрерывный набор событий без дублей.

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

Но это допущение неверное.

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

Нужно проектировать по наихудшему варианту, а не наилучшему.

Планирование миграции

Теперь вы понимаете, что нельзя просто проснуться в одно прекрасное утро и начать рвать монолит. Необходимо спроектировать целевое состояние, в том числе:

  • где событийная архитектура придется кстати, а где нет;
  • как справиться с каждой из обозначенных нами трудностей;
  • какие соглашения понадобятся для обеспечения согласованности проектирования системы;
  • как разработчики и инженеры по тестированию ПО разработают и протестируют приложение.

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

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

Нужно планировать поэтапную реализацию. Вот способы ее проведения.

Шаблон Strangler

В этом сценарии части системы постепенно заменяются, но извне разница не заметна благодаря фасаду.

Цель  —  снизить риски при миграции, выполняя ее по чуть-чуть до полного перенесения всего приложения.

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

Инкрементная эволюция

Этой техникой событийная архитектура реализуется только для новых функций.

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

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

Сначала проблемная область

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

В зависимости от того, как эта функция интегрируется в монолит, сборка может оказаться технически сложной.

И аудитория

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

Исходя из обратной связи пользователей, определяется порядок разработки функций в новом приложении.

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

Другие

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

  • заполучать и удерживать внимание вовлеченных сторон;
  • минимизировать риск;
  • не создавать проблем и не усложнять себе жизнь;
  • добиваться ожидаемых преимуществ в намеченные сроки.

Заключение

Мы попытались разобраться с событийной архитектурой: где она нужна и где нет и что учитывать при переходе к ней от монолита.

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

Миграция не тот проект, который реализуется на стороне или в свободное время. Это серьезное дело, которое требуется тщательно спланировать, спроектировать и реализовать.

И оно выполнимо. Многие справились с ним и воспользовались преимуществами событийной архитектуры.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Martin Hodges: How to move from a monolith to an event-based system

Предыдущая статьяЧто важнее — промпт-дизайн или промпт-инжиниринг?