Pull to refresh

Comments 23

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

Пояснительная бригада прибыла.

как именно выполнялись ролбеки

У каждого домена для каждого переходного состояния (то есть когда какой-то процесс происходит над сущностью) есть обработка "хорошего" и "плохого" сценария. Например, при создании ВМ в состоянии, когда мы ждем запуска ОС, в "хорошем" сценарии (при ответе, что ОС запущена), мы перейдем в следующее состояние, попутно затригеррив соответствующее действие. В "плохом" сценарии (при ответе, что произошла ошибка), мы переходим в соответствующее началу роллбека состояние, попутно запустив удаление ВМ. Для каждого отдельно взятого состояния конкретное действие будет разным, потому что в разных состояниях может понадобится выполнять разные действия для роллбека, но процесс роллбека запускается в любом случае, и проходит через все состояния, чтобы откатить/удалить сущность.

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

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

как обеспечивалась работа параллельных транзакций

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

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

Привет. Так как тема интересная для меня, позволю себе несколько комментариев и вопросов.

«Ой да сагу добавим, и все заработает». В действительности это не так.

Да нет, в действительности это именно так и есть. Описанное здесь, это ни что иное, как сага, потому как Сага это именно воплощение распределённой во времени бизнес транзакции. Сага неминуема в высоко распределённых системах, если нужно выполнить действия в разных компонентах связанные друг с другом для достижения общего и конечного результата. Непонятно почему автор продолжает рассказывать о "распределённой транзакции" и почему отбрыкивается от концепции Саги. Хотелось бы узнать.

CQRS

Не очень понятно, почему вы делаете такой упор на CQRS в этой статье? В принципе этот аспект совершенно не интересен в этом процессе. Для чего существует CQRS? Только для того, чтобы упростить канал чтения и снизить нагрузку на систему - нет никакого смысла загружать полную модель (под)системы с кучей сложных бизнес объектов только для того, чтобы отобразить что-то на экране или ответить на какой-то заранее известный запрос. Концепция CQRS создавалась Грегом Янгом именно исходя из этих требований. Наложения SOA там не было никогда (это к вопросу о командах).

DDD

Очень заинтересовало упоминание DDD в данном контексте. Вообще DDD хорошо накладывается на реальность там, где есть очень сложный набор бизнес требований, очень высокая сложность моделирования и очень частое изменение этих бизнес аспектов. Конечно из описанного выше сложно ответить однозначно, но сложной модели и взаимосвязи между компонентами тут совсем не наблюдается. Мне показалось, что DDD тут больше для "красного словца" - моделировать кластер, группы машин и машины как-то совсем не сложно для задачи из развертывания и отслеживания результата развёртывания. Хотелось бы понять, что же такого дало DDD в данном случае, потому как DDD подразумевает очень серьезный набор непростых концепций - entites, value objects, bounded contexts, aggregates, repostitories, etc., etc. Опять же агрегаты DDD - это напрямую о transactional boundaries consistency, что в случае с данной областью задач вызывает вопросы. Мне кажется, если и надо было концентрироваться на чём-то в этой статье, то именно на этой области.

Если я понял правильно, то создаётся объектная модель будущего кластера (с узлами и прочим) и после этого выполняются команды по развертыванию каждого объекта в кластере. Модель подгружается как результат обработки сообщения на шине и обновляется для каждого шага и объекта? Если это так, то это именно то, что обычно делается в Саге и объекте состояния Саги - объекте, который и описывает состояние бизнес процесса во времени.

Доменная логика «не вытекает» из домена, а доменные транзакции независимы

Ну, вообще-то, в этом вся концепция домена и агрегата в DDD - оттуда ничего и никогда не "вытекает", потому как это чистейшая абстракция бизнес требований и процессов. Модель создаётся из сохранённого состояния и исполняется для того, чтобы получить новое состояние модели, которое потом и сохраняется с использованием репозитории.

Термин "домен"

Вы очень смело и часто используете этот термин. Где-то даже приравниваете его к микросервису. Это в корне неверно! Домен это большая область проблемы или её решения. Если уже речь о DDD, то говорить надо об агрегатах. Зачастую микросервсисы либо содержат один агрегат, либо же больше - чтобы зазря не расходывать compute resources и объединять код, который подчиняется одним критериям масштабируемости, либо же versioning, либо же release cadence.

От меня

То, что вы выполняете запросы через p2p протокол между вашими подами - это не есть хорошо. Это означает, что у вас (с большой долей вероятности) неправильно проведены границы между агрегатами и bounded contexts, что у вас высокий уровень зависимости компонентов (temporal and spatial). Вам надо глубже погрузиться в SOA, EDA и уже потом накладывать DDD и иногда добавлять CQRS (но только иногда и в основном для UI), чтобы избавиться от p2p взаимодействия.

