Event Driven Architecture
Тип максимально слабо связанной архитектуры, в которой компоненты обмениваются друг с другом событиями. События отличаются от сообщений тем, что не имеют конкретного адресата, за счет чего и достигается слабая связанность.
Все взаимодействия между сервисами делятся на 3 группы: Query, Event, Command
Запрос (Query)¶
Запрос данных от одного сервиса другим - в общем случае проще всего реализовать через HTTP GET запросы без каких-либо дополнительных особенностей
Команда (Command)¶
Команда от одного сервиса другому на совершения какого-то действия. Например, СоздатьЗаказ или ОтправитьПисьмо. В распределенной системе желательно реализовывать сообщением через какую-нибудь очередь (RabbitMQ, ActiveMQ, Kafka). Имя команды это всегда начинается с глагола. Сервис, исполняющий команду должен быть только один. Посылающая сторона знает какому сервису направляет команду.
У каждой команды обязательно должны быть следующие поля:
- id (UUID) - Идентификатор команды. Большинство очередей гарантируют доставку “at least once”. То есть одно и тоже сообщение может быть доставлено несколько раз. Поле id может помочь сделать дедупликацию команд на стороне приемника
- trace_id/process_id/correlation_id (UUID) - Сквозной идентификатор процесса. Все команды/события выпущенные в рамках одного бизнес процесса должны иметь одинаковый trace_id. Это позволит, например, в логах отфильтровать все, что касается этого процесса по всем сервисам.
Событие (Event)¶
Событие описывает какой-то факт, произошедший в системе. Например ЗаказРазмещен
, ПосылкаДоставлена
. В отличие от команды, событие может быть принято любым количеством сервисов. Сервис, публикующий событие не задумывается о том, кто будет потребителями и что будет сделано ими. Таким образом, при использовании событий достигается слабая связанность между сервисами.
У каждого события обязательно должны быть следующие поля:
- id (UUID) - Идентификатор события.
- trace_id/process_id/correlation_id (UUID) - Сквозной идентификатор процесса.
При использовании подхода Domain Driven Design события можно рассматривать как изменения состояния агрегата. В этом случае добавляются поля:
- aggregateId - Идентификатор агрегата, публикующего событие. Например, в событии PersonNameChanged поле aggregateId будет идентификатором агрегата Person
- version - порядковый номер события в рамках агрегата. Нужен в случае если хочется восстанавливать состояние агрегата по событиям (Event Sourcing). Для этого мы берем все события агрегата, сортируем по полю version и применяем последовательно к пустому агрегату. Плюсы и минусы этого подхода описаны в статьях по Event Sourcing. По опыту ES нужно применять только в случае если больше нет никаких других вариантов реализовать все бизнес требования, так как этот подход очень сложен в реализации.
Проблемы EDA¶
Хотя EDA архитектура приводит к низкой связанности, но в то же время может стать проблемой понимание связей между сервисами в контексте единого процесса (destructive decoupling). Фанатичное желание везде использовать события может привести к тому, что события используются как “пассивно-агресивные” команды. То есть, публикующая событие система ожидает, что какой-то из слушателей произведет определенные действия, но при этом маскирует свои намерения в форму события вместо того, чтобы послать команду напрямую в целевой сервис. Из за этого приходится использовать средства мониторинга или логирования, чтобы понять, какой сервис должен обработать выпущенное событие, чтобы процесс продолжил свое выполнение.
Насколько “толстыми” должны быть события?¶
События могут нести в себе минимум информации, например только id размещенного заказа (паттерн Event Notification). И тогда ожидается, что получивший событие сервис дополнительно будет запрашивать всю нужную ему информацию. Или событие может уже содержать в себе все поля связанной с ним сущности (Event-Carried State Transfer). Выбор между двумя подходами зависит от ситуации. Если стараться напихать в событие все необходимые данные - это может привести к частым изменениям в составе полей события. С другой стороны это повышает надежность (availability) и быстродействие - сервису не нужно после приема события никуда ходить за дополнительной информацией.
В случае Event-Carried State Transfer потребитель получает данные, которые были актуальны на момент публикации этого сообщения и могли быть изменены. Если потребителю важно работать только с актуальной информацией - стоит отказаться от этого паттерна.