Привет, Хабр!
Сегодня мы предлагаем вашему вниманию небольшой материал о микросервисах и распределенной архитектуре. Он, в частности, затрагивает идею Мартина Фаулера о том, что новая система должна начинаться с монолита, а даже в развитой микросервисной архитектуре целесообразно оставлять большое монолитное ядро.
Приятного чтения!
Сегодня все только и размышляют о микросервисах, а также пишут их – и я не исключение. Если исходить из базовых принципов микросервисов и их истинного контекста, то понятно, что микросервисы – это распределенная система.
Транзакции, охватывающие множество физических систем или компьютеров в сети, называются попросту распределенными транзакциями. В мире микросервисов транзакция распределяется между множеством сервисов, которые вызываются в некоторой последовательности для завершения всей транзакции.
Вот монолитная система интернет-магазина, в которой используются транзакции:
Рис. 1: Транзакция в монолите
Если в вышеприведенной системе пользователь отправляет к платформе запрос на заказ (Checkout), то платформа создает локальную транзакцию в базе данных, и эта транзакция охватывает множество таблиц базы данных, чтобы обработать (Process) заказ и зарезервировать (Reserve) товары со склада. Если любой из этих шагов совершить не удастся, то транзакция может откатиться, что означает отказ как от самого заказа, так и от зарезервированных товаров. Этот набор принципов называется ACID (атомарность, согласованность, изоляция, долговечность) и гарантируется на уровне системы базы данных.
Вот декомпозиция системы интернет-магазина, построенной из микросервисов:
Рисунок 2: Транзакции в микросервисе
Выполнив декомпозицию этой системы, мы создали микросервисы
С внедрением микросервисной архитектуры базы данных утрачивают свою ACID-природу. В силу возможного распространения транзакций между множеством микросервисов и, следовательно, баз данных, приходится иметь дело со следующими ключевыми проблемами:
Атомарность означает, что в любой транзакции могут быть завершены либо все шаги, либо ни одного. Если в вышеприведенном примере не удастся завершить операцию ‘заказать товары’ в методе
Допустим, объект от любого из микросервисов поступает на долговременное хранение в базу данных, и в то же время другой запрос считывает этот же объект. Какие данные должен вернуть сервис – старые или новые? В вышеприведенном примере, когда
Современные системы проектируются с учетом возможных отказов, и одна из основных проблем при обработке распределенных транзакций хорошо сформулирована Патом Хелландом.
Как правило, разработчики просто не делают больших масштабируемых приложений, которые бы предполагали работу с распределенными транзакциями
Две вышеупомянутые проблемы весьма критичны в контексте проектирования и создания приложений на основе микросервисов. Для их решения применяется два следующих подхода:
Как понятно из названия, такой способ обработки транзакций предполагает два этапа: фазу подготовки и фазу фиксации. Важную роль в данном случае играет координатор транзакций, организующий жизненный цикл транзакции.
На подготовительном этапе все микросервисы, участвующие в работе, готовятся к фиксации и уведомляют координатора, что готовы завершить транзакцию. Затем на следующем этапе либо происходит фиксация, либо координатор транзакции выдает всем микросервисам команду выполнить откат.
Вновь рассмотрим для примера систему интернет-магазина:
Рисунок 3: успешная двухфазная фиксация в микросервисной системе
В вышеприведенном примере (рисунок 3), когда пользователь направляет запрос на заказ, координатор
Рисунок 4: Неудавшаяся двухфазная фиксация при работе с микросервисами
В сценарии отказа (рисунок 4) – если в любой момент отдельно взятый микросервис не успеет приготовиться,
Одно из лучших определений согласованности в конечном счете дается на сайте microservices.io: каждый сервис публикует событие всякий раз, когда обновляет свои данные. Другие сервисы подписываются на события. При получении события сервис обновляет свои данные.
При таком подходе распределенная транзакция выполняется как совокупность асинхронных локальных транзакций на соответствующих микросервисах. Микросервисы обмениваются информацией через шину событий.
Опять же, давайте рассмотрим в качестве примера систему, работающую в интернет-магазине:
Рисунок 5: Согласованность в конечном счете / SAGA, успешный исход
В примере выше (рисунок 5), клиент требует, чтобы система обработала заказ. При этом запросе
Вся коммуникация на основе событий между микросервисами происходит через шину событий, а за ее организацию (хореографию) отвечает другая система – так решается проблема с излишней сложностью.
Рисунок 6: Согласованность в конечном счете / SAGA, неудачный исход
Если по какой-то причине
Серьезное преимущество такого подхода заключается в том, что каждый микросервис сосредотачивается лишь на собственной атомарной транзакции. Работа микросервисов не блокируется, если на работу другого сервиса требуется сравнительно много времени. Это также означает, что не требуется блокировать и базу данных. При помощи такого подхода можно обеспечить хорошую масштабируемость системы при работе под высокой нагрузкой, поскольку предлагаемое решение асинхронное и основано на работе с событиями.
Основной недостаток этого подхода заключается в том, что здесь не обеспечивается изоляция при чтении. Таким образом, в вышеприведенном примере клиент увидит, что что заказ был создан, но уже через секунду заказ будет удален в ходе компенсирующей транзакции. Кроме того, когда увеличивается количество микросервисов, усложняется их отладка и поддержка.
Первая альтернатива предложенному подходу – вообще отказаться от распределенных транзакций. Если создается новое приложение, начинайте с монолитной архитектуры, как описано в MonolithFirst у Мартина Фаулера. Процитирую его.
Сегодня мы предлагаем вашему вниманию небольшой материал о микросервисах и распределенной архитектуре. Он, в частности, затрагивает идею Мартина Фаулера о том, что новая система должна начинаться с монолита, а даже в развитой микросервисной архитектуре целесообразно оставлять большое монолитное ядро.
Приятного чтения!
Сегодня все только и размышляют о микросервисах, а также пишут их – и я не исключение. Если исходить из базовых принципов микросервисов и их истинного контекста, то понятно, что микросервисы – это распределенная система.
Что такое распределенная транзакция?
Транзакции, охватывающие множество физических систем или компьютеров в сети, называются попросту распределенными транзакциями. В мире микросервисов транзакция распределяется между множеством сервисов, которые вызываются в некоторой последовательности для завершения всей транзакции.
Вот монолитная система интернет-магазина, в которой используются транзакции:
Рис. 1: Транзакция в монолите
Если в вышеприведенной системе пользователь отправляет к платформе запрос на заказ (Checkout), то платформа создает локальную транзакцию в базе данных, и эта транзакция охватывает множество таблиц базы данных, чтобы обработать (Process) заказ и зарезервировать (Reserve) товары со склада. Если любой из этих шагов совершить не удастся, то транзакция может откатиться, что означает отказ как от самого заказа, так и от зарезервированных товаров. Этот набор принципов называется ACID (атомарность, согласованность, изоляция, долговечность) и гарантируется на уровне системы базы данных.
Вот декомпозиция системы интернет-магазина, построенной из микросервисов:
Рисунок 2: Транзакции в микросервисе
Выполнив декомпозицию этой системы, мы создали микросервисы
OrderMicroservice
и InventoryMicroservice
, обладающие отдельными базами данных. Когда от пользователя приходит запрос на заказ (Checkout), вызываются оба этих микросервиса, и каждый из них вносит изменения в свою базу данных. Поскольку теперь транзакция распространяется на несколько баз данных во множестве систем, она считается распределенной.В чем проблема при совершении распределенных транзакций в микросервисах?
С внедрением микросервисной архитектуры базы данных утрачивают свою ACID-природу. В силу возможного распространения транзакций между множеством микросервисов и, следовательно, баз данных, приходится иметь дело со следующими ключевыми проблемами:
Как поддерживать атомарность транзакции?
Атомарность означает, что в любой транзакции могут быть завершены либо все шаги, либо ни одного. Если в вышеприведенном примере не удастся завершить операцию ‘заказать товары’ в методе
InventoryMicroservice
, то как откатить изменения в ‘обработке заказа’, которые были применены OrderMicroservice
?Как обрабатывать конкурентные запросы?
Допустим, объект от любого из микросервисов поступает на долговременное хранение в базу данных, и в то же время другой запрос считывает этот же объект. Какие данные должен вернуть сервис – старые или новые? В вышеприведенном примере, когда
OrderMicroservice
уже завершил работу, а InventoryMicroservice
как раз выполняет обновление, нужно ли включать в число запросов на заказы, выставленных пользователем, также и текущий заказ? Современные системы проектируются с учетом возможных отказов, и одна из основных проблем при обработке распределенных транзакций хорошо сформулирована Патом Хелландом.
Как правило, разработчики просто не делают больших масштабируемых приложений, которые бы предполагали работу с распределенными транзакциями
Возможные решения
Две вышеупомянутые проблемы весьма критичны в контексте проектирования и создания приложений на основе микросервисов. Для их решения применяется два следующих подхода:
- Двухфазная фиксация
- Согласованность в конечном счете и компенсация / SAGA
1. Двухфазная фиксация
Как понятно из названия, такой способ обработки транзакций предполагает два этапа: фазу подготовки и фазу фиксации. Важную роль в данном случае играет координатор транзакций, организующий жизненный цикл транзакции.
Как это работает
На подготовительном этапе все микросервисы, участвующие в работе, готовятся к фиксации и уведомляют координатора, что готовы завершить транзакцию. Затем на следующем этапе либо происходит фиксация, либо координатор транзакции выдает всем микросервисам команду выполнить откат.
Вновь рассмотрим для примера систему интернет-магазина:
Рисунок 3: успешная двухфазная фиксация в микросервисной системе
В вышеприведенном примере (рисунок 3), когда пользователь направляет запрос на заказ, координатор
TransactionCoordinator
первым делом начинает глобальную транзакцию, обладая полной информацией о контексте. Сначала он отправляет команду prepare микросервису OrderMicroservice
, чтобы создать заказ. Затем отправляет команду prepare к InventoryMicroservice
, чтобы зарезервировать товары. Когда оба сервиса готовы внести изменения, они блокируют объекты от дальнейших изменений и уведомляют об этом TransactionCoordinator
. Как только TransactionCoordinator
подтвердит, что все микросервисы готовы применить свои изменения, он прикажет этим микросервисам сохранить их, запросив фиксацию транзакции. В этот момент все объекты будут разблокированы. Рисунок 4: Неудавшаяся двухфазная фиксация при работе с микросервисами
В сценарии отказа (рисунок 4) – если в любой момент отдельно взятый микросервис не успеет приготовиться,
TransactionCoordinator
отменит транзакцию и начнет процесс отката. На схеме OrderMicroservice
по какой-то причине не смог создать заказ, но InventoryMicroservice
откликнулся, что готов создать заказ. Координатор TransactionCoordinator
запросит отмену на InventoryMicroservice
, после чего сервис откатит все сделанные изменения и разблокирует объекты базы данных.Преимущества
- Такой подход гарантирует атомарность транзакции. Транзакция завершится либо в том случае, когда оба микросервиса сработают успешно, либо в случае, когда микросервисы не внесут никаких изменений.
- Во-вторых, данный подход позволяет изолировать чтение от записи, так как изменения в объектах не видны до тех пор, пока координатор транзакций не зафиксирует эти изменения.
- Данный подход представляет собой синхронный вызов, при котором клиент будет уведомлен об успехе или неудаче.
Недостатки
- Не бывает ничего совершенного; двухфазные фиксации протекают довольно медленно по сравнению с операциями над одним микросервисом. Они сильно зависят от координатора. транзакций, что может значительно замедлять работу системы в период высокой загруженности.
- Другой серьезный недостаток заключается в блокировке строк базы данных. Блокировка может стать узким местом, затрудняющим производительность, причем, может возникнуть взаимная блокировка, где две транзакции намертво стопорят друг друга.
2. Согласованность в конечном счете и компенсация / SAGA
Одно из лучших определений согласованности в конечном счете дается на сайте microservices.io: каждый сервис публикует событие всякий раз, когда обновляет свои данные. Другие сервисы подписываются на события. При получении события сервис обновляет свои данные.
При таком подходе распределенная транзакция выполняется как совокупность асинхронных локальных транзакций на соответствующих микросервисах. Микросервисы обмениваются информацией через шину событий.
Как это работает
Опять же, давайте рассмотрим в качестве примера систему, работающую в интернет-магазине:
Рисунок 5: Согласованность в конечном счете / SAGA, успешный исход
В примере выше (рисунок 5), клиент требует, чтобы система обработала заказ. При этом запросе
Choreographer
порождает событие Create Order (создать заказ), чем начинает транзакцию. Микросервис OrderMicroservice
слушает это событие и создает заказ – если эта операция прошла успешно, то он порождает событие Order Created (Заказ создан). Координатор Choreographer
слушает это событие и переходит к заказу товаров, порождая событие Reserve Items (зарезервировать товары). Микросервис InventoryMicroservice
слушает это событие и заказывает товары; если это событие прошло успешно, то он порождает событие Items Reserved (товары зарезервированы). В данном примере это означает, что транзакция закончена.Вся коммуникация на основе событий между микросервисами происходит через шину событий, а за ее организацию (хореографию) отвечает другая система – так решается проблема с излишней сложностью.
Рисунок 6: Согласованность в конечном счете / SAGA, неудачный исход
Если по какой-то причине
InventoryMicroservice
не удалось зарезервировать товары (рисунок 6), он порождает событие Failed to Reserve Items (Не удалось зарезервировать товары). Координатор Choreographer
слушает это событие и запускает компенсирующую транзакцию, порождая событие Delete Order (удалить заказ). Микросервис OrderMicroservice
слушает это событие и удаляет ранее созданный заказ. Преимущества
Серьезное преимущество такого подхода заключается в том, что каждый микросервис сосредотачивается лишь на собственной атомарной транзакции. Работа микросервисов не блокируется, если на работу другого сервиса требуется сравнительно много времени. Это также означает, что не требуется блокировать и базу данных. При помощи такого подхода можно обеспечить хорошую масштабируемость системы при работе под высокой нагрузкой, поскольку предлагаемое решение асинхронное и основано на работе с событиями.
Недостатки
Основной недостаток этого подхода заключается в том, что здесь не обеспечивается изоляция при чтении. Таким образом, в вышеприведенном примере клиент увидит, что что заказ был создан, но уже через секунду заказ будет удален в ходе компенсирующей транзакции. Кроме того, когда увеличивается количество микросервисов, усложняется их отладка и поддержка.
Заключение
Первая альтернатива предложенному подходу – вообще отказаться от распределенных транзакций. Если создается новое приложение, начинайте с монолитной архитектуры, как описано в MonolithFirst у Мартина Фаулера. Процитирую его.
Более распространен подход, когда система создается в виде монолита, после чего по краям от нее постепенно начинают отсекаться микросервисы. При таком подходе в сердце микросервисной архитектуры остается крупное монолитное ядро, но большинство новых разработок приходится на микросервисы, а монолит остается относительно нетронутым. — Мартин ФаулерЕсли необходимо обновить данные сразу в двух местах в результате одного события, то подход с согласованностью в конечном счете/ SAGA предпочтителен при обработке распределенных транзакций по сравнению с двухфазным подходом. Основная причина в том, что двухфазный подход в распределенной среде не масштабируется. При использовании согласованности в конечном счете также возникает свой набор проблем, например, как атомарно обновлять базу данных и порождать событие. Переходя к такой философии разработки, необходимо изменить ее восприятие как с точки зрения разработчика, так и с точки зрения тестировщика.