Власний CQRS-фреймворк для мікросервісних продуктів: досвід Intellias

9 хв. читання

Автор: Сергій Селецький, Senior Solution Architect в Intellias. У написанні статті також брали участь Павло Ритіков, .NET Engineering Lead в Intellіas, та Олег Галай, Senior .NET Developer в Intellіas.

Ми створили унікальний архітектурний фреймворк, який дозволяє швидко і якісно розробляти cloud-native мікросервіси з використанням CQRS, DDD, Event Sourcing. Це реалізація відносно нового підходу CQRS/ES Serverless.

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

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

З чого все почалось

Компанії потрібна була централізована внутрішня система для керування всіма процесами. Вона отримала назву IntelliGrowth. На ринку є схожі продукти, наприклад, Axoniq чи EventFlow, але вони потребують суттєвих інвестицій в інфраструктуру.

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

Для цього ми розробили акселератор, на базі якого розгортаються всі потрібні нам мікросервісні продукти і можна застосувати GraphQL, gRPC, REST (на вибір). Він мав працювати у реальному часі завдяки SignalR або GraphQL Subscription, з простотою розгортання та готовими ARM-темплейтами.

Готове рішення мало відповідати таким вимогам:

  • швидка автоматична масштабовність;
  • можливість розгортання cloud-інфраструктури та CI/CD для нового проєкту за хвилини;
  • опрацювання мільйонів запитів за секунду;
  • можливість rolling-update із zero-downtime;
  • низька ціна розробки та хостингу сервісів.

Ми створили фреймворк з максимально гнучкою архітектурою для мікросервісів в Azure Cloud — Intellias.CQRS.Framework, який можна підлаштувати під різні системи: FinTech, Healthcare, Enterprise тощо. Далі розкажемо про етапи та деталі створення нашого рішення.

Робота над проєктом

Для створення фреймворку ми скористались такими технологіями: C#, Azure Functions, Azure Service Bus, Cosmos DB, Azure Search, Azure Storage, GraphQL, Azure DevOps.

Azure ми обрали тому, що цей cloud-вендор найшвидше розвивається і приваблює багато наших клієнтів. Планів на адаптацію в AWS поки немає.

В основу системи закладена архітектура event-driven, що побудована на serverless-моделі. CQRS дозволяє адаптуватися до асиметричного навантаження при записі та зчитуванні даних. Вся комунікація є асинхронною і відбувається через команди і події (events). Щоб спростити діалог з бізнесом, розробку і підтримку продукту, ми використовуємо DDD.

Система складається з таких основних частин:

  • Front-end аплікація.
  • Gateway — обробляє запити на читання і відправляє запити на запис в глиб системи.
  • Subdomains — частини рішення, що відповідають за окремі бізнес-сектори (аналог мікросервісів). Складаються з трьох частин:
    • CommandHandler — перенаправляє команду в один чи більше агрегатів і продукує подію;
    • EventHandler — обробляє події та відправляє сигнали про це в Gateway;
    • ProcessManager — реагує на події, надсилаючи команди в Subdomain.
  • Shared — складається з сервісів конфігурації, моніторингу, месседжингу.

image1

Розкажемо, як відбувається повний цикл проходження даних (data flow), а також зчитування даних та відправлення помилки до користувача.

Користувач заходить на сторінку сайту (SPA), у нашому випадку це сторінка внутрішнього ресурсу IntelliGrowth. Розглянемо його взаємодію з сайтом на прикладі дії «оновити назву компетенції». Наш користувач натискає відповідні кнопки на сайті, а його запит від SPA йде у Gateway.

Gateway приймає усі запити і знає, в чергу якого субдомену їх направити відповідно до типу запиту. Наразі таких частин чотири, кожна відповідає за відокремлену зв'язану групу бізнес-процесів компанії. Запити з черги субдомену обробляє Command Handler, де і відбувається вся «магія» — вносяться зміни в систему. Ми активно використовуємо DDD при написанні субдоменів. Command Handler активує відповідальний Aggregate Root і делегує команду йому.


public async Task<IExecutionResult> Handle(CommandRequest<UpdateCompetenciesNodeNameCommand> request)
{
    var (command, context, scope) = request;

    var node = await scope.FindAggregateAsync<CompetenciesNodeAggregateRoot, CompetenciesNodeState>(command.AggregateRootId, context);
    if (node == null)
    {
        return ValidationFailed(CoreErrorCodes.AggregateRootNotFound)
            .ForCommand<UpdateCompetenciesNodeNameCommand>(c => c.AggregateRootId);
    }

    var result = node.UpdateName(command.Name);
    if (!result.Success)
    {
        return result;
    }

    return IntegrationEvent<CompetenciesNodeNameUpdatedIntegrationEvent>(context, e =>
    {
        e.CompetenciesNode = node.SnapshotId;
        e.Name = node.State.Name;
    });
}

Aggregate Root її приймає, обробляє і зберігає зміни у своєму стані у вигляді change set'у. На цьому рівні ми реалізували патерн Event Sourcing, завдяки якому завжди можна простежити причинно-наслідкові зв'язки тих чи інших дій.


public IExecutionResult UpdateName(string name)
{
    if (context.UserId != State.OwnerId)
    {
        return AccessDenied(CompetencyErrorCodes.OnlyOwnerCouldUpdateName)
            .ForCommand<UpdateCompetenciesNodeNameCommand>();
    }

    if (State.ProficiencyScale.Status == ProficiencyScaleStatus.Legacy)
    {
        return ValidationFailed(CompetencyErrorCodes.UpdateNameUnderLegacyNode)
            .ForCommand<UpdateCompetenciesNodeNameCommand>();
    }

    PublishEvent<CompetenciesNodeNameUpdatedStateEvent>(e =>
    {
        e.Name = name;
    });

    return Success();
}
	

Після проведення зміни все зберігається. Command Handler надсилає один сигнал про те, що команда успішно виконана, і публікує Integration Event з даними про зміну стану субдомену. Далі інформація про зміну з Integration events передається в Event Handlers, які по черзі оновлюють свої дані в QueryModels. У цьому випадку назва компетенції змінюється усюди, де використовується.

public Task Handle(IntegrationEventNotification<CompetenciesNodeNameUpdatedIntegrationEvent> notification)
{
    return HandleAsync(notification, e => e.CompetenciesNode.EntryId, (e, qm) =>
    {
        qm.Name = e.Name;
    });
}

QueryModels мають дані агрегатів, але вони оптимізовані під читання. Після того як Event Handlers завершать роботу, вони передають про це сигнал через Gateway до SPA і зміни виводяться на екран користувача.

image3

Коли є запит для читання даних, а не на модифікацію, Gateway «заглядає» одразу в QueryModels і виводить інформацію для користувача. Система оптимізована під зчитування даних і має таку кількість подій, щоб користувачі завжди отримували майже миттєвий відгук на свої запити і дії.

image2

Якщо запит користувача не проходить валідацію, Command Handler не продукує Integration Event і публікує лише сигнал з помилкою. Ми розробили потужний механізм генерації помилок, який дозволяє передати детальну інформацію про походження помилки. Наприклад, така валідаційна помилка:

return ValidationFailed<JobLevelCollection>(JobProfilesErrorCodes.JobLevelNamesShouldBeUnique)
    .ForCommand<JobLevelCollection, UpdateJobFamilyTracksCommand>(c => c.JobTracks[0].Levels[index].Name)
	

буде відправлена до SPA в такій формі:

{
    "code": "JobProfiles.JobLevelNamesShouldBeUnique",
    "source": "updateJobFamilyTracksCommand.jobTracks.0.levels.0.name"
}

Тоді користувач отримує вичерпну відповідь і бачить, що пішло не так і як це виправити.

Етапи розробки та складності

Розробка системи відбувалася у декілька етапів:

  • Розробка ядра системи. Ядро системи складається з базових абстракцій для DDD, меседжингу й інфраструктурних сервісів. Наприклад, ми маємо базову абстракцію для AggregateRoot, що реалізує EventSourcing. Також ми маємо базові абстракції для команд і подій, яких ми підтримуємо аж 3 види:
    • Integration event — сповіщає про зміну стану Subdomain. Є основою для CQRS і побудови QueryModels;
    • State event — сповіщає про зміну стану AggregateRoot. Є основою для EventSourcing;
    • Signal — сповіщає про зміну в системі, не зберігається.
  • Розробка cloud-конекторів і бібліотек для роботи з сервісами Azure, на кшталт Service Bus, Storage Queue, Table Storage тощо.
  • Розробка системи деплойменту і моніторингу.
  • Розробка realtime-стрімінгу з використанням GraphQL.
  • Розробка фреймворку для швидкого написання тестів.

Однією з основних складностей при розробці системи було забезпечення консистентності даних. Optimistic concurrency для команд, гарантії дедуплікації і порядку для подій, двофазні коміти і використання хореографії для виконання бізнес-процесів — це все невіддільні частини системи, які забезпечують її надійну роботу.

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

Зараз місткість системи складає до 100Тб і максимальний скейл фреймворку — обробка ~4M транзакцій за секунду, обмеження cloud serverless. Швидкість масштабування до 1000 інстансів за хвилину і з повністю незалежним розгортанням кожного мікросервісу.

Для тестування ми розробили власний event-driven фреймворк, щоб писати функціональні тести з покриттям повного циклу CQRS.

Фреймворк в Open Source

Тепер наша розробка активно використовується на кількох проєктах компанії. Також фреймворк вже доступний в Open Source.

image4

Команда: Сергій Селецький, Senior Solution Architect, Павло Ритіков, Senior Software Engineer, Олег Галай, Senior .NET Software Engineer, Роман Смічик, Senior Front-end Developer, Дмитро Мазур, Strong Middle .NET Engineer, Ольга Скочинська, Senior UX Designer, Ганна Радченко, Strong Middle UX Designer, Роман Березін, QA Test Engineer, Яна Шулюк, Business Analyst, Дмитро Юрченко, Delivery Manager.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.9K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація