Эволюция архитектуры проекта. Из монолита в микросервисы

Здравствуйте, меня зовут Денис, и сейчас я работаю на позиции Team Lead в компании Fiverr. Уже 10 лет я занимаюсь разработкой веб-сервисов. За это время я участвовал в техническом развитии нескольких крупных компаний, таких как криптобиржа EXMO, автоматизировал работу складов компании Westwing и так далее.

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

Шаг 1. Монолит. Кеширование

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

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

Хорошо, кстати, если тесты кто-то вообще писал. В этот момент далеко не все проджект-менеджеры понимают, почему такая простая задача занимает столько времени. Или в приложении начинает тормозить один участок, и приходится выливать несколько инстансов, чтобы распределить нагрузку именно на него, хотя для всего проекта это не нужно.

Как было в Fiverr

Мы начали создание сайта как монолитного приложения Ruby on Rails, поддерживаемого базой данных MySQL.

По мере того как сайт набирал популярность, а трафик увеличивался, необходимо было добавлять дополнительный слой кеширования в виде Memcached и Rails Action кэширования. Чтобы избежать вычислений данных для поиска и просмотра каталогов в реальном времени, были добавлены некоторые задания cron, в которых для разогрева кеша попадала заранее подготовленная информация.

Ранняя архитектура, состоящая из Rails, MySQL и Memcached

Но рост и масштабирование имеет свою стоимость, и в течение следующих 3 лет увеличение трафика заставило акцентироваться на оригинальной архитектуре.

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

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

Шаг 2. Сделать из монолита микросервисы

Если же не разделять монолит, то хотя бы новый функционал нужно писать в микросервисе. Один из исходов событий, который мне нравиться больше всего, это когда разработчик говорит проджекту: «Давай я сделаю эту задачу в отдельном проекте, и это будет раза в 2 быстрее». Конечно, он согласится, а разработчик заодно попробует еще новую технологию или хотя бы будет работать в чистом проекте без легаси-кода. По ощущениям это как пленочку с телефона снять.

Также микросервисы становиться намного легче масштабировать горизонтально.

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

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

Fiverr в свое время объединил шаги № 2 и № 3.

Наряду с большой шумихой вокруг микросервисов мы начали детские шаги в путешествии, чтобы сломать монолит.

При разработке первого микросервиса были важные понятия, которые мы хотели продвинуть:

  • Простота начальной разработки — наша команда инженеров на бэкенде говорила на Ruby, и мы полюбили его за простоту и удобство для разработчиков. Мы хотели сохранить Ruby в качестве нашего языка (который позже эволюционировал в добавление Golang в наш стек, но об этом чуть позже).
  • Вирусное решение — усилия по масштабированию начинаются в первую очередь с инженеров, и мы должны были убедиться, что наша команда создает решение, которое является одновременно и глубоко стабильным, надежным, масштабируемым, и вирусным — это означает, что его принятие среди инженеров будет простым.
  • Асинхронный обмен сообщениями между сервисами — по мере увеличения трафика возникла необходимость в быстром и отзывчивом сайте, и мы поняли, что, позволяя пользователю ждать синхронного завершения обновлений, замедляли не только его сессии, но и всю платформу. Так как меньше запросов может быть обработано в то время, как другие пользователи используют сессию.

Шаг 4. Добавить события

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

Как это было в Fiverr

Мы создали «химеру», вдохновленную мифологическим двуглавым зверем, которая представляет собой Ruby-шаблон микросервиса, сочетающий в себе:

  • READ SIDE Grape API — для получения данных из ограниченного контекста, представленного сервисом, если это информация о пользователях, ценах на билеты или аналитика конкретного заказа.
  • WRITE SIDE RabbitMQ consumer — прослушивание темы сообщения и асинхронное выполнение обновлений модели ограниченного контекста сервиса.

«Химера» также пользуется:

Shared Core — так как обе «головы» находятся в одном репозитории, они наслаждаются повторным использованием кода и делятся бизнес-логикой домена, утилитами и так далее.

Общий набор коннекторов БД — переход на микросервисы инициировал использование специфических для сервиса баз данных, таких как MongoDB или Redis, которые мы сейчас широко используем. Сохраняя источник истины в виде реляционного кластера MySQL, мы хотели продвигать каждый сервис для доступа к собственной базе данных, оптимизированной для домена, и нам нужны были утилиты для упрощения подключения ко всем этим различным хранилищам.

