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

Транзакции - классическая схема

В простейшем случае монолитного сервиса бэкенд просто работает с БД. Допустим, мы создаем юзера, заказ для него и некую сущность в рамках этого заказа и все это можем выполнить в рамках одной транзакции. Все либо создается, либо нет, поскольку у нашей БД есть ACID (https://ru.wikipedia.org/wiki/ACID) - принципы, которые защищают нас от всего, что только можно.

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

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

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

По сути мы вводим еще одну базу данных, задача которой - контролировать транзакции (теперь это координатор транзакций). И наше действие разбивается на две фазы - сначала все готовятся, потом выполняют коммиты. Т.е. сначала сервис юзеров готовит свой коммит и общается с координатором. Затем сервисы заказов и нотификации присоединяются к тому же ID транзакции. На второй фазе по команде координатора все выполняют свои коммиты. Процедура так и называется - двухфазный коммит (2PC или 2 phase commit).

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

Почему классика не работает

Фокус в том, что в реальном мире из основных свойств распределенной системы - консистентности, доступности и устойчивости к разделению - всегда можно поставить во главу разработки не более двух. Это так называемая CAP-теорема, вот здесь есть ее хороший разбор: https://habr.com/ru/articles/328792/

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

И это trade-off - наименьшее из зол, которое нашла индустрия. Просто остальные варианты еще хуже.

Хореография vs оркестрация

Есть два подхода к организации транзакций в распределенных системах - описанный выше двухфазный коммит и его альтернатива - SAGA паттерн. SAGA может быть двух видов - на основе хореографии или оркестрации. Рассмотрим их на примере той же цепочки действий: 

  • создаем юзера;

  • для него создаем заказ;

  • для заказа отправляем уведомление.

Хореография - о том, что все сервисы “танцуют” вместе. У них нет одного начальника, они взаимодействуют друг с другом напрямую, постепенно выполняя всю необходимую работу.

Наш пример с хореографией будет выглядеть следующим образом. 

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

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

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

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

Пример с кинотеатром

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

В монолите оба действия можно засунуть в одну транзакцию и всё, у нас всё выполнится атомарно - либо деньги спишутся и билет забронируется, либо ничего из этого не произойдёт, т. к. транзакция откатится (представим себе для этого примера, что списание средств делается путем вычитания из поля “баланс” в БД). Но если деньги списывает один сервис, а бронирует другой, сделать это в одной транзакции уже не выйдет. Мы, конечно, можем забить, и оставить всё как есть, без транзакции. И тогда если мы сначала спишем деньги, а потом забронируем место, может оказаться, что это место уже недоступно, ведь его мог успеть занять кто-то другой, пока мы списывали средства. Наш клиент деньги заплатит, а своё место не получит. Если же мы сначала забронируем место, а списать средства с клиента не сможем (банковская карта просрочена или на ней нет денег), то билет клиенту достанется бесплатно. В англоязычной литературе проблема называется dual write problem (https://www.confluent.io/blog/dual-write-problem/). 

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

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

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

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

Пример с агентством путешествий

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

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

При чем тут масштаб

Выбор между хореографией и оркестрацией обычно зависит от масштабов системы. 

У хореографии довольно узкий скоуп - каждый сервис “видит” себя и своих соседей, с которыми общается. Никто из них не “видит” систему целиком. Это и радость, и печаль. Для небольших систем все просто. Но для крупных - где микросервисов хотя бы штук 20 - становится вообще непонятно, в каком состоянии вся система и где именно проблема, из-за которой все встало колом. В итоге хореографию не рекомендуют использовать там, где в последовательность выстроено очень много сервисов или есть какие-то сложные взаимодействия, поскольку бизнес-логика получается размазана по всем сервисам. Мы рассылаем события, делая вид, что не знаем, кто на что подписан и подписан ли вообще - что у нас слабая связанность. Но на самом деле мы же не отправляем бесполезные события просто так. Мы отправляем только то, что, как мы знаем, потребуется другому сервису. И обычно даже знаем, какому. То есть сценарий не явно прописан и размазан по системе. И это сложнее поддерживать, потому что здесь нет одного файла, где бы мы увидели все это описание. Никакой сервис целиком не понимает сценарий. Если система начнет разрастаться, с хореографией будет очень тяжело.

В итоге хореография лучше подходит для простых случаев.

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

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

Может показаться, что оркестратор - это по сути монолит, из которого мы просто повыдергивали отдельные сервисы. Но на самом деле это не так. Оркестратор - агностик, он ничего не знает про наш бизнес и его логику. Весь бизнес-процесс описан в отдельном файле - это может быть XML или даже код Java. Этот файл и есть то, что осталось от монолита.

Иными словами, выбор между хореографией и оркестрацией - это очередной trade off в мире микросервисов. В целом мне кажется, что в этом мире вообще нет хороших решений. Есть боли и ты просто выбираешь, какая боль сегодня подходит тебе лучше.

Готовые решения

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

Список готовых фреймворков на GitHub: https://github.com/meirwah/awesome-workflow-engines

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

Статья написана по горячим следам с тренинга по распределенным транзакциям от Дмитрия Литвина (@Captain_Jack).

Посмотреть и почитать по теме:

P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на нашу страницу в VK или на Telegram-канал, чтобы узнавать обо всех публикациях и других новостях компании Maxilect.

Комментарии (5)


  1. Caefah
    22.08.2024 13:12

    В упомянутом списке фреймворков почему-то не упомянут Enduro/X (или незаслуженно забыт).


    1. Captain_Jack
      22.08.2024 13:12

      Enduro/X похоже делает 2 phase commit, а оркестраторы в списке делают саги.

      Ещё это похоже, если я правильно читаю, это комбайн, который делает ещё 10 разных вещей кроме оркестрации транзакций, например может использоваться как брокер сообщений.


  1. nronnie
    22.08.2024 13:12

    не пишите свой оркестратор или workflow менеджер.

    Да как же без этого жить :))) Два месяца когда то убил на написание своего workflow engine когда перед этим после моего робкого предложения использовать для этого MassTransit (которого, к слову сказать, я сам далеко не фанат) меня в навозе вываляли. Упаси боже - это ведь целую чужую *.dll надо в проект включать в качестве зависимости.

    Если серьёзно, насчет статьи, то я бы еще прошелся по шаблону "Compensating Transaction".


  1. mishamota
    22.08.2024 13:12

    Для простой статьи совсем непонятный переход от микросервисов и двухфазного комита к CAP теореме, которая относится к распределённым системам. Вы случайно не путаете микросервисную архитектуру и распределённые системы (https://microservices.io/articles/scalecube.html) ?


  1. vlio
    22.08.2024 13:12

    Не надо SAGA заглавными, это просто слово "сага ". Она же Long-running Action (LRA).