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

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

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

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

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

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

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

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

10 ответов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Создайте пользователя BOB с событием CREATE USER и отправьте сообщение в шину сообщений.
  2. Служба кошелька, подписанная на это событие, может создать кошелек, соответствующий пользователю.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам