Top.Mail.Ru

Распределенные транзакции для самых маленьких

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

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

В простейшем случае монолитного сервиса бэкенд просто работает с БД. Допустим, мы создаем юзера, заказ для него и некую сущность в рамках этого заказа и все это можем выполнить в рамках одной транзакции. Все либо создается, либо нет, поскольку у нашей БД есть 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 менеджеры и как они устроены (и осознанно выбирать их не использовать, если уж на то пошло).

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

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

Наши статьи по теме:

Все статьи

Связаться с нами

Мы свяжемся с вами в течение 24 часов.