Типичный асинхронный запрос на обновление будет выглядеть так, как при использовании «химеры»:

  1. Пользователь выполнит POST для создания заказа.
  2. REST API будет проверять запрос во время сеансов, пока ничего не сохраняя в базе данных. Это выполняется очень быстро, так как валидация поверхностная и включает в себя базовые входные валидации, а также некоторые проверки целостности данных с БД, которые выполняются без какой-либо тяжелой обработки.
  3. В этот момент, если валидация пройдена, в почтовый брокер (RabbitMQ) посылается сообщение с ключом маршрутизации, указывающим на собственного работника «химеры». Сообщение содержит всю информацию, переданную методом POST, и будет выполняться асинхронно, а пользователю не нужно будет ждать.
  4. Затем пользователю возвращается быстрый 201 статус, и он может сразу же продолжить использование платформы.
  5. Сообщение получает потребитель. RabbitMQ (работник) «химеры» имеет доступ к той же модели домена и может использовать его для выполнения команды и сохранения порядка в БД.

После представления «химеры», которая является разновидностью микросервиса, наша обновленная архитектура бэкенда выглядит примерно так:

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

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

  • Командные события — события, посылаемые «химерой» внутрь своего работника (потребителя RabbitMQ) для асинхронной обработки обновлений.
  • Доменные события (Domain Events) — события, посылаемые химерой в подшаблоне pub любой другой химере, зарегистрированной для их прослушивания, информируя систему о том, что что-то случилось внутри ограниченного контекста «химерой».

Шаг 6. CQRS-подход и Event Sourcing

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

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

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

Рассмотрим один сценарий использования CQRS, который нам очень пригодился — аналитика! Простым примером для демонстрации будет приборная панель заказов продавцов:

Получение общего количества заказов на разных этапах потребует простой группы по команде в SQL. Но, когда дело доходит до больших масштабов, мы не хотим каждый раз при обновлении приборной панели «Управление продажами» выполнять такой объемный запрос для каждого из продавцов. CQRS на помощь!

Получение статистики заказа теперь уменьшено с o(n) до o(1). Прекрасно!

CQRS позволяет обновлять контекст ограниченных заказов, сохранять изменение статуса заказа в реляционной таблице MySQL, затем, используя доменное событие, информирующее любой заинтересованный компонент системы об этом изменении, мы фиксируем изменение в совершенно ином ограниченном контексте — аналитической «химере». И обновляем наш прочитанный оптимизированный MongoDB-документ, увеличивая конкретное значение bucket.

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

Итак, наша первая эволюция платформы в итоге выглядела так:

Вывод

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

Зато с таким подходом компания может быть готова к рекламе на Super Bowl, внезапному росту биткоина до 60К, порождающему массовые торги, увеличению количества товара на складе в несколько раз за ночь, огромному баннеру на главной странице App Store. А это все реальные истории. Когда выпадает шанс вырасти в несколько раз, нужно быть готовым, чтобы продукт это выдержал.

👍НравитсяПонравилось10
В избранноеВ избранном10
Подписаться на тему «архитектура»
LinkedIn



Підписуйтесь: Soundcloud | Google Podcast | YouTube


30 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Автор, у вас был event sourcing или domain events? Это разные вещи, насколько мне известно.

Шаг 6. CQRS-подход и Event Sourcing

Event sourcing і CQRS не мають прямого відношення до мікросервісной архітектурі. Ви спокійно могли їх використовувати і в моноліті.

дополнительный слой кеширования в виде Memcached
WRITE SIDE RabbitMQ consumer

А навіщо використовувати старі малопопулярні технлогії, коли якраз для високонавантажених систем придумали Redis і Kafka?

Кафка и Рэббит — это в первую очередь архитектурно разные звери (разный дизайн, разные возможности/гарантии, разные ограничения). Соответственно, предлагать вместо месседж брокера использовать ивент стримы не зная внутренней кухни (решаемых задач в первую очередь), а только потому что это стильно-модно-молодежно — это профанация.

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

Все вирішувані завдання я взяв з цього поста:

