Circuit breaker pattern в Node.js: opossum, fallback strategies, monitoring
Вы разбили монолит на микросервисы, каждый со своей базой данных. Теперь простой заказ превращается в проблему: списать деньги, зарезервировать товар, создать доставку — три сервиса, три базы. Если третий шаг упадёт, как откатить первые два? Распределённые транзакции в стиле 2PC блокируют ресурсы, медленны и ломаются при сетевых сбоях. Saga pattern — это практичная альтернатива.
Проблема: ACID-транзакции невозможны в микросервисах
В монолите вы оборачиваете всё в BEGIN TRANSACTION — либо всё фиксируется, либо откатывается атомарно. В микросервисах каждый сервис владеет своей базой. Попытки использовать 2PC (two-phase commit) наталкиваются на реальность: фаза «prepare» держит блокировки, координатор — единая точка отказа, современные брокеры (Kafka, RabbitMQ) не поддерживают XA-протокол. CAP-теорема: при разделении сети выбираете либо доступность, либо консистентность. Большинство систем выбирают доступность и eventual consistency.
Saga pattern: компенсирующие транзакции
Saga — это последовательность локальных транзакций T1, T2, ..., Tn. Каждая Ti фиксируется в своей базе. Если Ti падает, выполняются компенсирующие транзакции C(i-1), ..., C1 в обратном порядке.
Ключевое: компенсация — не откат базы данных, а новая бизнес-операция. Если вы списали 1000₽, компенсация — это возврат 1000₽ (новая запись), а не удаление исходной.
Пример saga для заказа:
T1: CreateOrder → T2: ChargePayment → T3: ReserveInventory → T4: CreateShipment
Если T3 падает: C2: RefundPayment → C1: CancelOrder
Консистентность eventual: между шагами система в промежуточном состоянии. Клиент видит заказ «processing» несколько секунд.
Choreography: event-driven подход
Нет центрального координатора. Каждый сервис реагирует на события и публикует свои:
// Order Service
orderBus.on('ORDER_CREATED', async (order) => {
try {
await paymentClient.charge(order.userId, order.total);
eventBus.emit('PAYMENT_COMPLETED', { orderId: order.id });
} catch (err) {
eventBus.emit('PAYMENT_FAILED', { orderId: order.id });
}
});
// Inventory Service
eventBus.on('PAYMENT_COMPLETED', async ({ orderId }) => {
try {
await inventory.reserve(orderId);
eventBus.emit('INVENTORY_RESERVED', { orderId });
} catch (err) {
eventBus.emit('INVENTORY_FAILED', { orderId });
}
});
// Compensation
eventBus.on('INVENTORY_FAILED', async ({ orderId }) => {
await paymentClient.refund(orderId);
await orderRepo.cancel(orderId);
});
Sequence diagram:
Client → Order → Payment → Inventory
↓ FAIL
Order ← Payment (refund)
Client ← Order (409)
Плюсы: нет единой точки отказа, сервисы слабо связаны, легко добавить подписчика.
Минусы: workflow неявный, отладка требует distributed tracing, риск event storm, сложно гарантировать порядок компенсаций.
Правило: choreography для 2–3 шагов с линейным флоу.
Orchestration: центральный координатор
Orchestrator знает весь workflow, вызывает сервисы по очереди и запускает компенсации при сбоях:
class OrderSagaOrchestrator {
constructor(redis, orderClient, paymentClient, inventoryClient) {
this.redis = redis;
this.steps = [
{ name: 'createOrder',
execute: (ctx) => orderClient.create(ctx.orderData),
compensate: (ctx) => orderClient.cancel(ctx.orderId) },
{ name: 'chargePayment',
execute: (ctx) => paymentClient.charge(ctx.userId, ctx.total),
compensate: (ctx) => paymentClient.refund(ctx.paymentId) },
{ name: 'reserveInventory',
execute: (ctx) => inventoryClient.reserve(ctx.orderId),
compensate: (ctx) => inventoryClient.release(ctx.reservationId) }
];
}
async executeSaga(sagaId, orderData) {
const context = { sagaId, orderData, completedSteps: [] };
await this.redis.set(`saga:${sagaId}`, JSON.stringify(context));
try {
for (const step of this.steps) {
const result = await step.execute(context);
Object.assign(context, result);
context.completedSteps.push(step.name);
await this.redis.set(`saga:${sagaId}`, JSON.stringify(context));
}
return { status: 'COMPLETED', orderId: context.orderId };
} catch (err) {
await this.compensate(context);
return { status: 'COMPENSATED', error: err.message };
}
}
async compensate(context) {
const completed = [...context.completedSteps].reverse();
for (const stepName of completed) {
const step = this.steps.find(s => s.name === stepName);
if (step.compensate) {
try {
await step.compensate(context);
} catch (err) {
// Критично: компенсация упала — DLQ для ручной обработки
await this.redis.lpush('saga:failed-compensations',
JSON.stringify({ sagaId: context.sagaId, step: stepName }));
}
}
}
}
}
Плюсы: весь workflow в одном месте, явный контроль компенсаций, retry-логика, таймауты, состояние персистится.
Минусы: потенциальное узкое горло, сервисы связаны с orchestrator.
Реализация на Node.js: пример с заказом
const express = require('express');
const Redis = require('ioredis');
const { OrderSagaOrchestrator } = require('./saga');
const app = express();
const redis = new Redis();
const saga = new OrderSagaOrchestrator(redis, orderClient, paymentClient, inventoryClient);
app.post('/orders', async (req, res) => {
const sagaId = `saga-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const result = await saga.executeSaga(sagaId, req.body);
if (result.status === 'COMPLETED') {
res.status(201).json({ orderId: result.orderId, sagaId });
} else {
res.status(409).json({ error: result.error, sagaId });
}
});
app.get('/admin/sagas', async (req, res) => {
const keys = await redis.keys('saga:*');
const sagas = await Promise.all(keys.map(k => redis.get(k)));
res.json(sagas.map(JSON.parse));
});
Критичные паттерны:
- Idempotency: используйте
sagaIdкак idempotency key в downstream-сервисах - Transactional Outbox: событие и изменение БД фиксируются атомарно
- Dead Letter Queue: упавшие компенсации в DLQ для ручной обработки
- Мониторинг: алертите, если saga висит >30 секунд
Choreography vs Orchestration: когда что использовать
| Критерий | Choreography | Orchestration |
|---|---|---|
| Шаги | 2–3 | 4+ |
| Флоу | Линейный | Ветвления, условия |
| Ownership | Разные команды | Одна команда |
| Отладка | Distributed tracing | Централизованные логи |
| Связанность | Слабая (события) | Сильная (команды) |
Для сложных workflow используйте готовые движки: Temporal (Go/TypeScript/Python), AWS Step Functions, Camunda. Они персистят состояние, автоматически ретраят шаги и предоставляют UI для мониторинга.
Вывод
Saga pattern — это признание реальности распределённых систем. Отказы нормальны, сеть ненадёжна, консистентность eventual. Вместо иллюзии ACID вы получаете явные компенсации, персистентное состояние и возможность восстановления.
Начинайте с choreography для простых флоу. Переходите на orchestration при условиях, параллельных ветках или нужде в аудируемости. Делайте каждый шаг идемпотентным. Используйте transactional outbox для надёжной публикации событий. Мониторьте длительность saga и застрявшие компенсации. Некоторые сбои требуют ручного вмешательства — проектируйте систему так, чтобы они всплывали явно.