Создание Event-Driven архитектуры
Большинство современных систем — распределённые и реализуют микросервисную архитектуру. Многим веб-приложениям нужна возможность обработки и обновления данных в режиме реального времени:
- клиенты должны получать обновления сразу, как только сменился статус по их заказу;
- антифрод-система должна получать информацию о подозрительных операциях в ту же секунду, как они произошли;
- пользователям мессенджеров требуется мгновенная отправка сообщений собеседнику, где бы тот ни находился.
Event-Driven архитектура (EDA) появилась как ответ на необходимость более гибкого и масштабируемого способа взаимодействия между компонентами распределённой системы. А постоянный рост требований ещё больше стимулировал развитие EDA.
В этом уроке мы разберём, что представляет собой Event-Driven архитектура, из каких шагов состоит её проектирование, про какие паттерны стоит помнить при выборе EDA. А также детально разберём, как применить CQRS в рамках Event-Driven системы.
Event-Driven архитектура
✏️ Event-Driven архитектура, или EDA, — это архитектурный подход, который основан на асинхронном взаимодействии между слабо связанными компонентами в системе посредством передачи событий. EDA призвана улучшить масштабируемость, устойчивость и производительность системы.
Какие проблемы решает Event-Driven архитектура:
- МасштабируемостьEDA упрощает горизонтальное масштабирование благодаря асинхронному взаимодействию между компонентами системы.
- ОтказоустойчивостьУменьшение зависимости между компонентами системы снижает риск сбоев всей системы из-за отказа одного из них.
- Гибкость в разработкеРазработчики могут добавлять или изменять компоненты без внесения изменений в другие части системы.
- Реальное время обработкиБлагодаря немедленной реакции на события система может обрабатывать данные почти в реальном времени (near real-time).
Эти результаты достигаются за счёт следующих особенностей EDA:
- Разделение обязанностей
Каждый компонент фокусируется на своих задачах и отвечает за генерацию конкретных событий. Это уменьшает необходимость в прямом взаимодействии между сервисами.
- Асинхронная коммуникация
Обмен событиями обычно происходит асинхронно, что означает отсутствие блокировки процессов в ожидании ответа от других компонентов.
- Динамическая подписка на события
Компоненты могут подписываться или отписываться от определённых событий динамически, в зависимости от текущего контекста или состояния, что предоставляет дополнительную гибкость в координации работы сервисов.
Команда сервиса «ОнлиСхемы» уже обеспечила своему приложению возможность автоматически горизонтально масштабироваться под нагрузку. Однако при тестировании обнаружили проблемы, которые значительно снижают гибкость этого процесса: из-за синхронного взаимодействия при росте нагрузки на сервис досок пропорционально растёт нагрузка и на сервисы пользователей и уведомлений. Это приводит к необходимости пропорционально масштабировать сразу все компоненты вместо одного.
В этом смысле ситуация почти не отличается от той, когда все компоненты приложения были расположены в одном монолитном приложении. За исключением того, что из-за необходимости взаимодействия сервисов по сети с использованием синхронного протокола приложение менее отказоустойчиво.
Время обработки запросов также растёт из-за суммарного времени на взаимодействия: сервис досок по HTTP вызывает сервис уведомлений, который, в свою очередь, сначала по HTTP вызывает сервис пользователей, а затем также по синхронному протоколу SMTP вызывает email-сервер.
Обсудим EDA подробнее и посмотрим, как она может помочь решить описанные выше проблемы. Начнём с изучения базовых понятий EDA, ключевым из которых является событие.
Что такое событие
✏️ Событие — это значимое изменение в состоянии системы или внешней среды, которое может быть зарегистрировано и обработано системой (например, изменение статуса заказа, нажатие кнопки пользователем, достижение определённого времени).
Именовать события необходимо в прошедшем времени, а формулировать — с использованием бизнес-терминов, чтобы обеспечить самую низкую связанность между компонентами приложения.
События могут отражать какое-то изменение в бизнес-домене. Например, бронирование комнаты в отеле может привести к событию
room_booked, а отмена заказа — к событию order_canceled. Также события могут отражать действие пользователя, например, пользователь залогинился — user_logged_in, пользователь просмотрел товар — user_checked_item.Каждое событие должно быть записано в поток событий (event stream). В потоки оптимально объединять события, которые относятся к одной доменной сущности, хотя иногда границы объединения могут быть и чуть шире — это зависит от архитектурных решений в конкретной ситуации.
💡 При этом и события, и потоки событий запрещено изменять: ни содержимое самих событий, ни их порядок в потоке.
Это важно по нескольким причинам:
- Одно событие может быть обработано множеством независимых потребителей. Изменение состояния события после изначальной публикации может привести к нарушению консистентности системы в целом, так как разные потребители прочитают одно и то же событие с разными значениями. Например, изменение постфактум стоимости заказа в событии
order_placedможет привести к тому, что платёжная система обработает платёж на основании первого значения и уже не обнаружит обновлённого, так как посчитает событие обработанным. Это приведёт либо к излишне списанным средствам с клиента, либо к недостаче в бухгалтерии. - Поток событий отражает последовательность происходящих изменений, которые, в свою очередь, определяют состояние какой-либо сущности на момент проведения следующего изменения. Нарушение их порядка может значительно повлиять на бизнес-процесс. Например, поменяв местами событие блокировки расходных операций с банковской карты и событие списания средств, можно позволить мошенникам обналичить украденные средства.
- Потребитель может в любой момент прочитать заново весь поток событий с самого старого доступного в брокере события, например, чтобы восстановить состояние после перезагрузки приложения. Поменяв состояния или порядок событий местами, мы рискуем нарушить целостность данных в системе и привести к последствиям, которые будет сложно исправить.События долговечны и воспроизводимы — они сохраняются до тех пор, пока нужны. Журнал событий можно использовать для получения состояния объекта в любой интересующий момент времени. Развитие этой идеи мы подробнее разберём в уроке про Event Sourcing.
Базовые понятия в Event-Driven архитектуре
- Producer, или поставщик событийКогда происходит изменение состояния сущности данных, за которую отвечает компонент, или инициируется действие в нём, поставщик генерирует сообщение с данными о событии и отправляет их в брокер событий.
- Consumer, или потребитель событийПодписывается на интересующие его потоки события и реагирует на них, выполняя определённые действия, которые также могут породить публикацию события. То есть один компонент системы может быть одновременно потребителем и поставщиком. При этом потребитель может принять решение проигнорировать или отфильтровать событие, но должен подтвердить его обработку. Однако важно помнить об идемпотентности обработки событий.
- Event Broker, или брокер событийПромежуточный компонент, который принимает события от поставщиков и передаёт их потребителям. При публикации и чтении компоненты оперируют топиками (наименования могут отличаться), которые представляют собой логическое разделение сообщений на группы. Технически брокеры не ограничивают использование одного топика для нескольких потоков событий, хотя так делать не рекомендуется.На рынке существует множество технологий, которые реализуют эту функциональность, самые популярные — Kafka и RabbitMQ. К брокерам предъявляются очень высокие требования отказоустойчивости и производительности: в некоторых системах количество событий достигает нескольких миллионов в секунду.
Многие современные системы проектируются в микросервисной архитектуре. Одним из её базовых принципов является независимость компонентов. Часто это достигается за счёт репликации (или дублирования) данных между компонентами. Расскажем про это подробнее.
Использование событий для репликации
Репликация позволяет микросервису при обработке запроса не обращаться к другому сервису. Это повышает устойчивость к сбоям других компонентов.
Помимо снижения зависимости, репликация может быть инструментом повышения производительности и снижения времени отклика, так как не будет затрачиваться время на обращение к другим компонентам по сети.
Репликация может быть эффективным инструментом, хотя и сопряжена со сложностями поддержания согласованного состояния. Отставание в реплике одного компонента может приводить к нарушениям работы системы в целом.
С развитием EDA события стали одним из самых эффективных способов репликации. Компоненты получают возможность подписаться на интересующий их поток событий и собирать максимально актуальную реплику данных на своей стороне. Например, сервис, отвечающий за создание заказов в онлайн-магазине, может реплицировать нужные ему данные о клиентах на основании событий, которые публикует сервис, отвечающий за учёт данных о клиентах.
Преимущества использования событий для репликации данных:
- Децентрализация и отказоустойчивостьНе требуется централизованное хранилище для получения данных, отчего повышается отказоустойчивость системы.
- Гибкость в обработке данныхКомпоненты могут обрабатывать и хранить события данных так, как это больше подходит для их бизнес-логики. В том числе используя свои модели данных и альтернативные хранилища данных.
- Снижение связанностиРепликация по событиям снижает связанность между сервисами и, например, даёт возможность принимать заказы, даже если сервис клиентов недоступен.
- Улучшение масштабируемостиПоскольку компоненты общаются асинхронно через события, систему легче масштабировать, добавляя новые экземпляры компонентов или распределяя нагрузку между ними.
Сложности Event-Driven архитектуры
- Сложность системыПорой в рамках EDA количество компонентов и событий, которыми они обмениваются, становится слишком большим. Такую систему сложно развивать, могут возникать циклические зависимости и другие вопросы. Чтобы этого избежать, рекомендуется уделять внимание документированию: наглядное понимание зависимостей между сервисами позволит вовремя увидеть проблемные места.
- Гранулярность событийЕсли события спроектированы слишком маленькими, количество взаимодействий между сервисами может значительно увеличиться. Это, в свою очередь, приведёт к завышенному потреблению ресурсов. Также мелкая гранулярность событий усложняет поддержание согласованного состояния в случаях, когда в рамках одной операции публикуется сразу несколько событий об изменении одной сущности данных. Это увеличивает шанс потери или ошибок при обработке событий.Старайтесь не приводить к таким ситуациям: когда в рамках одного запроса на изменение одной сущности данных сервис публикует больше одного события, нужно проанализировать решение ещё раз. В некоторых ситуациях это может быть обоснованным, но чаще всего — нет.
- Упорядочивание событийВо многих процессах важен правильный порядок событий. Его нарушение может привести к негативным последствиям для бизнеса. Например, нарушение порядка финансовых транзакций несёт серьёзные финансовые и репутационные риски для банка. В условиях распределённых приложений и асинхронной обработки необходимо уделять особое внимание порядку событий и учитывать это при разработке системы. Например, в случае использования Kafka для обмена событиями важно выбрать правильный ключ для группировки событий по партициям, что позволит потребителям обрабатывать их именно в порядке публикации.
- Нарушение согласованностиПотребители обрабатывают события независимо друг от друга и от поставщика, поэтому существует высокая вероятность нарушения согласованности состояний между ними. Большие расхождения могут быть результатом сбоя или недостаточной производительности при обработке, когда потребитель «не успевает» за поставщиком.Асинхронное взаимодействие между компонентами всегда подразумевает итоговую согласованность. Уделяйте внимание конфигурации брокеров событий для обеспечения нужного уровня надёжности и гарантии доставки. Однако 100% гарантию доставки обеспечить практически невозможно, поэтому ваша система и процессы сопровождения должны учитывать это и уметь восстанавливать актуальное состояние.
Когда EDA не стоит использовать
Хотя EDA предлагает множество преимуществ, существуют сценарии и условия, при которых её использование может оказаться неоправданным или даже вредным. Определение таких ситуаций помогает выбрать наиболее подходящую архитектурную модель для конкретного проекта или задачи:
- Простые или монолитные приложенияПри разработке небольших и простых или монолитных приложений, где нет сложных потоков данных или множества взаимодействующих сервисов, введение EDA может привести к неоправданному усложнению системы. В таких случаях прямое и синхронное взаимодействие компонентов может быть более эффективным.
- Высокие требования к согласованностиХотя системы могут быть настроены на надёжную доставку сообщений, обеспечение 100% гарантии доставки в асинхронной системе сложнее, чем в синхронной. Для случаев, когда потеря сообщений недопустима, может потребоваться рассмотреть альтернативные подходы.
- Ограниченность в ресурсахРазработка EDA требует использования брокеров сообщений и, как правило, приводит к частому дублированию данных между сервисами, что требует дополнительных вычислительных, сетевых и хранилищных ресурсов для обработки и хранения событий. В средах с ограниченными ресурсами или для проектов с ограниченным бюджетом подход EDA может оказаться слишком ресурсоёмким.
- Неопределённость в требованияхЕсли проект находится на стадии, когда бизнес-требования определены не полностью, внедрение EDA сразу может увеличить сложность и затруднить последующие изменения в логике обработки событий. В таких случаях целесообразнее начать с более простой архитектуры и постепенно переходить к EDA, когда требования станут яснее.
Итак, команда сервиса «ОнлиСхемы» после долгих обсуждений решила внедрить использование EDA в архитектуру приложения. Таким образом:
- Сервис клиентов будет публиковать события о создании, изменении и удалении карточки пользователя, откуда сервисы досок и нотификации смогут получать интересующие их изменения и сохранять в локальную реплику:
- Сервис досок интересуют только события о создании и удалении карточки: это нужно, чтобы поддерживать актуальный список пользователей в своей реплике и использовать, например, при отметке в комментарии.
- Сервис нотификаций также будет содержать реплику данных о клиентах, но уже с расширенной информацией: контактным email, флагом разрешения на отправку уведомлений.
Такой подход позволяет решить сразу несколько проблем:
- Разрыв синхронной связи между сервисами, что повышает отказоустойчивость приложения в целом, так как недоступность одного из сервисов не вызывает полного или частичного отказа его потребителей.
- Снижение времени отклика на операции, в рамках которых ранее выполнялись запросы к другим сервисам. Теперь сервис досок публикует сообщение, а сервис нотификаций уже асинхронно обрабатывает это событие.
- Снижение влияния роста нагрузки на сервис досок. Благодаря использованию реплик данных о клиентах синхронные запросы в сервис пользователя требуются только при работе самого пользователя в личном кабинете. В то же время благодаря асинхронной обработке событий сервисом нотификаций команда может регулировать количество реплик, исходя из требований к максимальной задержке отправки уведомления пользователю. Эти изменения позволяют экономить используемые ресурсы памяти и процессора.
Примерно так теперь выглядит архитектура сервиса:
Итоги
- Event-Driven архитектура — это архитектурный подход, который основан на асинхронном взаимодействии между слабо связанными компонентами в системе посредством передачи событий. EDA призвана улучшить масштабируемость, устойчивость и производительность системы.
- Event-Driven архитектура помогает решать проблемы масштабируемости, отказоустойчивости, делает разработку гибче и уменьшает время обработки данных.
- Ключевое понятие EDA — событие. Это значимое изменение в состоянии системы или внешней среды, которое может быть зарегистрировано и обработано системой (например, изменение статуса заказа, нажатие кнопки пользователем, достижение определённого времени).
- EDA предлагает множество преимуществ, однако существуют сценарии и условия, при которых её использование может оказаться неоправданным или даже вредным. Определение таких ситуаций помогает выбрать наиболее подходящую архитектурную модель для конкретного проекта или задачи.