Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 30
×

Поясните за CQRS/Event Sourcing/DDD

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Есть несколько скользких вопросов по архитектуре касательно CQRS/Event Sourcing/DDD, т.к. сам IRL не сталкивался, а из знакомых кто сталкивался, то очень поверхностно, и деталей не знают. В статьях толковых объяснений «на пальцах» с примерами сайд-эффектов особо не нашёл — все слишком специфичны и слабо интегральны («вот мы сделали так-то в таком-то случае», при этом большая картина остаётся в тумане войны).

1. Подразумевает ли это полностью реактивный фронт? Я так понимаю, что да, т.к.:
1.1. Нет смысла сразу же вычитывать проекцию, если мы даже не знаем, успела ли она обновиться после реквеста и соответствующей команды.
1.2. Книжное понимание CQRS (и CQS) подразумевает, что команда ничего и никогда не возвращает, НО при автогенерации айдишечки базой при создании сущности мы даже при желании вычитать созданную сущность не сможем, если у нас не будет айди. Кстати, упомянутый выше Mark Seemann предложил вот это как вариант, и Грег Янг туда же (с аргументами «а если будет батч из команд на один реквест вместо одной, а что с идемпотентностью»), и многие другие люди, но я пока внутренне не могу этого принять)).

2. Баги. В «стандартной» системе бывает так, что обнаруживаются старые пропущенные ошибки, которые привели к неправильным данным в базе. В этом случае мы, что делать, идём и правим базу (может, даже не одну) либо с помощью языка запросов, либо, если всё сложно, пишем отдельную тулу для этого. Но что делать, если мы уже напедалили проекций, одни ивенты породили другие ивенты, которые лежат в ивент сторе, и т.д.? Понятно, что необязательно делать всю систему такой, и не всегда прям будут такие апокалиптические цепочки, но чем больше охват, тем больше риск. Да и в целом любая ошибка (вроде как) сделает больше беды, чем обычно.

3. Бизнес-валидации и возврат ошибок — наверное, самая контраверсионная тема. Люди говорят противоположные вещи (большинство, что проекции нельзя юзать для валидации, но тот же Грег Янг в своих видео достаточно язвительно, как он любит, заявляет в сторону первой группы, что куча кейсов, когда можно). В инете гуляет много вариантов под разные юз-кейсы:
3.1. Если данные для валидации касаются только одной сущности, для каждой сущности используется свой стрим/коллекцию/you name it со своим ID, чтобы проигрывать ивенты из стора каждый раз, и получать реальное состояние. Но если что-то часто меняется, может же быть печаль с перформансом из-за постоянного реплея (даже с учётом снепшотов в ивент сторе)?
3.2. Для узкого кейса «уникальность значения» создаётся отдельное индексное хранилище (напр., таблица) а-ля «если там такое уже есть, то нельзя». Выглядит так себе.
3.3. Если нужно много данных для валидации, и требуется восстанавливать состояния агрегатов, то даунсайд по перформансу из п. 3.1 может(?) здесь выстрелить ещё сильнее (в зависимости от размера агрегатов).

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
1. Подразумевает ли это полностью реактивный фронт? Я так понимаю, что да, т.к.:
1.1. Нет смысла сразу же вычитывать проекцию, если мы даже не знаем, успела ли она обновиться после реквеста и соответствующей команды.

Речь идет о strong consistency в понимании ACID судя по описанию из самого определения cqrs выплывает, что такие системы eventually consistent же. Для многих задач это покрывает требования ui. Там где нужна strong consistency сущеcтвуют разные решения — из того чем сталкивался и что позиционируется даже как паттерны — HTTP Api ожидают обновления проекции перед тем как отдать 200 OK (этот подход вроде позиционирует как паттерн microservices.io), другой подход — веб-сокеты и пуш апдейт с бекенда + подписка на изменения, еще вариант делать strong consistency — писать в ивент лог и синхронно обновлять read projection + идемпотентные проджекторы так как апдейт может заходить 2 раза. Вообщем вариантов много в зависимости от используемых технологий и сложности дизайна системы.

