Транзакции через микросервисы REST?

195

Допустим, у нас есть микросервисы User, Wallet REST и API-шлюз, который склеивает вещи. Когда Боб регистрируется на нашем веб-сайте, нашему API-шлюзу необходимо создать пользователя через микросервис User и кошелек через микросервис Wallet.

Теперь вот несколько сценариев, где что-то может пойти не так:

  • Не удалось создать пользователя Боба: все в порядке, мы просто возвращаем сообщение об ошибке Бобу. Мы используем транзакции SQL, поэтому никто никогда не видел Боба в системе. Все хорошо :)

  • Пользователь Bob создан, но до того, как наш кошелек может быть создан, наш шлюз API сильно падает. Теперь у нас есть пользователь без кошелька (противоречивые данные).

  • Пользователь Bob создан, и когда мы создаем кошелек, HTTP-соединение разрывается. Возможно, создание кошелька прошло успешно, а может и нет.

Какие решения доступны для предотвращения такого несоответствия данных? Существуют ли шаблоны, позволяющие транзакциям охватывать несколько запросов REST? Я читал страницу Википедии о двухфазной фиксации, которая, кажется, затрагивает эту проблему, но я не уверен, как применить ее на практике. Эта Атомная Распределенная Транзакция: документ о дизайне RESTful также кажется интересным, хотя я еще не читал его.

Кроме того, я знаю, что REST может просто не подходить для этого варианта использования. Возможно ли правильный способ справиться с этой ситуацией, чтобы полностью отбросить REST и использовать другой протокол связи, такой как система очереди сообщений? Или я должен обеспечить согласованность в коде моего приложения (например, с помощью фонового задания, которое обнаруживает несоответствия и исправляет их, или с помощью атрибута «состояние» в моей модели пользователя с «созданием», «созданием» значений и т. Д.)?

Оливье Лалонд
источник
3
Интересная ссылка: news.ycombinator.com/item?id=7995130
Оливье Лалонд
3
Если для пользователя нет смысла без кошелька, зачем создавать для него отдельный микросервис? Может быть, что-то не так с архитектурой в первую очередь? Зачем вам нужен общий API-шлюз, кстати? Есть ли какая-то конкретная причина для этого?
Владислав Раструсный
4
@VladislavRastrusny это был вымышленный пример, но вы могли бы подумать, что служба кошелька обрабатывается, например, Stripe.
Оливье Лалонд
Вы можете использовать диспетчер процессов для отслеживания транзакции (шаблон диспетчера процессов) или дать каждому микросервису знать, как вызвать откат (шаблон диспетчера саг), или выполнить какое-то двухфазное принятие ( blog.aspiresys.com/software-product-engineering). / producteering /… )
Эндрю Пэйт
@VladislavRastrusny «Если у пользователя нет смысла без кошелька, зачем создавать для него отдельный микросервис» - например, кроме того, что пользователь не может существовать без кошелька, у него нет общего кода. Таким образом, две команды собираются разрабатывать и развертывать микросервисы User и Wallet независимо друг от друга. Разве не в этом вся суть использования микросервисов?
Ник

Ответы:

148

Что не имеет смысла:

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

Что даст вам головную боль:

  • EJB с распределенными транзакциями . Это одна из тех вещей, которые работают в теории, но не на практике. Сейчас я пытаюсь заставить распределенную транзакцию работать для удаленных EJB-компонентов через экземпляры JBoss EAP 6.3. Мы разговаривали с поддержкой RedHat уже несколько недель, и она пока не работает.
  • Двухфазные фиксации решения в целом . Я думаю, что протокол 2PC - отличный алгоритм (много лет назад я реализовал его в C с помощью RPC). Это требует комплексных механизмов восстановления после сбоев, с повторными попытками, хранилищем состояний и т. Д. Вся сложность скрыта в рамках транзакции (например: JBoss Arjuna). Тем не менее, 2PC не является отказоустойчивым. Есть ситуации, когда транзакция просто не может быть завершена. Затем вам нужно определить и исправить несоответствия базы данных вручную. Если вам повезет, это может происходить один раз в миллион транзакций, но это может происходить один раз в каждые 100 транзакций в зависимости от вашей платформы и сценария.
  • Саги (Компенсационные сделки) . Это накладные расходы на реализацию компенсирующих операций и механизм координации для активации компенсации в конце. Но компенсация также не является доказательством. Вы все равно можете столкнуться с несоответствиями (= некоторая головная боль).