Заключение

Хочу добавить, что статья так и не даёт толкового объяснения вашей реализации - разговор о том, что это "транзакция в транзакции, поэтому их все видно и это круто" вызывает больше вопросов, чем ответов. Было бы интересно увидеть, что же именно скрывается за этим "транзакции в транзакциях", потому как с их помощью вы опровергаете саги, привносите DDD и говорите о преимуществах. Всё же речь здесь не о транзакциях, а о бизнес процессе создания любого ресурса, результат которого становится известен вызывающему (ну или любому интересующемуся, так как вы используете шину Kafka). И поэтому наблюдаемость любого такого процесса, в принципе, тривиальная задача решаемая через статус каждого компонента и какого-то UI, умеющего эти статусы показывать.

Хорошие вопросы, попробую ответить на каждый

«Ой да сагу добавим, и все заработает». В действительности это не так.

Да нет, в действительности это именно так и есть. Описанное здесь, это ни что иное, как сага, потому как Сага это именно воплощение распределённой во времени бизнес транзакции. Сага неминуема в высоко распределённых системах, если нужно выполнить действия в разных компонентах связанные друг с другом для достижения общего и конечного результата. Непонятно почему автор продолжает рассказывать о "распределённой транзакции" и почему отбрыкивается от концепции Саги. Хотелось бы узнать.

Может быть у вас какие-то особые саги, поделитесь. Но все, что я видел (и в книгах, и к сожалению вживую) это

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

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

Второй вариант с событиями и всем таким - это реактивный подход, когда каждый сервис реагирует на определенное событие каким-то действием. Можно конечно вместо событийной модели построить командную (message-based), но тогда возникает риск либо прийти к оркестратору (который будет рассылать команды), либо к протеканию одного сервиса в другой, и так или иначе я вижу event-based обычно в связке с хореографической сагой.

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

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

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

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

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

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

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

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

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

В итоге в дереве вызовов (или дереве транзакций) из A->B->C получается, что A оркестрирует B, а B оркестрирует C (в статье даже есть картинка). При этом А ничего не знает про С, С не знает про B, и B не знает про A, и это больше похоже на хореографическую сагу.

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

CQRS

Не очень понятно, почему вы делаете такой упор на CQRS в этой статье? В принципе этот аспект совершенно не интересен в этом процессе. Для чего существует CQRS? Только для того, чтобы упростить канал чтения и снизить нагрузку на систему - нет никакого смысла загружать полную модель (под)системы с кучей сложных бизнес объектов только для того, чтобы отобразить что-то на экране или ответить на какой-то заранее известный запрос. Концепция CQRS создавалась Грегом Янгом именно исходя из этих требований. Наложения SOA там не было никогда (это к вопросу о командах).

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

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

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

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

Я не пурист, поэтому предпочитаю использовать существующие практики и адаптировать их к конкретным задачам, нежели стремиться строго советовать постулатам (очень общим и абстрактным в конкретном случае), выдвинутым довольно давно и уже обросшими своими практиками. DDD, как мы знаем, тоже не только Big Blue Book, а имеет множество интерпретаций, в том числе противоречивых. Грегу привет.

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

DDD

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

Если кратко - сложность есть, и она довольно большая, но статья сильно не про это (а еще есть NDA), поэтому я очень сильно упрощал в статье и сами модели, и примеры FSM. Ради примера количество возможных состояний одного из доменов в агрегате больше сорока, таких доменов больше полдюжины. Изначально мы решали проблему того, что логики очень много и она сложная, и как раз DDD (и гексагон как практическая реализация на уровне архитектуры) помогли эту проблему решить.

Ну общая сложность всего домена проблемы (problem domain) или домена решения (solution domain) в данном случае не должна напрямую отображаться в решении этой задачи. Именно поэтому я и задумался - что же такого даёт вам DDD в этом контексте помимо довольно простой объектной модели и доменного сервиса, который и выполняет роль координатора саги, как я понял из вашего объяснения. Отсюда и моё мнение, что DDD в данном случае - перебор.

Доменная логика «не вытекает» из домена, а доменные транзакции независимы

Ну, вообще-то, в этом вся концепция домена и агрегата в DDD - оттуда ничего и никогда не "вытекает", потому как это чистейшая абстракция бизнес требований и процессов. Модель создаётся из сохранённого состояния и исполняется для того, чтобы получить новое состояние модели, которое потом и сохраняется с использованием репозитории.

Все так. В этой конкретной цитате есть противопоставление оркестрируемой саге, где логика как раз протекает в оркестратор.

Наверное я не очень понял, но разве ваш доменный сервис не играет роль того самого дирижёра/оркестратора саги? Разве это не в этом доменном сервисе вы реализуете логику саги?

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

От меня