Баги. В «стандартной» системе бывает так, что обнаруживаются старые пропущенные ошибки, которые привели к неправильным данным в базе. В этом случае мы, что делать, идём и правим базу (может, даже не одну) либо с помощью языка запросов, либо, если всё сложно, пишем отдельную тулу для этого. Но что делать, если мы уже напедалили проекций, одни ивенты породили другие ивенты, которые лежат в ивент сторе, и т.д.?

Делать миграцию данных. Фиксить код, далее генерить новые ивенты, что пофиксят стейт в сторе, а дальше они пофиксят снепшоты агрегатов и проекции соотвественно. Очевидно фиксить дольше чем, когда данные храняться в одном экземпляре.

3. Бизнес-валидации и возврат ошибок — наверное, самая контраверсионная тема. Люди говорят противоположные вещи (большинство, что проекции нельзя юзать для валидации, но тот же Грег Янг в своих видео достаточно язвительно, как он любит, заявляет в сторону первой группы, что куча кейсов, когда можно). В инете гуляет много вариантов под разные юз-кейсы:

хз, строго юзать снепшоты агрегатов для сложной валидации бизнесс логики будет работать по-разному только в какой-то неконкурентной среде — каких-то акторах или на консьюмерах кафки, в остальных случаях оно не имеет отличий, что юзаешь вроде как вообще — снепшот агрегата из event store или read projection они оба могут быть stale в рамках какого-нибудь среднестатистического web api и не важно, что ты хоть все это обновляешь в рамках одной транзакции даже, имею в виду event store + derived read projections — что бы валидация работала идеально правильно в таких условиях надо юзать критические секции и блокировки на этапе вычитки данных для валидации и до конца обновления данных как минимум в рамках используемых write агрегатов — такого из коробки не умеет вроде как ни один event store, проще всего сделать с реляционной субд было бы, что умеют делать lock for update и я бы не сказал, что это правильно;

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

Є варіант повертати інкрементальний ід з команд сервісу і чекати на нього в результаті з проекції.
Можна вигадати ще пуш-нотифікації для ФЕ по опрацюванню івентів команди.

1.2 Не повертає. Ід агрегата краще використовувати як ююід. Якщо на фе потрібен ід з бази — отримати його з проекції

2. Тільки коригуючі івенти.

3.1...Но если что-то часто меняется, может же быть печаль с перформансом из-за постоянного реплея

CQRS доцільно використовувати при співвідношенні W/R меньше 20%.

3.2. Таке сховище нічим не краще за проекцію.
Доречі, можна такі ід теж генерувати через саги.

3.3 Пошукайте про саги.

3.2. Таке сховище нічим не краще за проекцію.
Доречі, можна такі ід теж генерувати через саги.

3.3 Пошукайте про саги.

Сорри, не понял, как оба пункта связаны с сагами.

3.2.Саги використовуються для міжагрегатної валідації та завершення такої багатофазної операції. Тобто може відправляти останню команду з унікальним айді.

3.3.Якщо саги базуються на івентах, то в них валідація може відбуватись безпосередньо на проекції, яка може бути оптимізована саме під валідацію.
docs.axoniq.io/...​art-ii-domain-logic/sagas

3.2.Саги використовуються для міжагрегатної валідації та завершення такої багатофазної операції.

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

Як раз основна частина «менеджменту зміни стану» це і є валідація (плюс команда завершення чи відкату багатофазної операції). І для цього треба отримати якісь дані для валідації. А сага (як специфічний івентхендлер) може вести свої проекції для цілей валідації для запобігання вичитування всих івентів для агрегатів, що її цікавлять.

Не могу согласиться — выглядит как велосипеды с нарушением SRP и сильная связность через передачу саге не свойственных ей функций. В саге ведь нет понятия «валидации» — только правила перехода из одного состояния в другое в зависимости от результата предыдущих шагов. Если нужны какие-то специфические проекции именно для валидации, они будут в том сервисе, который будет выполнять конкретный шаг транзакции, а сага об этом знать не будет, т,к. это просто координатор а-ля if/else или switch/case. Поэтому разве что в качестве исключения.

