14 февраля в 10:39

Разработка транзакционных микросервисов с помощью агрегатов, Event Sourcing и CQRS (Часть 1) перевод


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

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

Однако микросервисы являются не таким уж простым и универсальным решением. В частности, модели предметной области, транзакции и запросы удивительно устойчивы к разделению по функциональному признаку. В результате разработка транзакционных бизнес-приложений с использованием микросервисной архитектуры является довольно сложной задачей. В этой статье мы рассмотрим способ разработки микросервисов, при котором эти проблемы решаются с помощью паттерна проектирования на основе предметной области (Domain Driven Design), Event Sourcing и CQRS.

Основные тезисы:

  • Микросервисная архитектура функционально разделяет приложение на отдельные сервисы, каждый из которых соответствует определенному бизнес-объекту или бизнес-процессу.
  • Одной из ключевых проблем при разработке микросервис-ориентированных приложений является то, что транзакции, модели предметной области (Domain models) и запросы «сопротивляются» разделению на отдельные сервисы.
  • Модель предметной области (Domain model) может быть разложена на агрегаты (Aggregates) в рамках паттерна проектирования на основе предметной области (Domain Driven Design).
  • Каждый сервис представляет собой модель предметной области, состоящую из одного или нескольких агрегатов DDD.
  • В рамках сервиса каждая транзакция создает или обновляет один-единственный агрегат.
  • События (Events) используются для поддержания согласованности между агрегатами (и сервисами).

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

Проблемы разработки микросервисов


Модульность имеет важное значение при разработке больших и сложных приложений.
Большинство современных приложений слишком велики, чтобы их мог создать один разработчик. Кроме того, они слишком сложны, чтобы быть полностью понятыми одним человеком. Приложение должно быть разделено на модули, которые разрабатываются целой командой разработчиков. В монолитном приложении модульность определяется конструкциями используемого языка программирования, например, Java-пакетами. Такой подход, как правило, не очень хорошо работает на практике. Долгоживущие монолитные приложения обычно вырождаются в то, что известно как антипаттерн Big balls of mud.

Микросервисная архитектура использует сервис в качестве единицы модульности. Каждый сервис — это отдельный бизнес-процесс или бизнес-объект, который что-то делает для получения конкретного результата. Например, интернет-магазин, используя эту архитектуру, мог бы состоять из таких микросервисов, как Сервис Заказов (Order Service), Сервис Клиентов (Customer Service), Каталог товаров (Catalog Service) и т.д.



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

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

Проблема № 1: Разделение модели предметной области


Паттерн «модель предметной области» (Domain model) является хорошим способом реализации сложной бизнес-логики. Модель предметной области для интернет-магазина будет включать в себя такие классы, как Заказ, Позиция заказа, Клиент, Товар. В микросервисной архитектуре классы Заказ и Позиция заказа являются частью сервиса Заказ, класс Клиент является частью сервиса Клиент, а класс Товар принадлежит сервису Каталог.



Проблемой в разделении модели предметной области на сервисы является то, что классы часто ссылаются друг на друга. Например, Заказ ссылается на Клиента, который его сделал, а Позиции заказа ссылаются на Товары. Что же делать со ссылками, нарушающими границы сервисов? Позже мы увидим, как понятие агрегата (Aggregate) из DDD (Domain Driven Design) решает эту проблему.

Микросервисы и базы данных


Отличительной особенностью микросервисной архитектуры является то, что данные, принадлежащие сервису, доступны только через API этого сервиса. Например, в интернет-магазине Сервис Заказов имеет базу данных, которая включает в себя таблицу ORDERS, а Сервис Клиентов имеет свою базу данных, которая включает в себя таблицу CUSTOMERS. Из-за такой инкапсуляции сервисы слабо связаны, и разработчик может изменить схему своего сервиса без необходимости координировать свои действия с разработчиками, работающими над другими сервисами. Во время выполнения приложения сервисы изолированы друг от друга. Например, сервис никогда не будет ожидать окончания блокировки базы данных, принадлежащей другому сервису. С другой стороны, функциональное разделение базы данных затрудняет поддержание целостности данных, а также реализацию многих типов запросов.

Проблема № 2: Транзакции


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

BEGIN TRANSACTION
…
SELECT ORDER_TOTAL FROM ORDERS WHERE CUSTOMER_ID = ?
…
SELECT CREDIT_LIMIT FROM CUSTOMERS WHERE CUSTOMER_ID = ?
…
INSERT INTO ORDERS …
…
COMMIT TRANSACTION

К сожалению, мы не можем использовать такой простой подход для поддержания согласованности данных при микросервис-ориентированном подходе. Таблицы ORDERS и CUSTOMERS принадлежат различным сервисам и могут быть доступны только через API. Они даже могут находиться в различных базах данных.

В данном случае традиционным решением будут распределенные транзакции, но для современных приложений это неподходящая технология. Теорема CAP требует от разработчика сделать выбор между доступностью (Availability) и согласованностью данных (Consistency), и доступность, как правило, является предпочтительным выбором. Кроме того, многие современные технологии, такие как большинство NoSQL-баз данных, не поддерживают даже обычные транзакции, не говоря уже о распределенных. Важное значение имеет и поддержание целостности, так что нам нужно другое решение. Ниже мы увидим, что решением является использование cобытийно-ориентированной (Event-driven, message-driven) архитектуры, основанной на Event Sourcing.

Проблема № 3: Запросы к базе данных


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

SELECT * FROM CUSTOMER c, ORDER o
WHERE
  c.id = o.ID
  AND o.ORDER_TOTAL > 100000
  AND o.STATE = 'SHIPPED'
  AND c.CREATION_DATE > ?

Мы не можем использовать этот вид запроса в микросервис-ориентированном интернет-магазине. Как уже упоминалось ранее, таблицы ORDERS и CUSTOMERS принадлежат различным сервисам и могут быть доступны только через API. Некоторые сервисы могут даже не использовать SQL-базу. Но можно использовать подход, известный как Event Sourcing, что делает поиск информации еще более сложной задачей.

Дальше мы увидим, что решением является сохранение материализованных представлений с помощью подхода, известного как Command Query Responsibility Segregation (CQRS). Но сначала, давайте рассмотрим вопрос Domain-Driven Design (DDD) при разработке микросервисов.

DDD-агрегаты как строительные блоки микросервисов


Как видите, есть несколько проблем, которые необходимо решить ради успешной разработки приложений с использованием микросервисов. Решение некоторых из этих проблем может быть найдено в обязательной для прочтения книге Эрика Эванса “Domain-Driven Design”. В ней описывается подход к проектированию сложного программного обеспечения, что очень полезно при разработке микросервисов. В частности, Domain-Driven Design позволяет создавать модульную модель предметной области, которая может быть распределена по сервисам.

Что такое агрегаты?


В Domain-Driven Design Эванс определяет несколько строительных блоков для моделей предметной области. Многие из них стали частью повседневного языка разработчиков, включая Entity, Value object, Service, Repository и т.д. Однако, один строительный блок — агрегат — был, в основном, проигнорирован разработчиками, за исключением DDD-пуристов. Но оказывается, что агрегаты являются ключом к разработке микросервисов.

Агрегат представляет собой кластер объектов предметной области, которые можно рассматривать как единое целое. Он состоит из корневого объекта-сущности (Entity) и, возможно, одного и более других связанных с ними объектов-сущностей и объектов-значений (Value Object). Например, модель предметной области для интернет-магазина содержит такие агрегаты, как Заказ и Клиент. Агрегат Заказ состоит из корневой сущности Заказ, одного или нескольких объектов-значений Позиция заказа наряду с другими объектами-значениями, такими как Стоимость, Адрес доставки и Платежные реквизиты. Агрегат Клиент состоит из сущности Клиент и нескольких объектов-значений, таких как Информация о доставке и Информация о платеже.



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

Межагрегатные связи должны использовать первичные ключи


Первое правило заключается в том, что агрегаты всегда ссылаются друг на друга через уникальный идентификатор (например, первичный ключ) вместо прямых ссылок на объекты. Например, Заказ ссылается на своего Клиента, используя CustomerId, а не ссылку на объект клиента. Аналогичным образом, Позиция заказа ссылается на Товар, используя ProductID.



Такой подход сильно отличается от традиционного, при котором внешние ключи в модели предметной области считаются плохой практикой. Использование идентификатора, а не ссылки на объект, означает, что агрегаты слабо связаны. Вы можете легко разместить различные агрегаты в различных сервисах. На самом деле, бизнес-логика сервиса состоит из модели предметной области, которая представляет собой набор агрегатов. Например, OrderService содержит агрегат Заказ, а CustomerService содержит агрегат Клиент.

Одна транзакция создает или обновляет один агрегат


Второе правило заключается в том, что транзакция может создать или обновить только один агрегат. Когда я впервые прочитал об этом правиле много лет назад, это не имело никакого смысла! В то время я разрабатывал традиционные монолитные приложения на основе РСУБД, и поэтому транзакции могли обновлять произвольные данные. Сегодня же это ограничение идеально подходит для микросервисной архитектуры. Это гарантирует, что транзакция содержится внутри сервиса. Это ограничение также соответствует ограничениям транзакций большинства NoSQL-баз данных.

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

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

Например, выше сказано, что в модели предметной области интернет-магазина Заказ и Клиент — это отдельные агрегаты. Альтернативой является сделать Заказ частью агрегата Клиент. Преимуществом большого агрегата Клиент является то, что приложение сможет атомарно обеспечивать проверку кредитного лимита. Недостаток подхода в том, что он сочетает в себе функциональность Заказа и Клиента в одном и том же сервисе. Это снижает масштабируемость, так как транзакции, обновляющие разные заказы одного и того же клиента, не смогут выполняться параллельно. Кроме того, два пользователя могут вступить в конфликт, если они попытаются редактировать различные заказы одного клиента. С увеличением количества заказов загрузка агрегата Клиент будет становиться всё более дорогой. Из-за этих проблем лучше делать агрегаты настолько маленькими, насколько это возможно.

Даже соблюдая требование по созданию или обновлению транзакцией только одного агрегата, приложения по-прежнему должны поддерживать согласованность между агрегатами. Например, сервис Заказ должен проверить, что новый агрегат Заказ не превысит совокупный кредитный лимит клиента. Есть несколько различных способов поддержания согласованности. Одним из вариантов является обман приложения и создание/обновление нескольких агрегатов в одной транзакции. Это возможно только тогда, когда все агрегаты принадлежат одному и тому же сервису и сохраняются в одной и той же РСУБД. Другой, более правильный вариант заключается в поддержании согласованности между агрегатами с использованием событийно-ориентированного подхода.

Использование событий для поддержания согласованности данных


В современном приложении есть различные ограничения по транзакциям, которые делают его сложным для поддержания согласованности данных в сервисах. Каждый сервис имеет свои собственные данные, но использование распределенных транзакций не является жизнеспособным вариантом. Кроме того, многие приложения используют NoSQL-базы, которые не поддерживают даже обычные локальные транзакции, не говоря уже о распределенных. Следовательно, современное приложение должно использовать управляемую событиями модель транзакции, известную как «согласованность в конечном счете» (Eventually Consistent).

Что такое событие (Event)?


Как гласит словарь Merriam-Webster (и Капитан Очевидность), «событие» — это то, что происходит (случается):



В этой статье мы определяем событие предметной области (Domain Event) как то, что произошло с агрегатом. Событие обычно представляет собой изменение состояния. Рассмотрим, например, агрегат Заказ. События, изменяющие его состояние, включают в себя Заказ создан, Заказ отменен, Заказ отправлен. События могут представлять собой попытки нарушить бизнес-правила, например, кредитный лимит Клиента.

Использование событийно-ориентированной (Event-Driven) архитектуры


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

Интернет-магазин проверяет кредитный лимит клиента при создании заказа, используя следующую последовательность шагов:

  1. Агрегат Заказ, который создается со статусом NEW, публикует событие OrderCreated.
  2. Агрегат Клиент получает уведомление о событии OrderCreated, резервирует кредит для заказа и публикует событие CreditReserved.
  3. Агрегат Заказ получает уведомление о событии CreditReserved и меняет свой статус на УТВЕРЖДЕН.

Если кредитная проверка терпит неудачу из-за нехватки средств, агрегат Клиент публикует событие CreditLimitExceeded. Это событие не отражает изменения состояния, а представляет собой неудачную попытку нарушить бизнес-правила. Агрегат Заказ получает уведомление об этом событии и меняет свое состояние на ОТМЕНЕН.

Микросервисная архитектура как сеть событийно-управляемых агрегатов


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



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

Резюме


Микросервисная архитектура функционально разделяет приложение на сервисы, каждый из которых соответствует определенному бизнес-объекту или бизнес-процессу. Одной из основных проблем при разработке микросервисных бизнес-приложений является то, что транзакции, модели предметной области и запросы противостоят разделению на сервисы. Вы можете разделить модель предметной области, применяя концепцию «агрегата» из Domain Driven Design. Бизнес-логика каждого сервиса представляет собой модель предметной области, состоящую из одного или нескольких агрегатов DDD.

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

