Если вы работаете Enterprise-архитектором, вы наверняка слышали о микросервисной архитектуре и работали с ней. И хотя в прошлом вы, возможно, использовали REST в качестве слоя взаимодействия сервисов, всё больше и больше проектов переходят на событийно-ориентированную архитектуру (Event-Driven Architecture, EDA). Давайте разберем плюсы и минусы этого популярного подхода, ключевые проектные решения, которые он влечет за собой, и распространенные антипаттерны.
Что такое событийно-ориентированная микросервисная архитектура?
В событийно-ориентированной архитектуре, когда сервис выполняет какую-то работу, которая может быть интересна другим сервисам, он создает событие это запись о выполненном действии. Другие сервисы потребляют (consume) эти события, чтобы выполнить свои собственные задачи, необходимые в результате этого действия. В отличие от REST, сервисам, создающим запросы, не нужно знать детали реализации сервисов, потребляющих эти запросы.
Простой пример: Когда на сайте электронной коммерции размещается заказ, создается одно событие «заказ размещен» (order placed), которое затем потребляется несколькими микросервисами:
Сервис заказов, который может записать данные заказа в базу данных.
Сервис клиентов, который может создать запись о клиенте.
Сервис платежей, который может обработать оплату.
События могут публиковаться разными способами. Например, они могут отправляться в очередь, гарантирующую доставку соответствующим потребителям, или публиковаться в потоке по модели «pub/sub» (издатель/подписчик), что дает доступ к событию всем заинтересованным сторонам. В любом случае, продюсер (producer) публикует событие, а консьюмер (consumer) получает его и реагирует соответствующим образом. Обратите внимание, что иногда этих участников называют издателем (publisher) и подписчиком (subscriber).
Зачем использовать событийно-ориентированную архитектуру?
EDA предлагает несколько преимуществ по сравнению с REST:
Асинхронность. Архитектуры на основе событий асинхронны и неблокирующи. Это позволяет ресурсам свободно переходить к следующей зад��че сразу после завершения своей части работы, не беспокоясь о том, что происходило до или произойдет после. Также это позволяет ставить события в очередь или буферизировать их, что предотвращает создание обратного давления (back pressure) на продюсеров со стороны консьюмеров и блокировку их работы.
Слабая связность (Loose Coupling). Сервисам не нужны (и они не должны иметь) знания о других сервисах или зависимости от них. При использовании событий сервисы работают независимо, не зная о деталях реализации и транспортных протоколах друг друга. Сервисы в рамках событийной модели можно обновлять, тестировать и развертывать независимо и с меньшими усилиями.
Легкое масштабирование. Поскольку в EDA сервисы разделены и обычно выполняют только одну задачу, становится проще отследить узкие места (bottlenecks) до конкретного сервиса и масштабировать именно его (и только его).
Возможность восстановления. Событийная архитектура с использованием очередей позволяет восстановить потерянную работу путем «проигрывания» (replay) прошлых событий. Это ценно для предотвращения потери данных, когда потребителю необходимо восстановиться после сбоя.
Конечно, у событийно-ориентированных архитектур есть и недостатки. Их легко «переусложнить» (over-engineer), разделяя задачи, которые были бы проще при тесной связности; они могут потребовать значительных первоначальных инвестиций; часто они приводят к дополнительной сложности в инфраструктуре, контрактах сервисов или схемах данных, требуют поддержки полиглотных систем сборки и сложных графов зависимостей.
Пожалуй, самым существенным недостатком и вызовом является управление данными и транзакциями. Из-за своей асинхронной природы событийные модели требуют тщательной обработки несогласованных данных между сервисами и несовместимых версий. Нужно следить за дубликатами событий, и, как правило, такие системы не поддерживают ACID-транзакции «из коробки», полагаясь вместо этого на согласованность в конечном счете (eventual consistency), которую сложнее отслеживать и отлаживать.
Даже с учетом этих недостатков, событийно-ориентированная архитектура обычно является лучшим выбором для микросервисных систем корпоративного уровня. Плюсы: масштабируемость, слабая связность, дружественный к DevOps дизайн перевешивают минусы.
Когда использовать REST
Однако существуют ситуации, когда REST/веб-интерфейс все же предпочтительнее:
Вам нужен синхронный интерфейс запрос/ответ.
Вам нужна удобная поддержка строгих транзакций.
Ваш API открыт для публичного использования.
Ваш проект небольшой (REST намного проще в настройке и развертывании).
Важнейший выбор при проектировании — Фреймворк обмена сообщениями
Как только вы остановились на событийно-ориентированной архитектуре, пришло время выбрать событийный фреймворк. То, как ваши события производятся и потребляются, является ключевым фактором вашей системы. Существуют десятки проверенных фреймворков, и выбор правильного требует времени и исследования.
Базовый выбор сводится к обработке сообщений (Message Processing) или потоковой обработке (Stream Processing).
Обработка сообщений (Message Processing)
В традиционной обработке сообщений компонент создает сообщение, а затем отправляет его конкретному (и обычно единственному) получателю. Принимающий компонент, который до этого простаивал в ожидании, получает сообщение и действует соответствующим образом. Обычно, когда сообщение приходит, принимающий компонент выполняет один процесс. После этого сообщение удаляется.
Типичный пример архитектуры обработки сообщений это Очередь сообщений (Message Queue). Хотя большинство новых проектов используют потоковую обработку (описанную ниже), архитектуры с использованием очередей сообщений (или событий) по-прежнему популярны. Очереди сообщений обычно используют систему брокеров «store and forward» (сохрани и перешли), где события путешествуют от брокера к брокеру, пока не достигнут нужного потребителя. ActiveMQ и RabbitMQ это два популярных примера фреймворков очередей сообщений. Оба проекта имеют годы доказанной эффективности и устоявшиеся сообщества.
Потоковая обработка (Stream Processing)
С другой стороны, при потоковой обработке компоненты генерируют события, когда достигают определенного состояния. Другие заинтересованные компоненты слушают этот поток событий и реагируют соответствующим образом. События не нацелены на определенного получателя, а доступны всем заинтересованным компонентам.
При потоковой обработке компоненты могут реагировать на несколько событий одновременно и применять сложные операции к нескольким потокам и событиям. Некоторые потоки включают персистентность (persistence), где события остаются в потоке столько, сколько необходимо.
С помощью потоковой обработки система может воспроизвести историю событий, подключиться уже после того, как событие произошло, и все равно отреагировать на него, и даже выполнять вычисления в «скользящем окне» (sliding window computations). Например, можно рассчитать среднюю загрузку ЦП за минуту из потока посекундных событий.
Один из самых популярных фреймворков потоковой обработки это Apache Kafka. Это зрелое и стабильное решение, используемое во многих проектах. Его можно считать стандартным решением промышленного уровня для обработки потоков. У Kafka огромная база пользователей, помогающее сообщество и развитый набор инструментов.
Другие варианты
Существуют и другие фреймворки, предлагающие либо комбинацию потоковой обработки и сообщений, либо свои уникальные решения. Например, Pulsar это более новое предложение от Apache, это open-source система обмена сообщениями pub/sub, которая поддерживает как потоки, так и очереди событий, и всё это с чрезвычайно высокой производительностью. Pulsar богат функционалом, он предлагает мультитенантность и георепликацию и соответственно сложен. Говорят, что Kafka нацелена на высокую пропускную способность (throughput), тогда как Pulsar на низкую задержку (latency).
NATS это альтернативная система pub/sub с «синтетическими» очередями. NATS спроектирован для отправки небольших, частых сообщений. Он предлагает как высокую производительность, так и низкую задержку. Однако NATS считает определенный уровень потери данных приемлемым, ставя производительность выше гарантий доставки.
Другие аспекты проектирования
После выбора фреймворка стоит рассмотреть еще несколько проблем:
Event Sourcing
Сложно реализовать комбинацию слабосвязанных сервисов, различных хранилищ данных и атомарных транзакций. Паттерн, который может помочь это Event Sourcing. В Event Sourcing обновления и удаления никогда не выполняются непосредственно над данными; вместо этого изменения состояния сущности сохраняются как серия событий.
CQRS
Event Sourcing вносит другую проблему: так как состояние нужно собирать из серии событий, запросы (queries) могут быть медленными и сложными. CQRS (Command Query Responsibility Segregation) это проектное решение, которое предполагает раздельные модели для операций вставки (команд) и операций чтения (запросов).
Обнаружение информации о событиях (Event Discovery)
Одна из самых больших проблем в событийно-ориентированной архитектуре это каталогизация сервисов и событий. Где найти описания и детали событий? Какова причина события? Какая команда создала событие? Активно ли они над ним работают?
Управление изменениями
Изменится ли схема события? Как изменить схему события, не сломав другие сервисы? Ответы на эти вопросы становятся критичными по мере роста количества сервисов и событий.
Быть хорошим потребителем событий это значит писать код, готовый к изменению схем. Быть хорошим продюсером событий это значит осознавать, как изменения вашей схемы повлияют на другие сервисы, и создавать хорошо спроектированные события с четкой документацией.
On-Premise против Hosted-решений
Независимо от вашего фреймворка, вам также нужно будет решить: разворачивать ли его самостоятельно на своем оборудовании (on-premise), а брокерами сообщений управлять непросто, особенно с учетом высокой доступности или использовать управляемые (hosted) сервисы, такие как Apache Kafka на Heroku или Confluent Cloud.
Антипаттерны
Как и любая архитектура, EDA имеет свой набор антипаттернов. Вот несколько, которых стоит остерегаться:
Хорошего понемножку. Будьте осторожны, не увлекайтесь созданием событий чрезмерно. Создание слишком большого количества событий приведет к ненужной сложности взаимодействия между сервисами, увеличит когнитивную нагрузку на разработчиков, усложнит развертывание и тестирование, а также вызовет перегрузку у потребителей событий. Не каждый метод должен быть событием.
Обобщенные (Generic) события. Не используйте обобщенные события, ни по названию, ни по назначению. Вы хотите, чтобы другие команды понимали, зачем существует ваше событие, для чего его использовать и когда. События должны иметь конкретную цель и называться соответствующим образом. События с общими названиями или общие события с запутанными флагами внутри вызывают проблемы.
Сложные графы зависимостей. Следите за сервисами, которые зависят друг от друга и создают сложные графы зависимостей или петли обратной связи. Каждый сетевой хоп добавляет дополнительную задержку к исходному запросу, особенно трафик «север-юг» (выходящий за пределы дата-центра).
Зависимость от гарантированного порядка, доставки или побочных эффектов. События асинхронны; следовательно, допущения о порядке или отсутствии дубликатов не только добавят сложности, но и сведут на нет многие ключевые преимущества событийной архитектуры. Если ваш консьюмер имеет побочные эффекты (например, добавление значения в базу данных), вы можете потерять возможность восстановления путем проигрывания (replay) событий.
Преждевременная оптимизация. Большинство продуктов начинаются с малого и растут со временем. Хотя вы можете мечтать о будущих потребностях масштабирования до уровня огромной корпорации, если ваша команда мала, дополнительная сложность EDA может фактически замедлить вас. Вместо этого рассмотрите возможность проектирования системы с простой архитектурой, но с необходимым разделением ответственности (separation of concerns), чтобы вы могли заменить её по мере роста ваших потребностей.
Ожидание, что Event-Driven исправит всё. На менее техническом уровне: не ждите, что событийно-ориентированная архитектура решит все ваши проблемы. Хотя эта архитектура, безусловно, может улучшить многие области технической дисфункции, она не может исправить фундаментальные проблемы, такие как отсутствие автоматизированного тестирования, плохая коммуникация в команде или устаревшие практики DevOps.
Узнать больше
Понимание плюсов и минусов событийно-ориентированных архитектур, а также наиболее распространенных проектных решений и проблем это важная часть создания наилучшего дизайна системы.
Если вы хотите узнать больше, ознакомьтесь с эталонной событийно-ориентированной архитектурой, которая позволяет развернуть рабочий проект магазина по продаже вымышленного кофе одним кликом.