1. Не обов’язково. В принципі можна додавати локальну копію створеної сутності з тегом «локальна», якось відобразивши це в UI, а коли приїде оновлення з сервера — замінити.
1.2 Не бачу навіщо впиратися рогом що команди нічого не вертають, але якщо CQRS фреймворк всередині не має примітива, на якому можна реалізувати load linked/store conditional, iдентифікатори можна робити guid і передавати в команду.
2. Якщо в лог таки потрапили неправильні події і їх можна алгоритмічно визначити, то ми додаємо відповідний код в проекцію і піднімаємо версію проекції. Старі кешовані проекції автоматично дропаються і перераховуються з лога подій при наступному запиті. Якщо при цьому виникатимуть неприйнятні затримки, можна запустити перерахунок в фоні на внутрішньому вузлі одразу після оновлення.
3. Ми валідуємо з використанням проекцій, бо у нас є LL/SC, тобто можна писати події в конкретний агрегат за умови що він не змінився. Це ж дає змогу створювати послідовні ідентифікатори в рамках одного агрегата. Часом для економії ресурсів буває зручно створити окрему легеньку проекцію спеціально для валідації, тоді на вхідних вузлах команд немає потреби тримати чи завантажувати основну проекцію. Проекції одного агрегата оновлюються синхронно. Кешування проекцій дозволяє не програвати весь лог щоразу, а самі «гарячі» проекції висять в RAM, постійно оновлюються і періодично оновлюють кеш. Інколи має сенс створити окремий службовий агрегат і дублювати туди лише потрібні для валідації події, але для цього CQRS фреймворк має дозволяти LL/SC на кілька агрегатів одразу (наш дозволяє).

Ми валідуємо з використанням проекцій, бо у нас є LL/SC, тобто можна писати події в конкретний агрегат за умови що він не змінився. Це ж дає змогу створювати послідовні ідентифікатори в рамках одного агрегата.

Объясните, плз, вот этот пункт подробнее — много простора для искажения смысла). Даже если

Проекції одного агрегата оновлюються синхронно

Они обновляются не только синхронно все вместе, а и в то же время синхронно с обновлением самого агрегата, пока он залочен (т.е., лочится всё сразу), или как? Т.к. эта проблема выстреливает даже, если присутствует всего одна проекция — именно из-за того, что обновление проекций в целом асинхронно.

Проекції одного агрегата оновлюються синхронно

Я маю на увазі, що на кожному окремому вузлі розподіленої системи проекції, побудовані на одному агрегаті, оновлюються разом: на вузлі неможлива ситуація, коли бізнес-код завантажує дві проекції одного агрегата, одна з яких вже обробила певну подію, а інша ні. Саме оновлення відбувається асинхронно або за запитом бізнес-коду завантажити проекції, або пушем з вузлів, які відають сховищем подій (це можуть бути і ті ж самі вузли, це неважливо). Відповідно, так само як в примітиві LL/SC, або в compare-and-swap якщо це вам більше знайомо, нічого не лочиться, а працює за схемою хто перший встав того і капці. При записі в агрегат нових подій, валідованих чи утворених на основі проекції, можна опціонально вказати умову, типу If-Match: <etag>, і вказати, до якого місця використана проекція обробила події. Якщо ця умова не виконана, тобто проекція виявилася стара або хтось встиг встромити в агрегат нову подію, операція запису повертає помилку, і команда (або фреймворк) може оновити проекції, переобчислити події які потрібно записати, і спробувати ще раз. Проектування в такій моделі вимагає певної обережності, щоб не утворити агрегатів з надто великим contention’ом, але якісь аналогічні обмеження вилізуть в будь-якій моделі.

А, т.е. проекция версионируется, и по сути используется optimistic concurrency — понял. Спасибо, что сразу описали ограничения — это сняло дополнительные вопросы).

2. Баги.

Берешь и педалишь набор команд который это все исправит, ну или +1 команду, команд хендлер, евент хендлер(для агрегата и проекци) =) Варианты есть, да гемморойные как и весь ES, но существующие ивенты в сторе трогать не надо.

3. Бизнес-валидации и возврат ошибок

В агрегате как и вся бизнес логика.

1.2.

Если очень надо, то можно. 2. Если старая система то там обычно подразумевается антикорапшн левл. А вообще там нету четкого описания реализации все зависит от конкретного места применения, я бы сказал, что обычно делают системы с элементами

CQRS

в той или иной степени. Общие принципы и вариации реализации на местах.

Підписатись на коментарі