Что, вероятно, лучшая альтернатива:

  • Возможная последовательность . Ни ACID-подобные распределенные транзакции, ни компенсирующие транзакции не являются отказоустойчивыми, и оба могут привести к несоответствиям. Конечная последовательность часто лучше, чем «случайная несогласованность». Существуют разные дизайнерские решения, такие как:
    • Вы можете создать более надежное решение, используя асинхронную связь. В вашем сценарии, когда Боб регистрируется, шлюз API может отправить сообщение в очередь NewUser и сразу же ответить пользователю: «Вы получите электронное письмо для подтверждения создания учетной записи». Служба очереди может обработать сообщение, внести изменения в базу данных за одну транзакцию и отправить электронное письмо Бобу, чтобы уведомить о создании учетной записи.
    • Microservice Пользователь создает запись пользователя и запись кошелька в одной и той же базе данных . В этом случае хранилище кошелька в микросервисе пользователя является копией основного хранилища кошелька, видимой только для микросервиса кошелька. Существует механизм синхронизации данных, основанный на триггерах или периодически включаемый для отправки изменений данных (например, новых кошельков) из реплики на мастер и наоборот.

Но что, если вам нужны синхронные ответы?

  • Переделывать микросервисы . Если решение с очередью не работает, потому что потребителю сервиса сразу нужен ответ, то я бы предпочел переделать функциональность User и Wallet для размещения в одной и той же службе (или, по крайней мере, в одной виртуальной машине, чтобы избежать распределенных транзакций). ). Да, это на шаг дальше от микросервисов и ближе к монолиту, но избавит вас от головной боли.
Пауло Мерсон
источник
4
Возможная последовательность работала для меня. В этом случае очередь «NewUser» должна быть высокой доступности и отказоустойчивой.
Рам Бавиредди
@RamBavireddi Kafka или RabbitMQ поддерживают устойчивые очереди?
v.oddou
@ v.oddou Да, они делают.
Рам Бавиредди
2
@PauloMerson Я не уверен, чем вы отличаетесь. Компенсация транзакций до возможной последовательности. Что если в вашей возможной последовательности создание кошелька не удастся?
Бальзам
2
@balsick Одной из проблем возможных настроек согласованности является повышенная сложность дизайна. Часто требуется проверка согласованности и корректирующие события. Дизайн решения варьируется. В ответ я предлагаю ситуацию, когда запись Wallet создается в базе данных при обработке сообщения, отправленного через брокер сообщений. В этом случае мы могли бы установить канал мертвых писем, то есть, если обработка этого сообщения приводит к ошибке, мы можем отправить сообщение в очередь недоставленных сообщений и уведомить команду, ответственную за «Кошелек».
Пауло Мерсон
66

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

Вместо этого «модель Starbuck» более продуктивна, следуя модели «максимальных усилий» и компенсируя ошибки в процессе. Во-первых, они уверены, что вы платите! Затем есть очереди сообщений с вашим заказом, прикрепленным к чашке. Если что-то пойдет не так в процессе, например, вы не получили свой кофе, это не то, что вы заказали и т. Д., Мы вступаем в процесс компенсации и гарантируем, что вы получите то, что хотите, или вернете вам деньги. Это наиболее эффективная модель. для повышения производительности.

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

  • Не будьте слишком хороши при определении ваших веб-сервисов (я не уверен, что шумиха вокруг микро-сервисов происходит в наши дни: слишком много рисков зайти слишком далеко);

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

  • Создавайте более интеллектуальные сервисы, чтобы их можно было «вызывать» любое количество раз, обрабатывая их с помощью идентификатора пользователя или задачи, которые будут следовать порядку снизу вверх до конца, проверяя бизнес-правила на каждом этапе;

  • Используйте очереди сообщений (JMS или другие) и переключайтесь на процессоры обработки ошибок, которые будут применять операции для «отката», применяя противоположные операции. Кстати, для работы с асинхронным порядком потребуется какая-то очередь для проверки текущего состояния процесса, так что подумай;

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

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