Когда сервисов становится много и им нужно оперировать общими данными, то лучше всего передавать в очереди сами события о том, что произошло в системе,
WRITE SIDE RabbitMQ consumer — прослушивание темы сообщения и асинхронное выполнение обновлений модели ограниченного контекста сервиса.

Для таких завдань Kafka підходить ідеально.Відповідно, якщо автор щось приховав, то це вже не моя вина.

Для таких завдань Kafka підходить ідеально

Не совсем так. Кафка — оверхед для подобного задания, т.к. её сильно сложнее «готовить» с точки зрения инфраструктуры. Rabbit просто содержит в себе практически все продвинутые фичи обычных очередей и шин сообщений и в целом близок к ним по концепции, а с Кафкой всё сильно муторнее.

Кафка и Рэббит — это архитектурно разные звери (разный дизайн, разные возможности/гарантии, разные ограничения). Соответственно, предлагать вместо месседж брокера использовать ивент стримы не зная внутренней кухни (решаемых задач в первую очередь), а только потому что это стильно-модно-молодежно — это профанация.

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

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

как работают компании с большим количеством трафика

А що таке великий обсяг трафіку? У пості про це ні слова

А чому Ви вирішили, що стара архітектура вам не підходить?

Мабуть проводились якісь дослідження і load testing, було б цікаво побачити якісь графіки (web/not-web transaction time, apdex score, throuput графіки тощо) з порівняннями стабільності швидкості роботи системи при різних навантаженнях на різних рівнях — мережа, рельси, БД тощо.

Монолитная природа RoR означала, что любая ошибка или авария приведет к падению сайта

Незрозуміло і дуже цікаво де знаходиться оця межа, коли load-balancer + N rails instances та database replication & sharding вже не підходить і треба розбивати все на мікросервіси?

разработчик говорит проджекту: «Давай я сделаю эту задачу в отдельном проекте, и это будет раза в 2 быстрее».

Також дуже цікаво де знаходиться стадія проекту коли рефакторингу, документації та тестів вже недостатньо і терміново треба мікросервіси.

Перше враження склалося що це більш подібне на революцію архітектури проекту, а не еволюцію =)

К сожалению графики не сохранились. Писали монолит на laravel. Много кешировали, имели 4 реплики бекенда и столько же слейвов мускуля + мастер. Средний rps на один ендпоинт был в районе 20. Когда зоопарк из балансировщиков перестал помогать, а количество индексов в мускуле перевалило за адекватные пределы решили переходить на сервисы и отказываться от php

То что разделение на микросервисы решило вашу проблему это хорошо. Но вопрос то был где эта граница и ответ несколько неубедителен.

Средний rps на один ендпоинт был в районе 20

20K rps? На endpoint или на инстанс? Или ендпоинт делал 20 запросов к внешним сервисам. Непонятная фраза.

зоопарк из балансировщиков

по сути ж их там 2 всего: http (nginx/haproxy) — который и так обязателен и один для баз, чтоб разделить read/write нагрузку (proxysql/...).

количество индексов в мускуле перевалило за адекватные пределы

здесь же тоже без параметров серверов не обойтись, одно дело 4cpu и 8GB или хорошее железо (64+ ядра и 500+GB памяти, nvme ssd) где база может спокойно держать десятки тысяч rps oltp запросов, тысячи индексов и датасет в терабайты, если не косячить.
По-этому «адекватные» пределы тоже относительны. Ну и ничто не мешает в рамках монолита каждому модулю использовать своё хранилище.

А сервисы они сами по себе эволюционно возникают, как совершенно независимые подсистемы — полнотекстовый поиск, хранилище/отдача статики, конвертация видео, olap/big data storage и «тяжёлая» аналитика. Которые расширяют существующий функционал, а не требуют переписывания всего.

Я то не против микросервисов, но грань очень размытая когда стоит всё переписать. Скорее она даже больше организационная, чем техническая.

Я привёл пример, когда для моего проекта переход на сервисы был спасительным кругом. Но если вы ожидаете ответ «используйте сервисы начиная от ... rps и и когда ...» - его не будет. Для нас главным фактором было — точечный скейлинг для узких мест системы. Так же это позволило отойти от заданных mvp технологий в сторону более подходящих под наши задачи.

20K rps? На endpoint или на инстанс? Или ендпоинт делал 20 запросов к внешним сервисам. Непонятная фраза

