Распределенные транзакции
Что это такое?
Распределенные транзакции - это транзакции, в которых участвует несколько баз данных или програмных компонентах. Каждая из них может дать отказ транзакции в какой-то момент и тогда вся транзакция, на всех базах данных или програмных компонентах отменяется. Безусловно, издержки на реализацию подобного механизма растут кратно. Но это компенсируется теми потерями, которые может понести бизнес.
Данный инструмент востребован при высоких нагрузках и требованиях к высокой консистентности данных.
2PC
Протокол 2PC гарантирует, что изменения будут применены либо для всех участвующих компонентов, либо ни для одного. Правила такие же как и для классической ACID транзакции, но уже для многоступенчатых распределенных систем.
В 2PC операцию вовлечены:
- Transaction Coordinator - управляющий транзакцией, который инициирует транзакцию, коммуницирует с другими участниками и решает будет ли транзакция применена или отклонена;
- Participant - участники любого вида, которые выполняют некоторую работу в рамках транзакции. Может быть любым програмным компонентом - базой данных, микросервисом, еще одной подсистемой распределенных транзакций - любым компонентом, поддерживающим протокол 2PC.
Некоторые библиотеки реализуют данный механизм под другим маркетинговым названием. Например, Try-Confirm-Cancel, TCC.
Транзакция в 2PC
Состоит из 2 фаз:
Preparation Phase
- Transaction Coordinator создает транзакцию и опрашивает участников о возможности участия в ней;
- Каждый Participant выполняет локальные проверки (зависит от логики) и подтверждает или отклоняет свое участие в 2PC;
- Participant отправляет ответ Transaction Coordinator с определением о готовкности участия:
YesилиNo.
Commit Phase
- Если все прислали согласие на транзакцию (
Yes), то отправляется требование подтвердитьCommitтранзакцию всем Participant; - Если хотябы один Participant отказался от участия (
No), то всем Participant отправляется требованиеAbortтранзакции.
Если что-то пойдет не так
Что будет произойдет ошибка на стороне Transaction Coordinator?
- До Preparation Phase - никакого ущерба, транзакция даже не начнется;
- После Preparation Phase - подтверждения транзакций со стороны Participant не будет, но последуют издержки на создание, удержание и откат транзакций у каждого Participant (в связи с таймаутом подтверждения);
- После Preparation Phase, но до рассылки Commit Phase - аналогично предыдущему пункту.
Здесь важно отметить, что для каждого Participant необходимо предусмотреть откат транзакции. Так же необходимо использовать Write-Ahead Logging (WAL) для возможности восстановления транзакций.
Что будет произойдет ошибка на стороне Participant?
- До Preparation Phase - Transaction Coordinator отменит транзакцию;
- После Preparation Phase, но до ответа - Transaction Coordinator отменит транзакцию по истечении таймаута;
- После Preparation Phase и ответа
Yes, но до фактического подтверждения - транзакция “повиснет” у Participant и восстановление возможно только после восстановления работоспособности.
Самая большая угроза для 2PC состоит в подтверждении транзакции лишь частью Participant - это приводит к нарушению целостности. Еще одной угрозой является зависание транзакции в качестве незавершенной на длительное время в связи с накладывающимися на Participant блокировками.
В целом, 2PC имеет большое количество недостатков:
(1) ожидание пока Transaction Coordinator соберет все ответы;
(2) невозможно отменить транзакцию если Participant согласился в ней участвовать (только по таймауту);
(3) если Transaction Coordinator будет сломан, каждый Participant на длительное время будет в состоянии ожидания (которое может никогда не наступить);
(4) WAL сложный и дорогой инструмент;
(5) большая цепочка необходимых обменов сообщениями увеличивают потребляемые ресурсы;
(6) блокировки базы данных слишком дорогие операции;
(7) 2PC невозможен для систем с высокой доступностью (high-availability);
Возможные решения
- Использование альтернативных инструментов (например, 3PC, Paxos, Raft);
- Принятие рисков и реализация паттерна Optimistic Concurrency Control;
- Применение Saga Pattern.
3PC
Аналог 2PC, но добавляет еще 1 фазу Pre-Commit Phase, которая призвана ограничить время блокировки. Благодаря наличию списка изменений из Pre-Commit Phase, сервис не блокируется полностью. По истечении определенного таймаута, серсвис самостоятельно может принять решение и выполнить фазу Commit - это создает определенные риски, но хотя бы не держит сервис в подвисшем состоянии.
Consensus протоколы
Решают задачу консистентности данных посредством консенсуса. Благодаря консенсусу, атомарные узлы (nodes) действуют в отношении оперируемых данных как единое целое, (в той или иной степени) гарантируя целостность данных.
В случае если присутствует некороторое количество экземпляров сервиса, каждый из них будет иметь собственную выделенную память (RAM). И организовать целостность данных в рамках этого конкретного экземпляра можно стандартными средствами любого языка программирования. Но что если экземпляров несколько? У каждого из них будет собственная память, а значит и собственные данные, которые не получится синхронизировать.
Например, предположим, что в БД необходимо хранить какой-то счетчик (число), который должен быть уникальным. Гарантировать уникальность для нескольких экземпляров невозможно - все они могут одновременно хранить одинаковые значения.
В этом случае на помощь приходят Consensus протоколы, которые синхронизируют данные между известными экземплярами, создавая распределенное псевадо-единое хранилище данных. В связи с большими издержками на хранение и синхронизацию данных, использовать Consensus протоколы следует аккуратно, только к теми данными, которые действительно необходимо хранить и синхронизировать распределенно.
Paxos
Данный Consensus протокол, считается крайне надежным, но сложным в понимании и реализации.
Raft
Консистентность достигается путем выделения явного лидера всей группы экземпляров, который согласует записи изменений (log) с большинством участников группы. После получения согласования, лидер подтверждает измнения как у себя, так и остальных участников группы (высылая им соответсвующие RPC). В результате, данные у всех участников группы должны быть синхронизированы. В теории. На практике, это не всегда так. Принцип работы показан здесь. Там же представлены имплементации протокола под различные языки.
Имплементации:
Optimistic Concurrency Control, OCC
Предполагает, что конфликты и ошибки на второй стадии случаются редко, по-этому риски принимаются в угоду скорости. Подходит для высококонкурентных систем, где целостность данных но носит критический характер.
Второй тип контроля Pessimistic Concurrency Control похож на 2PC, по-этому не будет выделяться отдельно.
Saga Pattern
Более детально рассматривается в разделе посвященной архитектурам. Принцип заключается в отказе от длительных транзакций и дробление их на мелкие части, но предусматривающие компенсаторные действия (compensation action), которые активизируются только в случае отказов. В целом, может привести к нарушению целостности данных, но поскольку compensation action должны возвращать данные в состояние, которое у них было до выполнения действия.
Имплементации:
Что же выбрать?
- Для гарантированной целостности - 2PC или Consensus протоколы (Paxos, Raft);
- Для высокой доступности - OCC, Consensus протокол Raft или Saga Pattern;
- Для снижения стоимости блокировок - 3PC.