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


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

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

Однак микросервисы є не таким вже й простим і універсальним рішенням. Зокрема, моделі предметної області, транзакції і запити стійкі до розподілу за функціональною ознакою. У результаті розробка транзакційних бізнес-додатків з використанням микросервисной архітектури є досить складним завданням. У цій статті ми розглянемо спосіб розробки микросервисов, при якому ці проблеми вирішуються за допомогою паттерни проектування на основі предметної області (Domain Driven Design), Event Sourcing і CQRS.

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

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


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

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

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



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

На жаль, ділити програму на сервіси не так просто, як здається. Як вже згадувалося раніше, кілька різних аспектів додатків — моделі предметної області, транзакції і запити — важко піддаються такому поділу. Давайте подивимося на причини цих труднощів.

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



Проблемою в поділі моделі предметної області на сервіси є те, що класи часто посилаються один на одного. Наприклад, Замовлення посилається на Клієнта, який його зробив, а Позиції замовлення посилаються на Товари. Що ж робити з посиланнями, порушують межі сервісів? Пізніше ми побачимо, як поняття агрегату (Aggregate) з DDD (Domen 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-баз даних, не підтримують навіть звичайні операції, не кажучи вже про розподілених. Важливе значення має і підтримку цілісності, так що нам потрібно інше рішення. Нижче ми побачимо, що рішенням є використання подійно-орієнтованою (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) і, можливо, одного і більше інших пов'язаних з ними об'єктів-сутностей і об'єктів-значень (Object Value). Наприклад, модель предметної області для інтернет-магазину містить такі агрегати, як Замовлення Клієнт. Агрегат Замовлення складається з кореневої сутності Замовлення, одного чи декількох об'єктів-значень Позиція замовлення поряд з іншими об'єктами-значеннями, такими як Вартість, Адреса доставки Платіжні реквізити. Агрегат Клієнт складається із сутності Клієнт і декількох об'єктів-значень, таких як Інформація про доставку Інформація про платіж.



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

Межагрегатные зв'язку повинні використовувати первинні ключі
Перше правило полягає в тому, що агрегати завжди посилаються один на одного через унікальний ідентифікатор (наприклад, первинний ключ) замість прямих посилань на об'єкти. Наприклад, Замовлення посилається на свого Клієнта, використовуючи 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.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.