Распределённые системы
Two-Phase Commit (2PC)
Цели урока
- Понимать проблему атомарности в распределённых транзакциях
- Знать две фазы 2PC и роли coordinator / participants
- Объяснять blocking problem и почему coordinator - SPOF
- Различать сценарии применения 2PC vs Saga vs Outbox pattern
Предварительные знания
- Понимание ACID транзакций и WAL (write-ahead log)
- Знакомство с distributed systems failures (crash, omission, timing)
- Базовое понимание CAP-теоремы
Банковский перевод между двумя банками: деньги списаны, сеть оборвалась, зачисления не было. Без 2PC это не баг - это архитектурная гарантия потери данных.
- **Google Spanner** - 2PC + Paxos: единственная БД с глобальной strong consistency для межшардовых транзакций
- **Kafka Transactions (2017)** - exactly-once semantics через вариант 2PC с replicated TransactionCoordinator
- **PostgreSQL XA** - PREPARE TRANSACTION с 2005 года, используется в enterprise distributed transactions
- **Amazon DynamoDB Transactions (2018)** - 2PC под капотом для multi-item ACID транзакций
- **Stripe** - идемпотентные ключи как защита от проблем частичного выполнения без полного 2PC
Джим Грей и рождение distributed transactions
Джим Грей опубликовал описание Two-Phase Commit в 1978 году как часть работы над System R в IBM. В 1998 году получил Turing Award - высшую награду в CS - за работы по управлению транзакциями и базами данных. В 2007 году Грей пропал в море во время одиночного плавания. В его честь назван приз Jim Gray eScience Award для исследователей в области данных.
Проблема атомарности в распределённых системах
**2007 год. TJX Companies - крупнейшая в истории утечка платёжных данных. 94 миллиона карт. Один из технических корней - несогласованность между системами при обработке транзакций.** Это и есть проблема атомарности: деньги списаны с одного счёта, но из-за сбоя сети не зачислены на другой. Деньги исчезли в нигде.
**Атомарность** - свойство транзакции, при котором она либо выполняется полностью, либо не выполняется вообще. На одной машине это гарантирует СУБД через WAL. Но когда данные на двух разных серверах - кто гарантирует "всё или ничего"?
Классический пример: банковский перевод между двумя банками, каждый со своей БД. Три варианта сбоя: 1. деньги списаны, сеть упала до зачисления - минус баланс у отправителя, деньги потеряны; (2) ошибка при зачислении, но списание уже прошло - аналогично; (3) зачислено, но не списано - деньги созданы из воздуха. Все три варианта катастрофичны.
| Сценарий | Состояние A | Состояние B | Результат |
|---|---|---|---|
| Всё ок | Списано | Зачислено | Атомарно - правильно |
| Сбой после списания | Списано | Не зачислено | Деньги потеряны |
| Сбой до списания | Не списано | Зачислено | Деньги созданы |
| Оба недоступны | Неизвестно | Неизвестно | Неопределённость |
Проблема появляется не только в банкинге. Любые распределённые операции: запись в БД + публикация события в Kafka, обновление нескольких шардов, синхронизация между микросервисами. Везде нужен координатор, который либо подтверждает все изменения, либо откатывает все.
Достаточно использовать транзакции в каждой БД отдельно - это гарантирует атомарность всей операции
Локальные транзакции гарантируют атомарность только внутри одной БД. Для атомарности между несколькими системами нужен распределённый протокол типа 2PC
Каждая БД знает только о своей части операции. Даже если обе локальные транзакции атомарны, между их завершением может произойти сбой. Нужен координатор, который знает о состоянии всех участников.
Банковский перевод: деньги списаны с счёта A, но сервер банка B упал до зачисления. Что произошло с точки зрения атомарности?
Two-Phase Commit: две фазы
**2PC придуман Джимом Греем в 1978 году - тем самым, кто в 1998 получил Turing Award за работы по транзакциям и базам данных.** Идея элегантна: разделить фиксацию на два шага - сначала спросить всех "готовы?", и только если все ответили да - зафиксировать. Любой отказ на любом шаге - откат у всех.
Фаза 1: Prepare (голосование)
Coordinator рассылает PREPARE всем участникам. Каждый participant: проверяет, может ли выполнить операцию; записывает намерение в WAL (write-ahead log); берёт необходимые локи. Затем отвечает YES или NO. Если хоть один ответил NO - coordinator немедленно рассылает ABORT всем, включая тех кто ответил YES.
Фаза 2: Commit или Abort
Если все ответили YES - coordinator записывает решение COMMIT в свой WAL, затем рассылает COMMIT всем. Каждый participant фиксирует изменения и освобождает локи. Ключевой момент: после того как coordinator записал решение в лог, транзакция считается зафиксированной независимо от того, дошли ли сообщения до участников.
**State machine участника:** INIT -> WORKING (получил PREPARE) -> PREPARED (ответил YES, ждёт решения) -> COMMITTED или ABORTED. Состояние PREPARED - ключевое: participant уже взял локи и не может самостоятельно принять решение. Он заблокирован до получения ответа от coordinator.
Coordinator отправил PREPARE трём участникам: A ответил YES, B ответил YES, C ответил NO. Что делает coordinator?
Проблемы 2PC: blocking и SPOF
**2PC - блокирующий протокол.** Это его главная уязвимость. В промежутке между фазами participants держат локи и не могут принять решение самостоятельно. Падение coordinator в этот момент - deadlock на неопределённое время.
| Проблема | Что происходит | Последствие |
|---|---|---|
| Coordinator падает после PREPARE | Participants в состоянии PREPARED, держат локи | Deadlock до восстановления coordinator |
| Coordinator падает после COMMIT в лог | Часть participants получила COMMIT, часть нет | Неопределённое состояние без coordinator |
| Network partition во время фазы 2 | COMMIT дошёл не до всех | Участники в разных состояниях |
| Медленный participant | Все ждут самого медленного | Latency = max(all participants) |
Сценарий блокировки: t1 - coordinator отправляет PREPARE всем. t2 - все отвечают YES. t3 - coordinator записывает COMMIT в WAL. t4 - coordinator падает до отправки COMMIT. Результат: participants заблокированы в PREPARED бесконечно. Они держат локи на данные, не знают ни commit ни abort, и не могут принять решение без coordinator.
**WAL для recovery:** coordinator и participants пишут каждый шаг в лог до выполнения. При восстановлении: если нет PREPARED в логе - можно abort; если есть PREPARED без COMMITTED - спросить coordinator; если есть COMMITTED - применить изменения. WAL делает протокол восстанавливаемым, но не устраняет blocking.
**Coordinator как SPOF:** стандартный 2PC имеет единственную точку отказа - coordinator. Решение: реплицировать coordinator через Paxos или Raft. Так устроен Google Spanner: 2PC для атомарности + Paxos для fault tolerance coordinator.
Таймаут у participants решает проблему - они сделают abort и всё будет хорошо
Таймаут не решает проблему: coordinator мог записать COMMIT в WAL до падения. Если participant сделает abort, а coordinator после восстановления попробует commit - данные будут неконсистентны
Главная сложность 2PC: participant в состоянии PREPARED не знает, было ли принято решение coordinator до его падения. Любое самостоятельное решение participant по таймауту может нарушить атомарность.
Participant получил PREPARE, ответил YES, и ждёт ответа от coordinator. Coordinator упал. Что происходит с participant?
Альтернативы и применение 2PC
**2PC используется там, где действительно нужна строгая атомарность и есть возможность блокировки.** PostgreSQL поддерживает PREPARE TRANSACTION для XA-протокола с 2005 года. Google Spanner комбинирует 2PC с Paxos: 2PC даёт атомарность между шардами, Paxos реплицирует coordinator для fault tolerance. Kafka Transactions (с версии 0.11) используют вариант 2PC для exactly-once семантики.
| Система | Применение 2PC | Дополнение |
|---|---|---|
| PostgreSQL | PREPARE TRANSACTION для XA | PREPARE / COMMIT PREPARED / ROLLBACK PREPARED |
| MySQL + XA | Distributed transactions | XA START / XA END / XA PREPARE / XA COMMIT |
| Kafka Transactions | Exactly-once semantics | TransactionCoordinator как replicated log |
| Google Spanner | Межшардовые транзакции | 2PC + Paxos для fault-tolerant coordinator |
| Oracle RAC | Координация узлов кластера | DLM (Distributed Lock Manager) поверх 2PC |
Когда 2PC не подходит
Микросервисы - классический случай, где 2PC плохо работает. Проблемы: tight coupling (все сервисы должны поддерживать протокол), blocking (медленный сервис блокирует всех), coordinator становится bottleneck при масштабировании. Для микросервисов лучше Saga pattern - цепочка локальных транзакций с compensating actions при ошибке.
- 2PC — Строгая атомарность, 2 round-trips, blocking при падении coordinator. Подходит для: банковские транзакции между шардами, XA distributed transactions, системы где нельзя допустить частичных состояний.
- Saga — Eventually consistent, compensating transactions, non-blocking. Подходит для: микросервисы, долгие транзакции (заказ + доставка + оплата), системы с высокими требованиями к availability.
- 2PC + Paxos — Строгая атомарность + fault-tolerant coordinator. Подходит для: Google Spanner, распределённые БД enterprise-класса, когда нужна консистентность без SPOF.
**Outbox pattern** - практическая альтернатива 2PC для микросервисов: запись в БД и событие в Kafka атомарно через единую таблицу outbox в той же БД. Отдельный процесс читает outbox и публикует события. Дает at-least-once без distributed transaction.
Архитектор выбирает подход для e-commerce: списание средств + уменьшение склада + создание заказа в трёх разных микросервисах. Что лучше?
Вопросы для размышления
- Google Spanner использует 2PC + Paxos: 2PC для атомарности между шардами, Paxos для репликации coordinator. Какую проблему 2PC решает Paxos и почему нельзя обойтись только Paxos без 2PC?
Связанные уроки
- dist-07-transactions — 2PC implements atomic commit for distributed transactions
- ds-03-consensus — 2PC is a restricted form of consensus
- dist-08-paxos — Paxos generalises 2PC to tolerate coordinator failure
- dist-12-consistency — 2PC provides atomic durability underpinning strong consistency
- db-03-acid