То, что вы выполняете запросы через p2p протокол между вашими подами - это не есть хорошо. Это означает, что у вас (с большой долей вероятности) неправильно проведены границы между агрегатами и bounded contexts, что у вас высокий уровень зависимости компонентов (temporal and spatial). Вам надо глубже погрузиться в SOA, EDA и уже потом накладывать DDD и иногда добавлять CQRS (но только иногда и в основном для UI), чтобы избавиться от p2p взаимодействия.

А что вы собственно подразумеваете под p2p протоколом и какую альтернативу предлагаете?

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

Моё предположение о зависимости строится на простом наблюдении на протяжении многих лет - как только программистам разрешается открывать соединения к другим сервисам/компонентам/микросервисам - они постепенно начнут это делать, чтобы неминуемо будет приводить к размытию границ bounded contexts, появлению распределенных транзакций (не бизнес, а тех самых плохих, которые замучаешься побеждать), нестабильностям, и многому другому. И, так как вы сказали, что у вас микросервисы общаются друг с другом через gRPC всё вышеприведенное это только вопрос времени. Но опять же, основываю свои предположения на понимании того, что вы тут рассказали.

Что касается альтернатив, то в моём опыте EDA очень сильно решает эту проблему, правда необходимо придерживаться SOA и уже потом поверх всего этого накладывать DDD. Тогда, по опыту, шансы увеличить зависимость компонентов очень снижаются. Окончательно убрать это невозможно в силу человеческого фактора, но тем не менее - ситуация лучше.

Через gRPC происходит чтение данных моделей других сервисов (доменов). Я не знаю, какую альтернативу вы тут видите - агрегация всех данных в каком-то data lake и чтение оттуда, либо переход на event-sourcing и построение локального view каждой модели внутри сервиса?

Мы не используем event-sourcing, у нас более классический подход, основанный на CQRS, где запросы выполняются синхронно по gRPC, а команды асинхронно через сообщения. Где вы в такой модели видите размытия границ, я не вижу, при том, что запросы на чтение ничего не изменяют (а значит, никакого bounded contexts по факту там нет, да и доменов нет, потому что для запросов в CQRS отдельные упрощенные и оптимизированные для скорости чтения и кеширования модели), а запросы на изменение (команды) четко ограничены обработкой внутри домена, и никаких открытых соединений с другими сервисами в процессе этого нет, только чтение из брокера и запись в брокер..

Заключение

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

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

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

А каковы масштабы у вас? Сколько сервисов и сколько событий происходит в рамках среднего процесса?

Вот у нас в Cloudbreak в опенсорсе,так что могу чуток сравнить,не выходя из nda. Тоже выделяются ресурсы от облачного провайдера (все главные) и ставится ПО, процесс совсем не быстрый и с кучей "возможны варианты“ и ещё чаще,увы, "что то пошло не туда". И, интересное дело, тоже решили использовать конечные автоматы. Так как все на джаве и спринге, то был "допилен" spring state machine (Cloudbreak flow) под это дело, состояния в постгресе, в общем - вот где теория программного инженерного подхода встретилась с жизнью, наконец-то. Откаты бизнес транзакций через состояния, персистентность машин... Однако... Это написали давно и нынешнее поколение слегка боится этого кода. Мягко говоря,никого не знаю,кто не пытается избежать этих кусков. Описания FSM даже в специальных фреймворках и dsl слабо читаемы. Из того,что я видел - далее чем прямолинейный код никто не разумеет - местных теории конечных автоматов не учат. Как ни хороша задумка - она привела к слабо поддерживаемом у коду. Судя по всему люди,это писавшие, давно уволились, ибо "улучшили" spring state machine " V1 и апдейтить некому. Так что с переходом на k8s вопроса в переиспользовании не было - будем делать по человечески,а не по академически

Мы пишем на go, поэтому FSM решили реализовать в духе go-way - максимально просто и читабельно. В итоге каждый FSM это обычный switch вида

	switch state {
	case model.InitialState:
		switch event {
		case eventNodeChanged:
			// ...

		case eventNodeDeleted:
			// ...
		}

	case model.NodeInstanceCreationPending:
		switch event {
		case eventNodeChanged:
			// ...

		case eventNodeDeleted:
			// ...
		}

	case ...
	}

Да, в отдельных случаях такой свич разрастается до внушительных размеров, и его становится не очень удобно читать. Но при этом такой код понятен "без дебаггера" и прост в доработке и траблшутинге. Изначально тоже пробовал разные библиотеки, думал над табличной или иной динамической реализацией, но здравый смысл в итоге победил, и самое простое решение оказалось самым простым. Более того, код в принципе понятен и тем, кто не сильно знаком с концепцией конечных автоматов, потому что он (код) прост и вполне линееню

Sign up to leave a comment.