1. Введение
В этой статье мы обсудим варианты реализации транзакции между микросервисами.
Мы также рассмотрим некоторые альтернативы транзакциям в сценарии распределенного микросервиса.
2. Избегайте транзакций между микросервисами
Распределенная транзакция — очень сложный процесс с множеством движущихся частей, которые могут дать сбой. Кроме того, если эти части работают на разных машинах или даже в разных центрах обработки данных, процесс совершения транзакции может стать очень долгим и ненадежным.
Это может серьезно повлиять на работу пользователя и общую пропускную способность системы. Таким образом , один из лучших способов решить проблему распределенных транзакций — полностью их избежать.
2.1. Пример архитектуры, требующей транзакций
Обычно микросервис проектируется таким образом, чтобы быть независимым и полезным сам по себе. Он должен уметь решать какую-то атомарную бизнес-задачу.
Если бы мы могли разделить нашу систему на такие микросервисы, то, скорее всего, нам вообще не пришлось бы реализовывать транзакции между ними.
Например, рассмотрим систему широковещательного обмена сообщениями между пользователями.
Пользовательский микросервис будет связан с профилем пользователя (создание нового пользователя, редактирование данных профиля и т. д.) со следующим базовым классом домена :
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private String name;
@Basic
private String surname;
@Basic
private Instant lastMessageTime;
}
Микрослужба сообщений
будет связана с широковещательной рассылкой. Он инкапсулирует объект Message
и все, что вокруг него:
@Entity
public class Message implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private long userId;
@Basic
private String contents;
@Basic
private Instant messageTimestamp;
}
Каждый микросервис имеет свою базу данных. Обратите внимание, что мы не ссылаемся на сущность User
из сущности Message
, поскольку пользовательские классы недоступны из микрослужбы сообщений .
Обращаемся к пользователю только по id.
Теперь сущность User содержит поле
lastMessageTime
, потому что мы хотим показать информацию о времени последней активности пользователя в ее профиле.
Однако, чтобы добавить пользователю новое сообщение и обновить его lastMessageTime
, теперь нам нужно реализовать транзакцию между микросервисами.
2.2. Альтернативный подход без транзакций
Мы можем изменить архитектуру микросервиса и удалить поле lastMessageTime
из объекта User
.
Затем мы могли бы отобразить это время в профиле пользователя, выдав отдельный запрос к микросервису сообщений и найдя максимальное значение messageTimestamp
для всех сообщений этого пользователя.
Вероятно, если микросервис сообщений
сильно загружен или вообще не работает, мы не сможем показать время последнего сообщения пользователя в его профиле.
Но это может быть более приемлемым, чем неспособность зафиксировать распределенную транзакцию для сохранения сообщения только потому, что пользовательский микросервис не ответил вовремя.
Конечно, есть более сложные сценарии, когда нам нужно реализовать бизнес-процесс в нескольких микросервисах , и мы не хотим допускать несогласованности между этими микросервисами.
3. Двухфазный протокол фиксации
Протокол двухфазной фиксации (или 2PC) — это механизм реализации транзакции между различными программными компонентами (несколько баз данных, очереди сообщений и т. д.).
3.1. Архитектура 2PC
Одним из важных участников распределенной транзакции является координатор транзакции. Распределенная транзакция состоит из двух шагов:
- Этап подготовки — на этом этапе все участники транзакции готовятся к фиксации и уведомляют координатора о готовности завершить транзакцию.
- Фаза фиксации или отката — на этой фазе координатор транзакции выдает всем участникам команду фиксации или отката.
Проблема с 2PC в том, что он довольно медленный по сравнению со временем работы одного микросервиса.
Координация транзакций между микросервисами, даже если они находятся в одной сети, может сильно замедлить работу системы , поэтому этот подход обычно не используется в сценариях с высокой нагрузкой.
3.2. Стандарт ХА
Стандарт XA — это спецификация для проведения распределенных транзакций 2PC между вспомогательными ресурсами. Любой JTA-совместимый сервер приложений (JBoss, GlassFish и т. д.) поддерживает его по умолчанию.
Ресурсами, участвующими в распределенной транзакции, могут быть, например, две базы данных двух разных микросервисов.
Однако, чтобы воспользоваться этим механизмом, ресурсы должны быть развернуты на одной платформе JTA. Это не всегда возможно для микросервисной архитектуры.
3.3. REST-AT Стандартный проект
Другой предлагаемый стандарт — REST-AT , который был доработан RedHat, но до сих пор не вышел из стадии черновика. Однако он поддерживается сервером приложений WildFly из коробки.
Этот стандарт позволяет использовать сервер приложений в качестве координатора транзакций со специальным REST API для создания и присоединения к распределенным транзакциям.
Веб-службы RESTful, которые хотят участвовать в двухфазной транзакции, также должны поддерживать определенный REST API.
К сожалению, для бриджа распределенной транзакции на локальные ресурсы микросервиса нам все равно пришлось бы либо разворачивать эти ресурсы на единую JTA-платформу, либо решать нетривиальную задачу по написанию этого бриджа самостоятельно.
4. Возможная согласованность и компенсация
На сегодняшний день одной из наиболее осуществимых моделей обеспечения согласованности между микросервисами является консистентность в конечном счете .
Эта модель не применяет распределенные транзакции ACID между микрослужбами. Вместо этого предлагается использовать некоторые механизмы обеспечения того, чтобы система в конечном итоге была непротиворечивой в какой-то момент в будущем.
4.1. Случай окончательной согласованности
Например, пусть нам нужно решить следующую задачу:
- зарегистрировать профиль пользователя
- сделать некоторую автоматическую фоновую проверку, что пользователь действительно может получить доступ к системе
Вторая задача — убедиться, например, что этот пользователь не был забанен на наших серверах по какой-либо причине.
Но это может занять время, и мы хотели бы выделить его в отдельный микросервис. Было бы неразумно заставлять пользователя ждать так долго только для того, чтобы узнать, что он успешно зарегистрирован.
Один из способов решить эту проблему — использовать подход, основанный на сообщениях, включая компенсацию. Рассмотрим следующую архитектуру:
- пользовательский микросервис, которому поручено зарегистрировать профиль
пользователя
микросервис валидации
, которому поручено выполнять проверку биографических данных- платформа обмена сообщениями, которая поддерживает постоянные очереди
Платформа обмена сообщениями может обеспечить сохранение сообщений, отправленных микрослужбами. Затем они будут доставлены позже, если приемник в настоящее время недоступен.
4.2. Счастливый сценарий
В этой архитектуре удачным сценарием будет:
пользовательский микросервис регистрирует
пользователя
, сохраняя информацию о нем в своей локальной базе данныхпользовательский микросервис помечает этого пользователя флажком
.
Это может означать, что этот пользователь еще не прошел проверку и не имеет доступа ко всем функциям системы.пользователю отправляется подтверждение регистрации с предупреждением о том, что не весь функционал системы доступен сразу
пользовательский микросервис отправляет сообщение
микросервису
проверки
, чтобы выполнить фоновую проверку пользователямикросервис проверки запускает фоновую
проверку
и отправляет сообщениепользовательскому
микросервису с результатами проверкиесли результаты положительные,
пользовательский
микросервис разблокирует пользователяесли результаты отрицательные,
пользовательский
микросервис удаляет учетную запись пользователя.
После того, как мы выполнили все эти шаги, система должна быть в согласованном состоянии. Однако какое-то время объект пользователя находился в незавершенном состоянии.
Последний шаг, когда пользовательский микросервис удаляет недействительную учетную запись, — это этап компенсации .
4.3. Сценарии отказа
Теперь рассмотрим несколько сценариев отказа:
- если микрослужба
проверки
недоступна, то платформа обмена сообщениями с ее функцией постоянной очереди гарантирует, что микрослужбапроверки
получит это сообщение в более позднее время . - предположим, что платформа обмена сообщениями выходит из строя, тогда
пользовательский
микросервис пытается снова отправить сообщение через какое-то время, например, путем запланированной пакетной обработки всех пользователей, которые еще не прошли проверку. - если микрослужба
проверки
получает сообщение, проверяет пользователя, но не может отправить ответ из-за сбоя платформы обмена сообщениями, микрослужбапроверки также повторяет попытку отправки сообщения через некоторое время.
- если одно из сообщений было потеряно или произошел какой-то другой сбой,
пользовательский
микросервис находит всех не верифицированных пользователей с помощью плановой пакетной обработки и снова отправляет запросы на валидацию
Даже если некоторые сообщения будут отправлены несколько раз, это не повлияет на согласованность данных в базах данных микросервисов.
Тщательно рассмотрев все возможные сценарии отказа, мы можем гарантировать, что наша система будет удовлетворять условиям возможной согласованности. В то же время нам не нужно было бы иметь дело с дорогостоящими распределенными транзакциями.
Но мы должны понимать, что обеспечение согласованности в конечном итоге является сложной задачей. У него нет единого решения для всех случаев.
5. Вывод
В этой статье мы обсудили некоторые механизмы реализации транзакций в микросервисах.
Кроме того, мы также изучили некоторые альтернативы такому стилю транзакций.