В монолите транзакция — это BEGIN ... COMMIT. Все операции либо выполнились, либо нет. Когда система распадается на несколько сервисов с разными базами, одна транзакция перестаёт покрывать весь процесс.
Классическое решение — двухфазный коммит (2PC) — работает, но плохо масштабируется: координатор можно сделать отказоустойчивым, но сам протокол остаётся блокирующим и дорогим, держит блокировки на всех участниках до завершения и плохо ложится на автономные сервисы с независимыми хранилищами.
Saga — альтернативный подход: вместо одной большой транзакции выполняется последовательность локальных транзакций. Если одна из них падает, предыдущие откатываются через компенсирующие действия. Нет глобальных блокировок, нет координатора в классическом смысле — но появляются свои компромиссы.
В статье
- Что такое saga и зачем она нужна
- Оркестрация vs хореография
- Компенсирующие действия: откат без rollback
- Лунная база: saga при распределении аварийных ресурсов
- Идемпотентность: почему без неё saga не работает
- Семантика «at least once» и дубликаты
- Контрмеры: как жить с видимостью промежуточных состояний
- Где saga не подходит
- Checklist: готовы ли вы к saga
Что такое saga и зачем она нужна
Saga — это последовательность шагов, где каждый шаг:
- Выполняет локальную транзакцию в своём сервисе.
- Публикует событие или вызывает следующий шаг.
- Имеет компенсирующее действие на случай отката.
Если шаг N упал, система выполняет компенсации для шагов N−1, N−2, … , 1 в обратном порядке.
Важно: saga не даёт ACID-изоляции. Промежуточные состояния видны другим процессам. Это фундаментальное отличие от 2PC, и с ним придётся работать — но не вслепую: видимость можно ограничить контрмерами (см. ниже).
Оркестрация vs хореография
Оркестрация
Центральный оркестратор управляет последовательностью шагов. Он знает весь flow, вызывает участников, обрабатывает ответы и запускает компенсации при ошибке.
Плюсы:
- логика процесса в одном месте;
- легко отследить текущее состояние saga;
- проще добавлять шаги и менять порядок.
Минусы:
- оркестратор становится критичным компонентом;
- участники зависят от оркестратора;
- риск превратиться в «умный координатор, глупые сервисы».
Хореография
Нет центрального управления. Каждый сервис слушает события и решает, что делать дальше. Цепочка действий возникает из взаимодействия, а не из плана.
Плюсы:
- нет single point of failure;
- сервисы максимально независимы;
- легко добавлять подписчиков.
Минусы:
- сложно понять полный flow — он «растворён» в событиях;
- трудно отлаживать и мониторить;
- циклические зависимости могут возникнуть незаметно.
Что выбрать
Оркестрация — когда процесс линейный, шагов больше трёх и нужен чёткий контроль. Хореография — когда участников мало, связи простые и важна независимость сервисов.
Компенсирующие действия: откат без rollback
Компенсация — это не ROLLBACK. Это новое действие, которое отменяет эффект предыдущего шага.
Примеры:
| Шаг | Компенсация |
|---|---|
| Списать средства со счёта | Вернуть средства на счёт |
| Зарезервировать товар на складе | Снять резерв |
| Отправить уведомление | Отправить отмену (не всегда возможно) |
Важные свойства:
- компенсация должна быть идемпотентной — повторный вызов не должен менять результат;
- не все действия можно компенсировать (отправленное письмо не отозвать);
- порядок компенсаций — обратный порядку шагов.
Лунная база: saga при распределении аварийных ресурсов
Аварийный сценарий на лунной базе: разгерметизация модуля, нужно перераспределить ресурсы.
Шаги saga:
- Логистика: зарезервировать аварийный запас кислорода.
- Энергетика: переключить энергоснабжение на аварийный режим.
- Робототехника: отправить ремонтного робота к модулю.
- Жизнеобеспечение: активировать альтернативный контур подачи воздуха.
Если шаг 3 падает (робот недоступен):
- Компенсация шага 2: вернуть энергоснабжение в штатный режим.
- Компенсация шага 1: снять аварийный резерв.
- Эскалация: уведомить оператора для ручного решения.
Обратите внимание: шаг 4 не начнётся, пока не завершён шаг 3. А компенсация для «отправить робота» может быть «отозвать задание» — если робот ещё не выехал.
Идемпотентность: почему без неё saga не работает
В распределённой системе сообщения могут доставляться повторно. Если шаг saga не идемпотентен, повторная доставка сообщения выполнит его дважды.
Идемпотентность достигается через:
- уникальный идентификатор операции — каждый шаг проверяет, не выполнялся ли он уже с этим ID;
- idempotency key в запросе;
- условные обновления —
UPDATE ... WHERE status = 'pending'.
Без идемпотентности saga превращается в генератор дублирующих операций.
Семантика «at least once» и дубликаты
Семантика доставки зависит от конкретного брокера и его конфигурации (at-most-once, at-least-once, реже — effectively-once). Для saga мы сознательно выбираем at-least-once как гарантию транспорта: сообщение может прийти повторно, но не потеряется. Именно этот выбор и делает идемпотентность обязательной — за неё, а не за «магию брокера», отвечает наш код.
Для saga это означает:
- каждый обработчик должен быть готов к повторному вызову;
- компенсации тоже должны быть идемпотентными;
- outbox pattern помогает гарантировать, что событие опубликовано ровно тогда, когда транзакция закоммичена.
Контрмеры: как жить с видимостью промежуточных состояний
Отсутствие изоляции — не приговор. Есть набор приёмов (countermeasures), которые ограничивают вред от видимости незавершённой саги:
- Semantic lock — шаг помечает запись флагом «в процессе» (
status = pending), и другие процессы знают, что значение неокончательное. Самый частый и практичный приём; - Commutative updates — операции проектируются так, что порядок применения не важен (например, прибавить/убавить, а не «установить значение»), тогда промежуточная видимость не ломает результат;
- Pessimistic view — порядок шагов выстраивается так, чтобы «опасные» с точки зрения видимости операции выполнялись последними;
- Re-read value — перед изменением шаг перечитывает запись и проверяет, что она не менялась с момента чтения (защита от перезаписи чужой саги).
Минимум, который стоит закладывать почти всегда, — semantic lock: явный признак незавершённости избавляет от целого класса гонок «прочитали половину саги как финальное состояние».
Где saga не подходит
- Нужна строгая изоляция — saga не даёт ACID isolation. Промежуточные состояния видны. Если это недопустимо, нужен 2PC или пересмотр границ сервисов.
- Много участников с длинными компенсациями — чем длиннее цепочка, тем сложнее и ненадёжнее откат.
- Действия необратимы — если компенсация невозможна (физическое действие, отправка средств на внешний счёт), saga не решает проблему полностью.
- Простой домен — если транзакция помещается в одну базу, saga — это переусложнение.
Checklist: готовы ли вы к saga
- Действительно ли процесс пересекает границы нескольких сервисов с разными хранилищами?
- Определены ли компенсирующие действия для каждого шага?
- Идемпотентны ли все шаги и компенсации?
- Выбран подход: оркестрация или хореография?
- Есть ли мониторинг состояния саг (какие висят, какие застряли)?
- Продумана ли эскалация для случаев, когда компенсация невозможна?
- Допустима ли видимость промежуточных состояний — и если нет, заложены ли контрмеры (semantic lock и др.)?
Если на пункт 1 ответ «нет» — вам не нужна saga. Если на пункты 2–3 ответ «не знаем» — вы не готовы к saga.
Документация и первоисточники
- Канон: Garcia-Molina & Salem — «Sagas» (1987, PDF), microservices.io — Saga (Chris Richardson).
- Продукт/движки: Temporal, Camunda, Axon — Saga.
- Смежное на сайте: Saga на практикеготовится, с 18 августа (компенсации, изоляция, tooling), Хореография vs оркестрация, Temporal (durable workflows), гарантии доставки и идемпотентность, Outbox/Inbox, очереди задачготовится, с 17 сентября, согласованность данных, матрица решенийготовится, с 8 августа.
Комментарии