Посібник із транзакцій між мікросервісами

1. Вступ

У цій статті ми обговоримо варіанти здійснення транзакції між мікрослугами.

Ми також перевіримо деякі альтернативи транзакціям у сценарії розподіленого мікросервісу.

2. Уникнення транзакцій між мікросервісами

Розподілена транзакція - це дуже складний процес з великою кількістю рухомих частин, які можуть вийти з ладу. Крім того, якщо ці частини працюють на різних машинах або навіть в різних центрах обробки даних, процес здійснення транзакції може стати дуже довгим і ненадійним.

Це може серйозно вплинути на взаємодію з користувачем та загальну пропускну здатність системи. Тож одним із найкращих способів вирішити проблему розподілених транзакцій є їх повне уникнення.

2.1. Приклад архітектури, що вимагає транзакцій

Зазвичай мікросервіс розроблений таким чином, щоб бути самостійним та корисним самостійно. Він повинен мати можливість вирішити якесь атомне ділове завдання.

Якби ми могли розділити нашу систему на такі мікросервіси, є великий шанс, що нам взагалі не потрібно буде здійснювати транзакції між ними.

Наприклад, розглянемо систему широкомовних повідомлень між користувачами.

Користувач microservice буде пов'язаний з профілем користувача (створення нового користувача, редагування даних профілю і т.д.) за допомогою наступної основний клас домену:

@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; }

Повідомлення microservice стосуватиметься мовлення. Він інкапсулює сутність 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 , оскільки до класів користувачів не можна отримати доступ з мікросервісу повідомлень . Ми посилаємось на користувача лише за ідентифікатором.

Тепер сутність User містить поле lastMessageTime, оскільки ми хочемо показати інформацію про час останньої активності користувача у її профілі.

Однак, щоб додати нове повідомлення користувачеві та оновити її lastMessageTime , нам зараз доведеться реалізувати транзакцію між мікросервісами.

2.2. Альтернативний підхід без операцій

Ми можемо змінити нашу архітектуру мікросервісу та видалити поле lastMessageTime з сутності User .

Тоді ми могли б відобразити цей час у профілі користувача, подавши окремий запит до мікросервісу повідомлень і знайшовши максимальне значення messageTimestamp для всіх повідомлень цього користувача.

Можливо, якщо мікросервіс повідомлення перебуває під великим навантаженням або навіть не працює, ми не зможемо показати час останнього повідомлення користувача в її профілі.

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

Звичайно, є більш складні сценарії, коли нам доводиться реалізовувати бізнес-процес на декількох мікросервісах, і ми не хочемо допускати невідповідність цих мікросервісів.

3. Двофазний протокол коміту

Двофазний протокол коміту (або 2PC) - це механізм реалізації транзакції між різними програмними компонентами (декілька баз даних, черги повідомлень тощо)

3.1. Архітектура 2PC

Одним з важливих учасників розподіленої транзакції є координатор транзакцій. Розподілена транзакція складається з двох етапів:

  • Підготовча фаза - на цій фазі всі учасники транзакції готуються до фіксації та повідомляють координатора, що готові завершити транзакцію
  • Етап коміту або відкоту - на цьому етапі координатор транзакції видає команду фіксації або відкату всім учасникам

Проблема 2PC полягає в тому, що вона досить повільна в порівнянні з часом роботи однієї мікросервісної служби.

Координація транзакцій між мікросервісами, навіть якщо вони знаходяться в одній мережі, може по-справжньому уповільнити роботу системи , тому такий підхід зазвичай не використовується у сценарії високого навантаження.

3.2. Стандарт XA

Стандарт XA є специфікацією для проведення розподілених транзакцій 2PC через допоміжні ресурси. Будь-який сумісний з JTA сервер додатків (JBoss, GlassFish тощо) підтримує його нестандартно.

Ресурсами, що беруть участь у розподілених транзакціях, можуть бути, наприклад, дві бази даних двох різних мікросервісів.

