Целостность данных в монолитных приложениях во многом определяется свойствами атомарности, непротиворечивости, изолированности и долговечности (Atomicity, Consistency, Isolation, Durability — ACID). При этом по мере усложнения приложений недостатки монолитной модели становятся все более очевидными. Эффективно устранить многие из них позволяет микросервисная архитектура. Однако она также создает и серьезные проблемы для управления транзакциями и согласованности данных между независимыми базами данных и сервисами.
Структурированное решение этой проблемы предоставляет шаблон Saga. Он предлагает системный подход к управлению транзакциями между несколькими микросервисами. При этом устраняются проблемы распределенных транзакций в соответствии с принципами архитектуры микросервисов, характеризующейся слабой связностью и возможностью независимого развертывания сервисов.
Что такое шаблон Saga?
Saga — это шаблон проектирования, используемый для управления транзакциями и обеспечивающий согласованность данных между отдельными сервисами в распределенной системе, особенно в архитектуре микросервисов. В отличие от традиционных монолитных приложений, где транзакции выполняются в единой базе данных, не нарушая их согласованность, микросервисы часто обращаются к разным базам данных, что усложняет поддержание целостности данных во всей системе с использованием стандартных транзакций ACID. Шаблон Saga решает эту проблему, разбивая транзакцию на более мелкие локальные составляющие, обрабатываемые различными сервисами.
Шаблон включает три основных компонента: локальные транзакции, компенсационные транзакции и коммуникации. Они позволяют понять особенности работы Saga.
- Локальные транзакции. Каждый шаг бизнес-процесса выполняется как локальная транзакция в соответствующем сервисе.
- Компенсационные транзакции. Если одна из локальных транзакций завершается неудачно, компенсационные транзакции запускаются в тех службах, где предыдущие шаги были успешно выполнены. Компенсирующие транзакции — это по сути операции отмены, гарантирующие возврат системы в согласованное состояние.
- Коммуникации. Сервисы взаимодействуют между собой посредством сообщений или событий. Это может происходить синхронно, но чаще асинхронно с использованием очередей сообщений или шин событий. В случае сбоя/отказа для обеспечения стабильности системы контроллер выполнения (Saga Execution Controller) запускает эти события.
Как реализовать шаблон Saga с помощью Node.js
Существует два подхода к реализации шаблона «Saga»: «Saga на основе хореографии» и «Saga на основе оркестрации».
- Saga на основе оркестрации: один оркестратор (аранжировщик) управляет всеми транзакциями и сервисами для выполнения локальных транзакций.
- Saga на основе хореографии: все сервисы, являющиеся частью распределенной транзакции, публикуют новое событие после завершения своей локальной транзакции.
В этом примере использован подход «Saga на основе хореографии» и реальный сценарий бронирования номеров в отеле с тремя микросервисами: бронирования, оплаты и уведомлений (Booking Service, Payment Service и Notification Service).
- Booking Service. Запускается процесс резервирования номера. Это первая локальная транзакция. После подтверждения оплаты отправляется сообщение для обработки платежа в Payment Service.
- Payment Service. Прием сообщения и обработка платежа. Если платеж прошел успешно, совершается локальная транзакция с информированием сервисов бронирования и уведомлений (Booking Service и Notification Service).
- Notification Service. После получения подтверждения об успешной оплате, сервис отправляет пользователю электронное письмо с подтверждением бронирования.
- Обработка ошибок. Если Payment Service обнаружит проблемы (например, отказ в проведении платежа), он возвращает в Booking Service сообщение об ошибке. После этого Booking Service выполняет компенсирующую транзакцию для отмены бронирования номера, обеспечивающую возврат системы в исходное согласованное состояние.
Необходимые условия
- Проект Node.js с установленными зависимостями (express, amqplib, nodemailer, mongoose и dotenv).
- Сервер RabbitMQ, работающий локально или удаленно.
- Сервер электронной почты или сервис для Notification Service (например, Nodemailer с SMTP или сервисом API электронной почты).
Шаг 1. Создание конечной точки API для запуска процесса бронирования
// booking-service.js
const express = require('express');
const amqp = require('amqplib');
const app = express();
app.use(express.json());
//Подключение к RabbitMQ
const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('payment_queue');
}
// Конечная точка для бронирования номера
app.post('/book', async (req, res) => {
//Сохранение результатов бронирование в базе данных и попытка резервирования номера.
booking = { /* ... */ };
// ... логика бронирования
if (bookingReservedSuccessfully) {
await publishToQueue('payment_queue', booking);
return res.status(200).json({ message: 'Booking initiated', booking });
} else {
return res.status(500).json({ message: 'Booking failed' });
}
});
//Запуск сервера и подключение к RabbitMQ
const PORT = 3000;
app.listen(PORT, async () => {
console.log(Booking Service listening on port ${PORT} );
await connectRabbitMQ();
});
В процессе нового бронирования Booking Service обрабатывает запросы HTTP POST. Сервис пытается зарезервировать номер и в случае успеха отправляет сообщение в Payment Service через очередь сообщений (Payment_queue) RabbitMQ.
Шаг 2. Создание конечной точки API для прослушивания бронирований и обработки платежей
// payment-service.js
const amqp = require('amqplib');
const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('notification_queue');
await channel.assertQueue('compensation_queue');
channel.consume('payment_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логоки оформления платежа
const paymentSuccess = true; //Замена актуальным условием успешного платежа
if (paymentSuccess) {
await channel.sendToQueue('notification_queue', Buffer.from(JSON.stringify(booking)));
} else {
await channel.sendToQueue('compensation_queue', Buffer.from(JSON.stringify(booking)));
}
channel.ack(msg);
});
}
connectRabbitMQ();
Payment Service прослушивает сообщения в очереди Payment_queue и обрабатывает платеж. Затем в зависимости от результата либо отправляет сообщение в notification_queue (если платеж прошел), либо в compensation_queue (если платеж не выполнен).
Шаг 3. Создание конечной точки API для отслеживания успешных платежей и отправки электронной почты.
// notification-service.js
const amqp = require('amqplib');
const nodemailer = require('nodemailer');
const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('notification_queue');
channel.consume('notification_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логики для отправки уведомления пользователю по электронной почте.
console.log(Sending booking confirmation for bookingId: ${booking.id} );
//Настройка транспорта nodemailer
//Отправка электронного соообщения ...
channel.ack(msg);
});
}
connectRabbitMQ();
Notification Service прослушивает сообщения из очереди notification_queue. Получив сообщение он отправляет клиенту подтверждение по электронной почте.
Шаг 4. Создание сервиса компенсации для обработки ошибок
// compensation-service.js
const amqp = require('amqplib');
const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('compensation_queue');
channel.consume('compensation_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логики для отмены бронирования
console.log(Compensating transaction: cancelling bookingId: ${booking.id} );
//Обновляем статус бронирования в базе данных на «CANCELLED» или подобное...
channel.ack(msg);
});
}
connectRabbitMQ();
Сервис компенсации прослушивает сообщения в compensation_queue. Получив сообщение, указывающее на сбой платежа, он выполняет компенсирующую транзакцию для отмены бронирования и возврата системы в согласованное состояние.
Вы успешно реализовали 3 микросервиса с помощью шаблона Saga. Каждый из них выполняет свои задачи и взаимодействует с другими сервисами через события.
Шаблон Saga и метод оркестрации
При использовании Saga на основе оркестрации (вместо Saga + хореография) необходим центральный координатор, сообщающий участвующим сервисам, какие локальные транзакции нужно выполнять.
Главные отличия шаблона Saga c методом оркестрации:
- Всем процессом управляет отдельный сервис Orchestrator.
- Каждый сервис взаимодействует с Оркестратором после завершения локальной транзакции.
- Оркестратор принимает решение о выполнении очередного шага и отправляет соответствующему сервису команды, включая любые компенсирующие транзакции, необходимые в случае отказа.
Выбор между хореографией и оркестрацией часто определяется сложностью бизнес-процесса, задаваемой степенью взаимосвязи между сервисами и необходимостью централизованного контроля над бизнес-транзакциями. Более централизованный метод хореографии требует меньше настроек, а оркестрация может обеспечить больше контроля, упрощает управление и мониторинг сложных шаблонов Saga.
Заключение
В архитектуре микросервисов шаблон Saga позволяет эффективно решать проблемы распределенных транзакций, обеспечивая согласованность данных между независимыми сервисами. Грамотное применение шаблона Saga необходимо для создания надежных и отказоустойчивых приложений с микросервисами, которые сегодня наиболее востребованы.
Читайте также:
- Что такое шаблон SAGA и какую проблему он решает в микросервисной архитектуре
- Топ-10 антипаттернов при использовании микросервисов
- Состояние гонки в Node.js: практическое руководство
Читайте нас в Telegram, VK и Дзен
Перевод статьи Chameera Dulanga: Implementing Saga Pattern in Microservices with Node.js