Зображення створене за допомогою DALL·E®
Мікросервісні та подієво-орієнтовані архітектури в розподілених системах є inherently складними в інженерній практиці. Складність цих архітектур часто недооцінюється в проектах. Ефективне технічне управління не повинно ігнорувати управління складністю системи, оскільки вона має тенденцію зростати експоненційно в міру розвитку проекту. Це завдання особливо загострюється при роботі з розподіленими транзакціями, що може ще більше ускладнити ситуацію.
Виклик розподілених транзакцій
Забезпечення консистентності даних та коректне оброблення виключень у розподіленому середовищі є складним завданням. Для зменшення ризиків проекту, важливо проактивно розглядати стратегії, які уникатимуть необхідності використання розподілених транзакцій, оскільки вони можуть значно збільшити складність системи та внести додаткові ризики.
Однак, в багатьох сценаріях, особливо при налаштуваннях з багатьма регіонами та кластерами, уникнення розподілених транзакцій неможливо. У таких випадках критично важливо використовувати правильні інструменти та методології для управління складністю системи в межах контрольованого діапазону, гарантуючи, що проблеми, викликані розподіленими транзакціями, не вплинуть на стабільність та надійність системи.
Патерн Саги
Патерн Сага є широко використовуваним підходом для управління довготривалими, складними розподіленими транзакціями. Однак його реалізація, незалежно від того, чи це хореографія або оркестрація, часто призводить до значної складності та може бути важкою для виконання.
Temporal.io виділяється серед таких систем оркестрації та workflow движків, як Camunda Zeebe та Netflix Conductor, тим, що є більш дружнім до розробників завдяки своєму кодовому підходу до workflow та рідній підтримці виконання патерну Саги через Temporal-SDK Saga API.
Temporal.io значно спрощує реалізацію Саг, одночасно підвищуючи стійкість системи завдяки вбудованим функціям відмовостійкості, таким як повторні спроби та тайм-аути. Це робить Temporal.io кращим вибором для обробки Саг, оскільки воно дозволяє контролювати складність та підвищувати стійкість системи, гарантуючи, що обидві цілі будуть ефективно досягнуті.
Приклад з Java мікросервісами
Ось приклад, що демонструє, як використовувати Temporal.io для реалізації патерну Сага, використовуючи Quarkus як фреймворк для мікросервісів. ( Джерельний код можна знайти тут: https://github.com/xinhuagu/temperal-quarkus-demo )
Розглянемо простий сценарій, що ілюструє патерн Сага:
berndruecker/trip-booking-saga-java · GitHub
Припустимо, що ми вже маємо три мікросервіси: сервіс авіаперевезень, сервіс готелів та сервіс автомобілів, які беруть участь у всьому процесі бронювання. Використовуючи патерн Сага, ми оркеструємо виклики до кожного сервісу послідовно (наприклад, забронювати готель → забронювати авто → забронювати авіапереліт). Якщо будь-який етап не вдається — наприклад, бронювання авіаперельоту — то його відповідна логіка компенсації (скасувати авто → скасувати готель) буде активована, повертаючи систему в консистентний стан.
Для того, щоб використовувати Temporal для побудови цього процесу, нам потрібен ще один проект для визначення Temporal workflow та використання його як спільний JAR файл, який буде поділений між трьома згаданими сервісами. У цьому JAR файлі ми визначаємо workflow, активності та пов’язані тайм-аути, механізми повторних спроб тощо.
Нижче наведено визначення інтерфейсу для цього workflow.
Як ви можете побачити, ми надаємо різні черги для різних сервісів, що полегшує кожному сервісу отримувати завдання, орієнтовані саме на нього.
@WorkflowInterface
public interface BookingWorkflow {
final static String CAR_SERVICE_TASK_QUEUE = "carServiceTaskQueue";
final static String HOTEL_SERVICE_TASK_QUEUE = "hotelServiceTaskQueue";
final static String FLIGHT_SERVICE_TASK_QUEUE = "flightServiceTaskQueue";
@WorkflowMethod(name = "Booking")
BookingResultDTO startBooking(BookingDTO order);
}
У Temporal workflow має лише один вхідний метод для запуску самого workflow. У цьому прикладі цей метод — startBooking. Нижче наведено демонстрацію того, як він виконується в коді. Під час виконання ви побачите, як ми пов'язуємо активності з різних сервісів для формування workflow, і використовуємо вбудований API Saga від Temporal для визначення кроків компенсації.
@Override
public BookingResultDTO startBooking(BookingDTO booking) {
var workflowId = Workflow.getInfo()
.getWorkflowId();
Saga saga = new Saga(new Saga.Options.Builder()
.setParallelCompensation(false)
.build());
try {
booking = carBookingActivity.bookCar(booking);
saga.addCompensation(carBookingActivity::cancelCar, booking);
booking = hotelBookingAcitivity.bookHotel(booking);
saga.addCompensation(hotelBookingAcitivity::cancelHotel, booking);
booking = flightBookingActivity.bookFlight(booking);
return BookingResultDTO.builder().status(Status.SUCCESS)
.bookingId(booking.getId()).build();
} catch (ActivityFailure activityFailure) {
Workflow.newDetachedCancellationScope(() -> saga.compensate()).run();
var result = BookingResultDTO.builder()
.bookingId(booking.getId())
.status(Status.FAILED).build();
return result;
}
Якщо, наприклад, бронювання готелю не вдалося після успішного бронювання авто, Temporal.io автоматично викликає компенсуючу дію cancelCar для скасування бронювання авто. Дві компенсуючі дії cancelHotel та cancelCar викликаються по черзі, якщо бронювання авіаперельоту не вдалося після того, як бронювання готелю було успішно виконано.
Ця реалізація Саги гарантує, що система не залишатиметься в неконсистентному стані при часткових бронюваннях.
Налаштувавши тайм-аут і параметри повторних спроб для активностей Temporal з метою обробки можливих виключень, ось приклад:
private final RetryOptions retryOptions = RetryOptions.newBuilder()
.setInitialInterval(Duration.ofSeconds(2))
.setMaximumAttempts(4)
.setBackoffCoefficient(2)
.build();
private final ActivityOptions carBookingActivityOptions = ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(5))
.setScheduleToStartTimeout(Duration.ofSeconds(60))
.setScheduleToStartTimeout(Duration.ofSeconds(5))
.setTaskQueue(CAR_SERVICE_TASK_QUEUE)
.setRetryOptions(retryOptions)
.build();
Потрібно врахувати, як налаштування повторних спроб впливають на тайм-аути. У наведеній конфігурації ScheduleToCloseTimeout має бути достатньо великим, щоб врахувати до чотирьох повторних спроб у найгіршому випадку.
Після завершення створення JAR файлу для Workflow кожен з трьох сервісів повинен виконати наступні кроки:
- Включити цей JAR файл у свою runtime-середу.
- Надати код виконання для активностей, визначених у JAR файлі workflow.
- Коли сервіс запускається, зареєструвати себе як worker (робітника), прив’язаного до черги сервісу. Тобто:
Якщо є кілька реплік, то буде відповідна кількість робітників.
Ми використовуємо REST API сервісу бронювання авто для запуску workflow:
@Slf4j
@Path("/booking")
public class BookingResource {
@Inject
WorkflowClient workflowClient;
@POST
public String booking(BookingDTO booking) {
log.info("incoming booking: " + booking.toString());
WorkflowOptions options = WorkflowOptions.newBuilder()
.setTaskQueue(BookingWorkflow.CAR_SERVICE_TASK_QUEUE)
.setWorkflowId("Booking-" + booking.getId())
.build();
BookingWorkflow bookingWorkflow = workflowClient
.newWorkflowStub(BookingWorkflow.class, options);
var excution = WorkflowClient
.start(bookingWorkflow::startBooking, booking);
var workflowId = excution.getWorkflowId();
return "Workflow " + workflowId + " is started";
}
}
Якщо workflow виконується без проблем, ми побачимо в інтерфейсі Temporal, що три активності бронювання з різних сервісів успішно завершені.
Ми можемо викликати виключення в активності бронювання Flight Service, щоб змоделювати помилку на останньому етапі процесу бронювання, що активує патерн Saga.
На зображенні вище дві компенсуючі дії, cancelHotel та cancelCar, виконуються послідовно для відкату процесу.
Висновок
З прикладу вище ми бачимо, що порівняно з Netflix Conductor та Camunda Zeebe, Temporal.io має такі переваги при роботі з розподіленими транзакціями, особливо під час виконання патерну Saga.
Ми сподіваємося, що ця стаття дасть корисне розуміння того, чому ми рекомендуємо використовувати Temporal.io для обробки розподілених транзакцій. Зосереджуючись на зменшенні складності, Temporal.io робить управління складними розподіленими транзакціями простішим, а також легшим для експлуатації та підтримки.
Перекладено з: Managing the complexity of distributed transactions with Temporal.io