Саги и распределенные транзакции
Сага — это долговременная бизнес-транзакция, разбитая на последовательность локальных шагов в разных сервисах/хранилищах. Каждый шаг имеет компенсирующее действие, которое откатывает эффект шага при частичном провале. В отличие от 2PC/3PC, саги не удерживают глобальные блокировки и подходят для микросервисов, мульти-регионов и высоких нагрузок, где допустима eventual consistency.
1) Когда выбирать саги (а когда — нет)
Подходит:- Длительные/многошаговые бизнес-процессы (заказ → оплата → резерв → доставка).
- Разные домены и хранилища, где нет общей транзакции.
- Нужна высокая доступность и горизонтальное масштабирование.
- Твердая ACID-атомарность критична (например, перенос больших сумм в пределах одного реестра).
- Нет четкой компенсируемости (нельзя «раз-зарезервировать» или отменить эффект).
- Юридические/регуляторные ограничения требуют строгой изоляции и «моментального» инварианта.
2) Модели саг
1. Оркестрация (Saga Orchestrator): центральный координатор управляет шагами и компенсациями.
Плюсы: явный поток, контроль ошибок, упрощенная телеметрия.
Минусы: точка централизации, риск «толстого» координатора.
2. Хореография (Choreography): нет центра — шаги инициируются событиями («сервис А сделал X → сервис Б реагирует»).
Плюсы: слабая связность, простое масштабирование.
Минусы: сложнее отслеживать/дебажить поток, риск «разрастания» правил.
3. TCC (Try-Confirm/Cancel): каждый шаг — «резервирование» (Try), затем подтверждение (Confirm) или отмена (Cancel).
Плюсы: ближе к псевдо-двухфазному протоколу, управляемые ресурсы.
Минусы: дороже в реализации интерфейсов; требует таймаутов держателей «Try».
3) Проектирование шага и компенсации
Инварианты: четко сформулируйте, что должно быть истинно «до/после» шага (например, «остаток ≥ 0»).
Компенсация ≠ обратная транзакция: это логическое действие, отменяющее бизнес-эффект (refund, release, restore).
Идемпотентность: и шаг, и компенсатор должны безопасно повторяться (по `operation_id`).
Таймауты: каждый шаг имеет deadline; просрочка инициирует компенсации.
Невозвратные эффекты: фиксируйте их отдельно (уведомления, e-mail) и допускайте «best effort».
4) Согласованность и порядок
Eventual consistency: пользователи могут видеть временные расхождения; UX — с «ожиданием»/спиннерами/статусами.
Порядок по ключу: коммутационные шаги группируйте по бизнес-ключу (order_id), чтобы упорядочивать события.
Дедупликация: храните журнал обработок (`operation_id` → статус) с TTL.
5) Транспорт и надежность
Outbox pattern: запись события в локальную таблицу «outbox» внутри той же транзакции, а затем асинхронная публикация в шину.
Inbox/Idempotency store: на стороне потребителя — журнал уже обработанных сообщений.
Exactly-once эффективно: «outbox + idempotent consumer» дает практическое «ровно один раз».
DLQ: для «ядовитых» сообщений с богатой мета-информацией и безопасным редрайвом.
6) Политики ошибок, ретраи, backoff
Повторяем только идемпотентные шаги; операции записи — с `Idempotency-Key`.
Экспоненциальный backoff + джиттер; ограничение попыток и суммарного дедлайна саги.
При системной деградации — Circuit Breaker и graceful degradation (например, отменить второстепенную фичь-часть саги).
Бизнес-конфликты (`409`) — повтор после согласования или компенсировать и завершить.
7) Оркестратор: обязанности и структура
Функции:- Отслеживание состояния саги: `PENDING → RUNNING → COMPENSATING → DONE/FAILED`.
- Планирование шагов, дедлайны, таймауты, ретраи.
- Роутинг событий и запуск компенсаций.
- Идемпотентность операций координатора (журнал команд).
- Наблюдаемость: корреляция `saga_id` в логах/трейсах/метриках.
- Таблицы `saga`, `saga_step`, `commands`, `outbox`.
- Индексы по `saga_id`, `business_key`, `status`, `next_run_at`.
8) Хореография: правила и защита от «снежного кома»
Контракты событий: схемы и версионирование (Avro/Proto/JSON Schema).
Четкая семантика: «событие факта» vs «команда».
Останов цепочки: сервис, обнаружив несоответствие, публикует `Failed`/`Compensate` событие.
Сигнализация и алерты на «бесконечные петли».
9) TCC: практические детали
Try: резерв ресурса с TTL.
Confirm: фиксация, освобождение временных блокировок.
Cancel: откат резерва (без побочных эффектов).
Гарbage collection: автоматический отмен Try после TTL (идемпотентный Cancel).
Идемпотентные Confirm/Cancel: повтор безопасен.
10) Пример (словесная схема) — «Заказ с оплатой и доставкой»
1. CreateOrder (локально) → outbox: `OrderCreated`.
2. PaymentService: резерв `Try` (TCC); при успехе → `PaymentReserved`, при отказе → `PaymentFailed`.
3. InventoryService: резерв товара; при недостатке → `InventoryFailed`.
4. ShippingService: создание слота доставки (отменяемое).
5. Если любой шаг `Failed` → оркестратор запускает компенсации в обратном порядке: `CancelShipping` → `ReleaseInventory` → `PaymentCancel`.
6. Если все ок → `PaymentConfirm` → `OrderConfirmed`.
11) Псевдокод оркестратора
pseudo startSaga(saga_id, order_id):
steps = [ReservePayment, ReserveInventory, BookShipment, ConfirmPayment]
for step in steps:
res = execWithRetry(step, order_id)
if!res.ok:
compensateInReverse(steps_done(order_id))
return FAIL return OK
execWithRetry(step, key):
for attempt in 1..MAX:
try:
return step.run(key) # идемпотентно catch RetryableError:
sleep(backoff(attempt))
catch NonRetryableError:
return FAIL return FAIL
compensateInReverse(done_steps):
for step in reverse(done_steps):
step.compensate() # идемпотентно
12) Наблюдаемость и операционные SLO
Трейсинг: единый `saga_id`, аннотации `step`, `attempt`, `decision` (run/compensate/skip).
Метрики:- Успех/ошибка саг (%), средняя длительность, p95/p99.
- Доля компенсированных саг, топ причин компенсаций.
- Очереди/outbox лаги, ретраи по шагам.
- Логи/аудит: решения оркестратора, идентификаторы ресурсов, бизнес-ключи.
13) Тестирование и хаос
Инъекция ошибок в каждый шаг: таймауты, `5xx`, бизнес-конфликты.
Out-of-order события, дубликаты, пропуски (drop).
Долгие хвосты латентности → проверка дедлайнов и компенсаций.
Массовые саги → проверка WFQ/DRR и caps в очередях, отсутствие «head-of-line blocking».
Редрайв из DLQ по шагам и по целой саге.
14) Мульти-тенантность, регионы, соответствие
Теги `tenant_id/plan/region` в событиях и хранилищах саг.
Residency: данные/события не покидают регион; кросс-региональные саги проектируйте как федерации локальных саг + агрегирующие события.
Приоритизация: VIP-саги имеют больший квотный вес; изоляция воркеров per tenant.
15) Чек-лист перед продом
- У каждого шага есть четкий компенсатор, оба — идемпотентны.
- Выбран шаблон: оркестрация/хореография/TCC; описаны границы ответственности.
- Outbox/Inbox внедрены, дедупликация по `operation_id`.
- Политики ретраев: backoff с джиттером, лимиты попыток и общий дедлайн саги.
- Контракты событий версионированы, есть валидация схемы.
- DLQ и безопасный редрайв настроены.
- Телеметрия: метрики, трейсинг, корреляция `saga_id`.
- Операционные playbooks: ручной cancel/force-confirm, расшивка «зависших» саг.
- Тесты хаоса и нагрузки проходят, SLO/бюджет ошибок определены.
16) Типичные ошибки
Нет компенсатора или он «нечистый» (имеет побочные эффекты).
Отсутствует идемпотентность/дедуп — дубли и «качели» состояний.
«Сага в саге» без явных границ — циклы и взаимные блокировки.
Нет дедлайнов → «вечные» саги и утечки ресурсов.
Оркестратор хранит состояние «в памяти» без устойчивого стора.
Хореография без центра телеметрии → «невидимые» сбои.
Непрозрачный UX: пользователи не видят промежуточных статусов.
17) Быстрые рецепты
Классика SaaS: оркестрация + outbox/inbox, экспоненциальный backoff, DLQ, статусы саги в UI.
Сильные инварианты на ресурс: TCC с TTL резерва и GC Cancel.
Высокий объем/нагрузка: хореография событий + строгая идемпотентность и метрики по ключу.
Мульти-регион: локальные саги + финальные агрегаты; избегать глобальных блокировок.
Заключение
Саги — это способ получить предсказуемую согласованность в распределенных системах без глобальных блокировок. Четкие компенсаторы, идемпотентность, надежная доставка (outbox/inbox), дисциплина таймаутов и ретраев, плюс телеметрия и плейбуки — ключ к тому, чтобы сложные бизнес-процессы оставались устойчивыми и читаемыми при росте нагрузки, числа сервисов и географий.