Во второй части статьи мы рассмотрим, как реализовать надежную архитектуру, управляемую событиями с помощью Event Sourcing, а также как реализовать запросы в микросервисной архитектуре с помощью CQRS.
Автор: @NIX_Solutions Chris Richardson
NIX Solutions
рейтинг 113,22
Похожие публикации

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

  • 0
    Согласно схеме и в Customer Service и в Order Service есть DeliveryInfo и PaymentInfo. Хотелось бы узнать побольше про этот нюанс. Это разные инфо или одни и теже?

    Посылать евенты по каждому чиху плохая идея. Со временем этот подход превращает жизнь в ад. Надо хорошо подумать прежде так делать. Если создание заказа требует проверки финансов, вызовите этот финансовый сервис сразу же. Поставьте таймаут в 300 мс и не партесь. Если сервис не ответил или запишите в базу и попробуйте позже или пошлите эвент. А лутше обеспечте высокую доступность этого финансового сервиса, если уж он так критичен. Как другой вариант можно запилить WorkflowService который будет все это делать в рамках одного бизнес процесса.
  • +1
    Да, мне тоже кажется, что подобная архитектрура не слишком хороша… Я бы даже сказал, что она в значительной степени нивелирует выгоды микросервисной архитектуры (и в частности увеличиваетс связанность сервисов).

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

    Также сравнительно недавно я разрабатывал… ммм… компонент скажем так… которые позволяет реализовать нечто вроде транзакций в микросервисной архитектуре… Разработка была выполнена по мотивам какой-то статьи, описываюшей саму идею метода… Обязательна строгая реализация REST, реализация представляет собой что-то вроде прокси сервера, обращение к которому реализуется прозрачно, на уровне API Gateway, доступ к модифицируемому ресурсу ограничивается (по URL), блокировки снимаются по завершении этой псевдотранзакции, либо по истечении предопределенного timeout. Сам этот прокси сервер тоже является микросервисом, причем распределенным. В общем получилось неплохо, хотя всех проблем оно не решает (да и не должно)
  • +1
    Микросервисная архитектура оправдывает себя на более высоком уровне, пример:
    — сервис авторизации OAuth2
    — биллинг
    — процессинг
    — СХД
    — шина
    как правило над подобными системами трудятся > 10 разработчиков.
  • 0
    В своем решении отказался от аггрегатов, вместо этого реализовал т.н. тактики (велосипед). Тактика — это набор бизнес действий формирующих план по которому будет выполнено изменение модели.
    Тактики реагируют на внешние события, например, сервис интеграции с 1С говорит что сотрудник уволен, это вызывает исполнение определенной тактики, в которой отмечается что сотрудник уволен, и выполнение которой вызывает ряд следующих тактик, в которых пересчитываются отпуска, и все прочие связанные с сотрудником вещи. На выходе получаю список операций в виде — добавить в базу, обновить в базе, удалить из базы и т.п.
    Затем список оптимизируется, из него выкидываются взаимоисключающие операции и операции не приводящие к изменению модели.
    И в конце концов список исполняется в едином transaction scope, при этом каждая операция может относится к разным базам, в том числе nosql. В случае сбоя выполняется откат в каждой из них. Для тех баз которые не поддерживают транзакции был написан декоратор добавляющий их.

    Такой подход позволяет не создавать новые сущности и разносить логику на любой масштаб (по тактикам). Правда он же подразумевает что есть сервис который имеет доступ ко всем базам, как и было в моем случае.
    • 0
      Интересная идея — «тактики»
      Сразу появились мысли насчет «расширить и углубить»…
      буду обдумывать — спасибо за идею
    • 0
      По описанию это очень похоже на CQRS Saga. Которым, кстати, ничто не мешает быть и аггрегатом.
  • +1
    Агрегат представляет собой кластер объектов предметной области, которые можно рассматривать как единое целое
    Одна транзакция создает или обновляет один агрегат

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

    • +1
      С большой вероятностью он получится сервисом, владеющим одним агрегатом. Если «хочет» называться микросервисом.
      • 0

        Ну да… И если "хочет" называть "агрегатом" то, что "можно рассматривать как единое целое" :-)

  • 0
    Интересная архитектура. Жаль экспериментировать с ней дорого.

    Подскажите какой-нибудь open source проект, использующий такой подход, или статью с описанием такого подхода в живом проекте. Главное чтобы не фреймворк (такое-то не очень сложно сделать), а реальный конечный продукт.

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

Самое читаемое Разработка