Скажем, веб-сервис призван организовать всю операцию.

Псевдокод веб-сервиса будет выглядеть так:

  1. Позвоните в микросервис создания учетной записи, передайте ему некоторую информацию и какой-то уникальный идентификатор задачи. 1.1 Микросервис создания учетной записи сначала проверит, была ли эта учетная запись уже создана. Идентификатор задачи связан с записью учетной записи. Микросервис обнаруживает, что учетная запись не существует, поэтому создает ее и сохраняет идентификатор задачи. ПРИМЕЧАНИЕ: эту услугу можно вызывать 2000 раз, она всегда будет выполнять один и тот же результат. Служба отвечает «квитанцией, которая содержит минимальную информацию для выполнения операции отмены при необходимости».

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

  3. Оркестр проинформирован об ошибке. Он знает, что должен прервать создание Учетной записи, но сам не сделает этого. Он попросит службу кошелька сделать это, передав свою «минимальную квитанцию ​​об отмене», полученную в конце шага 1.

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

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

  6. Веб-сервис возвращает пользователю, что учетная запись не может быть создана.

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

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

Компенсирующая схема транзакции

user8098437
источник
2
Как вы думаете, вы могли бы расширить этот ответ, чтобы дать более конкретный совет ОП. В настоящее время этот ответ несколько расплывчатый и трудный для понимания. Хотя я понимаю, как подается кофе в Starbucks, мне неясно, какие аспекты этой системы следует эмулировать в службах REST.
JWG
Я добавил пример, связанный с делом, изначально предоставленным в оригинальном посте.
user8098437
2
Просто добавили ссылку на компенсирующий шаблон транзакции, как описано Microsoft.
user8098437
3
Для меня это лучший ответ. Так просто
Оскар Неварез
1
Обратите внимание, что компенсация транзакций может быть невозможна в некоторых сложных сценариях (как блестяще показано в документах Microsoft). В этом примере представьте, что перед созданием кошелька может произойти сбой, кто-то может прочитать подробности о соответствующей учетной записи, сделав вызов GET в службе учетных записей, которая в идеале не должна существовать в первую очередь, поскольку создание учетной записи не удалось. Это может привести к несогласованности данных. Эта проблема изоляции хорошо известна в шаблоне SAGAS.
Анмол Сингх Джагги
32

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

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

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

В качестве альтернативы вы можете подумать о том, почему вы так беспокоитесь о пользователе без кошелька. Верите ли вы, что это вызовет проблемы? Если это так, может быть, иметь их в качестве отдельных вызовов для отдыха - плохая идея. Если пользователь не должен существовать без кошелька, то вам, вероятно, следует добавить кошелек пользователю (в исходном вызове POST для создания пользователя).

Роб Конклин
источник
Спасибо за предложение. Сервисы User / Wallet были вымышленными, просто чтобы проиллюстрировать это. Но я согласен, что я должен разработать систему так, чтобы как можно больше избегать транзакций.
Оливье Лалонд
7
Я согласен со второй точкой зрения. Кажется, что ваш микросервис, который создает пользователь, должен также создать кошелек, потому что эта операция представляет собой атомную единицу работы. Кроме того, вы можете прочитать этот eaipatterns.com/docs/IEEE_Software_Design_2PC.pdf
Саттар Имамов
2
Это на самом деле отличная идея. Отмена - это головная боль. Но создание чего-либо в состоянии ожидания гораздо менее агрессивно. Все проверки были выполнены, но ничего определенного еще не создано. Теперь нам нужно только активировать созданные компоненты. Возможно, мы даже можем сделать это без транзакций.
Тимо
10

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

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

Mithrandir
источник
1
Предполагая, что мы хотим избежать любого и всех 2PC, и предполагая, что служба пользователя записывает данные в базу данных, мы не можем поместить сообщение в очередь событий пользователем, чтобы сделать его транзакционным, что означает, что он может никогда не сделать это Кошелек-сервис.
Роман Харьковский
@RomanKharkovski Действительно важный момент. Одним из способов решения этой проблемы может быть запуск транзакции, сохранение пользователя, публикация события (не являющегося частью транзакции), а затем принятие транзакции. (В худшем случае, крайне маловероятно, что фиксация завершится неудачно, и те, кто откликнется на событие, не смогут найти пользователя.)
Тимо
1
Затем сохраните событие в базе данных, а также объект. Запланируйте задание для обработки сохраненных событий и отправки их посреднику сообщений. stackoverflow.com/a/52216427/4587961
Ян Хонски
7

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

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

Если это просто вопрос касания другого надежного хранилища данных (скажем, такого, которое не может участвовать в ваших транзакциях sql), то, в зависимости от общих параметров системы, я мог бы рискнуть исчезающе малым шансом, что вторая запись не произойдет. Я мог бы ничего не делать, но выдвинуть исключение и справиться с противоречивыми данными с помощью компенсирующей транзакции или даже с помощью специального метода. Как я всегда говорю своим разработчикам: «если подобное происходит в приложении, оно не останется незамеченным».

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

На этом этапе вы можете ввести очередь сообщений вместе с понятием частично созданных пользователей и / или кошельков.

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

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

Роберт Москаль
источник
4

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

techagrammer
источник
3

Какие решения доступны для предотвращения такого несоответствия данных?

Традиционно используются распределенные диспетчеры транзакций. Несколько лет назад в мире Java EE вы, возможно, создали эти сервисы как EJB, которые были развернуты на разных узлах, и ваш шлюз API сделал бы удаленные вызовы этих EJB. Сервер приложений (если он настроен правильно) автоматически, используя двухфазную фиксацию, гарантирует, что транзакция либо фиксируется, либо откатывается на каждом узле, что обеспечивает согласованность. Но для этого необходимо, чтобы все службы были развернуты на одном сервере приложений одного типа (чтобы они были совместимы) и в действительности работали только со службами, развернутыми одной компанией.

Существуют ли шаблоны, позволяющие транзакциям охватывать несколько запросов REST?

Для SOAP (хорошо, а не REST) ​​существует спецификация WS-AT, но ни одна служба, которую мне когда-либо приходилось интегрировать, не поддерживает это. Для REST у JBoss есть кое-что в процессе . В противном случае «шаблон» - это либо найти продукт, который вы можете подключить к своей архитектуре, либо создать собственное решение (не рекомендуется).

Я опубликовал такой продукт для Java EE: https://github.com/maxant/genericconnector

Согласно документу, на который вы ссылаетесь, существует также шаблон «Попробуйте отменить / подтвердить» и связанный с ним продукт от Atomikos.

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

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

Существует много способов «привязки» нетранзакционных ресурсов к транзакции:

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

Или я должен обеспечить согласованность в коде моего приложения (например, с помощью фонового задания, которое обнаруживает несоответствия и исправляет их, или с помощью атрибута «состояние» в моей модели пользователя с «созданием», «созданием» значений и т. Д.)?

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

Муравей Кучера
источник
2

Лично мне нравится идея Micro Services, модулей, определяемых сценариями использования, но, как упоминается в вашем вопросе, у них есть проблемы с адаптацией для классических предприятий, таких как банки, страхование, телекоммуникации и т. Д.

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

Я написал блог о моем предложенном решении, может быть, это может помочь вам ....

https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/

posthumecaver
источник
0

Окончательная последовательность - вот ключ.

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

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

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

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

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

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

Viyaan Jhiingade
источник
-2

Почему бы не использовать платформу API Management (APIM), которая поддерживает сценарии / программирование? Таким образом, вы сможете построить составной сервис в APIM, не мешая микро сервисам. Я разработал, используя APIGEE для этой цели.

SRA
источник