Однак, щоб скористатися цим механізмом, ресурси слід розподілити на єдиній платформі JTA. Це не завжди можливо для архітектури мікросервісу.

3.3. Стандартний проект REST-AT

Іншим запропонованим стандартом є REST-AT, який зазнав певної розробки від RedHat, але все ще не вийшов зі стадії проекту. Однак це підтримується сервером додатків WildFly нестандартно.

Цей стандарт дозволяє використовувати сервер додатків як координатора транзакцій із певним API REST для створення та приєднання розподілених транзакцій.

Веб-сервіси RESTful, які бажають взяти участь у двофазній транзакції, також повинні підтримувати певний API REST.

На жаль, щоб зв’язати розподілену транзакцію з локальними ресурсами мікросервісу, нам все одно доведеться або розгорнути ці ресурси на одній платформі JTA, або вирішити нетривіальне завдання написання цього моста самостійно.

4. Потенційна послідовність та компенсація

By far, one of the most feasible models of handling consistency across microservices is eventual consistency.

This model doesn't enforce distributed ACID transactions across microservices. Instead, it proposes to use some mechanisms of ensuring that the system would be eventually consistent at some point in the future.

4.1. A Case for Eventual Consistency

For example, suppose we need to solve the following task:

  • register a user profile
  • do some automated background check that the user can actually access the system

The second task is to ensure, for example, that this user wasn't banned from our servers for some reason.

But it could take time, and we'd like to extract it to a separate microservice. It wouldn't be reasonable to keep the user waiting for so long just to know that she was registered successfully.

One way to solve it would be with a message-driven approach including compensation. Let's consider the following architecture:

  • the user microservice tasked with registering a user profile
  • the validation microservice tasked with doing a background check
  • the messaging platform that supports persistent queues

The messaging platform could ensure that the messages sent by the microservices are persisted. Then they would be delivered at a later time if the receiver weren't currently available

4.2. Happy Scenario

In this architecture, a happy scenario would be:

  • the user microservice registers a user, saving information about her in its local database
  • the user microservice marks this user with a flag. It could signify that this user hasn't yet been validated and doesn't have access to full system functionality
  • a confirmation of registration is sent to the user with a warning that not all functionality of the system is accessible right away
  • the user microservice sends a message to the validation microservice to do the background check of a user
  • the validation microservice runs the background check and sends a message to the user microservice with the results of the check
    • if the results are positive, the user microservice unblocks the user
    • if the results are negative, the user microservice deletes the user account

After we've gone through all these steps, the system should be in a consistent state. However, for some period of time, the user entity appeared to be in an incomplete state.

The last step, when the user microservice removes the invalid account, is a compensation phase.

4.3. Failure Scenarios

Now let's consider some failure scenarios:

  • if the validation microservice is not accessible, then the messaging platform with its persistent queue functionality ensures that the validation microservice would receive this message at some later time
  • suppose the messaging platform fails, then the user microservice tries to send the message again at some later time, for example, by scheduled batch-processing of all users that were not yet validated
  • if the validation microservice receives the message, validates the user but can't send the answer back due to the messaging platform failure, the validation microservice also retries sending the message at some later time
  • if one of the messages got lost, or some other failure happened, the user microservice finds all non-validated users by scheduled batch-processing and sends requests for validation again

Навіть якщо деякі повідомлення видавалися кілька разів, це не вплине на узгодженість даних у базах даних мікросервісів.

Ретельно розглянувши всі можливі сценарії відмов, ми можемо переконатися, що наша система задовольнятиме умови можливого узгодження. У той же час нам не потрібно мати справу з дорого розподіленими транзакціями.

Але ми повинні усвідомлювати, що забезпечення послідовності є складним завданням. Він не має єдиного рішення для всіх випадків.

5. Висновок

У цій статті ми обговорили деякі механізми реалізації транзакцій між мікросервісами.

І ми також вивчили деякі альтернативи виконанню цього стилю транзакцій.