Именно endpoint

по сути ж их там 2 всего: http (nginx/haproxy) — который и так обязателен и один для баз, чтоб разделить read/write нагрузку (proxysql/...).

Есть ещё кластер redis sentinel, с которым тоже хватало забот

здесь же тоже без параметров серверов не обойтись, одно дело 4cpu и 8GB или хорошее железо

Вот тут уже точно не скажу ничего, но по железу на каждую ноду было грустно

Спасибо за статью — на удивление хорошо написана, просто и понятно, без воды. Единственное, что хотелось бы больше деталей всё-таки).

2. REST API будет проверять запрос во время сеансов, пока ничего не сохраняя в базе данных. Это выполняется очень быстро, так как валидация поверхностная и включает в себя базовые входные валидации, а также некоторые проверки целостности данных с БД, которые выполняются без какой-либо тяжелой обработки.
3. В этот момент, если валидация пройдена, в почтовый брокер (RabbitMQ) посылается сообщение с ключом маршрутизации, указывающим на собственного работника «химеры». Сообщение содержит всю информацию, переданную методом POST, и будет выполняться асинхронно, а пользователю не нужно будет ждать.

Как воюете с конкурентной обработкой? Передаёте версию сущности после вычитки для валидации обратно в команду и сравниваете, или как-то иначе?

Очень хороший вопрос. Ответ на него сильно зависит от самой задачи и есть несколько вариантов.

Например для финансового сектора, где конечное состояние агрегата намного важнее скорости работы с ним, можно использовать мутексы и применять изменения только после того как все параллельные чтения с этим агрегатом были завершены. Так как lock применяется конкретно на каждого агрегата свой, то не так уже и медленно выходит. Мы так делали в гошных сервисах, когда агрегаты находятся непосредственно в памяти. Удобно.

В остальных случаях мы считаем события как источник правды, но не обязательно событие должно применится без ошибки. В таком случае сейчас оно попадает в отдельную очередь кафки «failed queue», где обрабатывается позже.

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

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

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

У вас разные микросервисы используют одну и ту-же БД? Если да то это антипаттерн.

По поводу ES

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

Можно примеры неоспоримого приемущества?
Я например честно не понимаю нафига нужен ES в классическом виде, когда для того чтобы получить текущее состояние аггрегата нужно каждый раз его восстанавливать из событий.

Асинхронный обмен сообщениями между сервисами

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

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

В основном, он и не нужен. Если система большая и там уже ES, то новые тяжёлые сервисы выиграют за счёт построения read модели из готовых данных.
Также очень весомый плюс, если кто-то собирается натравить на свои данные ML.

И вот событие создания заказа уже получено а товара еще нет, что делать?

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

По поводу преимущества.

Так как система хранит внутри себя историю всех изменений, то можно откатить её до любого момента времени. Например что-то пошло не так и неправильная обработка привела к проблемам с целостностью бд. В таком случаем можно её легко восстановить поменяв логику и обработав события заново. Без es в таком случае это может занять очень большой промежуток времени, если это вообще возможно.

Следующий пример это возможность строить read модели со структурой как тебе удобно и в любой момент. Работа с аналитикой и системой для отчётов это отдельное удовольствие.

Насчёт проблемы с асинхронными событиями.

В твоем конкретном примере перед тем как послать событие с созданием товара как раз можно сделать все проверки на наличие. Или же можно разделить событие на несколько — юзер создал заказ, заказ проверен на все наличия и тд. Или же просто вернуть юзеру сообщение о том, что товара еще нет. Если read модель работает быстро, то таких случаев достаточно мало и иногда просто достаточно попробовать обработать их еще раз через какой-то retry период.

В комментарии выше я описал примерные подходы для решения этой проблемы. Скорее это не недостаток es, а боль:), которую можно грамотно решить в зависимости от проблемы.

Так как система хранит внутри себя историю всех изменений, то можно откатить её до любого момента времени

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

Без es в таком случае это может занять очень большой промежуток времени, если это вообще возможно.

Без ES это называется аудит, как вариант вешались триггеры на наиболее важные таблички и писали всю историю в другие таблички. Когда что-то пошло не так, по аудиту можно было восстановить события.

Следующий пример это возможность строить read модели со структурой как тебе удобно и в любой момент.

А ES тут как помогает? Имея последнее состояние сущности тоже можно строить read модели как угодно. Если нужна история то можно прицепить CDC и сливать куда нибудь NoSQL, HDFS, S3 дальше строй какие-угодно модели.

Я вообще об этом
«Event sourcing is a great way to atomically update state and publish events. The traditional way to persist an entity is to save its current state. Event sourcing uses a radically different, event-centric approach to persistence. A business object is persisted by storing a sequence of state changing events. Whenever an object’s state changes, a new event is appended to the sequence of events. Since that is one operation it is inherently atomic. A entity’s current state is reconstructed by replaying its events.»

Т.е. каждый раз когда мне нужно последнее состояние сущности система восстанавливает его из событий вместо того чтобы просто взять последнее состояние из БД. Вот в этом какой смысл?
Последнее состояние сущности нужно в 99.9% и каждый раз нагружать систему проигрывать все события для восстановления сущности.

Хотя все навреное потом подумали и вот что Фаулер пишет
«In many applications it’s more common to request recent application states, if so a faster alternative is to store the current application state and if someone wants the special features that Event Sourcing offers then that additional capability is built on top.
Application states can be stored either in memory or on disk. Since an application state is purely derivable from the event log, you can cache it anywhere you like.»

В общем гадость эта ваша заливная рыба. Я вот именно хотел узнать реальные кейсы где нужен ES а не из книжек.

За ассинхронность
Юзер создает товар, рест принял запрос, валидировал, отправил команду создать товар и ответил юзеру 200. По идее юзер должне дождаться ивента от сервиса что товар создан и только после этого создавать заказ. Ну ок допустим так и есть.
Дальше юзер создает заказ, то же самое, рест-> команда идет в очередь -> 200.
Есть сервис отвечающий за заказы, команд хэндлер принимает команду создать заказ и ему нужно проверить некую бизнес логику.
На этом моменте пользователь получил от рест 200 и спокойно пьет кофе.
На моменте проверки бизнес логики сервису заказов нужна информация о товаре. Очевидно, что в таком случае он должен иметь у себя кеш товаров, и вот случилась беда в кеше нет товара добавленного в первом шаге. Что делать, вернуть пользователю ошибку, не правильно, с точнки зрения пользователя это не его проблемы. Обработать через retry как вариант DLQ, но когда таких зависимостей много на каждую команду городить DLQ получится оверхед.
В классическом CRUD который работает с одной БД такой проблемы нет, т.к. когда пользователь получит 200 после создания товара, товар будет в БД и будет доступен всем.
Я к тому, что в книжках и презентациях микросервисы и CQRS выглядят гладко, а в реальной жизни куча подводных камней. Мне понравилась фраза что CQRS это боль и его применение нужно очень хорошо обосновать.
Если просто разделить write и read модель то еще десятки лет назад это делили используя для read модели DWH и OLAP а в качестве write модели и source of truth OLTP. Я убежден и сейчас эта связка актуально для большинства задач, при грамотном подходе связка монолита с RDBMS работает отлично. А большинство проблем как-то простой при отказе БД или ошибка в одном методе сломала весь монолит, решается постоением грамотной архитектуры.

Юзер создает товар, рест принял запрос, валидировал, отправил команду создать товар и ответил юзеру 200. По идее юзер должне дождаться ивента от сервиса что товар создан и только после этого создавать заказ.

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

Мне понравилась фраза что CQRS это боль и его применение нужно очень хорошо обосновать.

Абсолютно. Тот же Грег Янг в своём CQRS пейпере пишет, что стандартная архитектура подходит для 80% проектов.

Последнее состояние сущности нужно в 99.9% и каждый раз нагружать систему проигрывать все события для восстановления сущности.

Snapshot’ы же. Все события проигрываются только для совсем маленьких стримов на очень редкие операции.

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

Не откатим, а возьмём состояния на конкретный момент, если это потребуется. Со снепшотами это, опять же, не такая уж страшная операция, как кажется. Откаты я даже не слышал, чтобы кто-то делал — зачем?

Без ES это называется аудит, как вариант вешались триггеры на наиболее важные таблички и писали всю историю в другие таблички. Когда что-то пошло не так, по аудиту можно было восстановить события.

В том-то и дело, что аудит и восстановление состояния всей системы — не одно и то же. Аудит в общем случае делается на узкий скоуп, что позволяет вести, собственно, аудит, который очень далёк от фичи «посмотреть состояние системы в такой-то момент времени».

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

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

Snapshot’ы же. Все события проигрываются только для совсем маленьких стримов на очень редкие операции.

Ну такое, не вижу разницы с аудитом, пока только оверхед хранить все ивенты. Даже для каталога товаров 50К будет как минимум 10 операций на товар итого объем данных возрастет в 10 раз и все равно будут читаться снепшоты.

В том-то и дело, что аудит и восстановление состояния всей системы — не одно и то же

Вот как физически выглядит восстановление состояния всей системы. Например нужно восстановить состояние всей системы на вчера. И главное зачем?

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

Ну, выше я описал, что это не такая уж и проблема в реальной жизни. А реальные очень редки и решаются жёстким two-phase lock, если хочется, чтобы всё обновилось прям одновременно.

Ну такое, не вижу разницы с аудитом, пока только оверхед хранить все ивенты. Даже для каталога товаров 50К будет как минимум 10 операций на товар итого объем данных возрастет в 10 раз и все равно будут читаться снепшоты.

Здесь есть пару моментов:
1. Event Sourcing — не топ-левел архитектура. Т.е., её необязательно прикручивать в 100% доменных контекстов и соответствующих им сервисов. Опять же, это не только не топ-левел архитектура, а даже не что-то обязательное в принципе, поэтому...
2. Там будет точно гораздо больше 10-ти операций на один товар: одно создание, редкие изменения (путём, скорее всего, разных ивентов в зависимости от требований), постоянные покупки, редкие возвраты, deprecation.

Вот как физически выглядит восстановление состояния всей системы. Например нужно восстановить состояние всей системы на вчера. И главное зачем?

Провалидировать гипотезы. Натравить ML на данные. Я недавно, кстати, общался со стартапом-соцсетью, где им нужно было и первое, и второе, и аудит из-за комплаенса. Аудит в том виде, как Вы описываете, применяют точечно и с ограниченным кол-вом данных, а если нет — ещё вопрос, где больше оверхед.

Это все решаетс с помощью такого паттерна, как Saga. Моя рекомендация хорошо почитать: «Микросервисы. Паттерны разработки и рефакторинга». У вас тогда отпадут все вопросы.
И если коротко на ваш вопрос:

И вот событие создания заказа уже получено а товара еще нет, что делать?

Есть компенсирующая транзакция, если что-то пошло, как говорится, не по счастливому пути.

Это все решаетс с помощью такого паттерна, как Saga.

Saga решает только вопрос, что все операции в распределённой транзакции пройдут либо откатятся, а человека волнует eventual consistency как таковая, которая никуда не денется без two-phase lock. Которую, в свою очередь, применять в описываемой проблеме я бы не стал, да и в целом это edge-case средство.

eventual consistency

Именно.... проблема в том что зачастую consistency может не быть на момент времени когда она нужна.

two-phase lock

и EDA думаю не совместимы. Да и сначала городим микросервисы, а когда они обрастают two-phase lock думаем зачем мы нагородили микросерсивы.

Да и сначала городим микросервисы, а когда они обрастают two-phase lock думаем зачем мы нагородили микросерсивы.

Потому что они не обрастают). Мы мобильный банк писали, и даже там 2PL был нужен только 1 раз, даже не скажу точно где, т.к. видел краем глаза, что в коде было... И то, если ничего не путаю, и это был действительно он.

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

За Сагу я в курсе, но тут как бы не тот случай. Юзер создал товар и даже получил уведомление. Дальше он создает заказ, и его бы успешно создал, но беда в том что микросервис, который отвечает за проверку бизнес логики закаказа еще не знает о товаре. Это как бы не проблема юзера и бросать ему сагу извини, попробуй создать заказ через насколько минут как бы не хорошо. Это же проблемы системы и система сама должны их порешать. В принципе их порешать можно через DLQ например, но все это очень усложняет логику, и код.

Дякую!
Але без огляду взаємодії з інфрастуктурою, моделі автентифікації та безпеки, ваші архітектурні рішення виглядають не повністю очевидними...
Очікую на продовження... і про перехід на Go в тому числі;))

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