По умолчанию репликация в Tarantool асинхронная: транзакция коммитится локально и попадает на реплики потом, без гарантий. Если мастер ответил клиенту "успех" и тут же умер, после failover на реплику транзакция может исчезнуть. Синхронная репликация решает эту проблему: транзакция не считается закоммиченной и клиент не получает ответ, пока запись не доедет до кворума узлов. Отдельно от этого работает автоматический выбор лидера на основе алгоритма Raft - он гарантирует, что в любой момент в наборе реплик не более одного писателя.
В Tarantool это две независимые подсистемы. Можно включить синхронную репликацию без выборов (лидера назначает внешний координатор), а можно включить выборы без синхронных спейсов. Полный Raft получается, когда обе включены вместе и настроены строго: все спейсы синхронные, кворум N/2+1, synchro_timeout фактически бесконечный, fencing включён.
Механика и архитектура
Лимб (limbo) - очередь синхронных транзакций. Сердце синхронной репликации - структура limbo на лидере. Когда вы коммитите транзакцию в синхронный спейс, её строки пишутся в WAL локально, но сама транзакция не подтверждается, а попадает в лимб и ждёт. Лидер рассылает её на реплики; каждая реплика, записав строки в свой WAL, шлёт обратно ACK (подтверждение vclock). Как только число подтверждений (включая саму лидера) достигает synchro_quorum, лидер пишет в WAL служебную запись CONFIRM и только тогда отвечает клиенту "ОК". CONFIRM реплицируется дальше, и реплики делают данные видимыми у себя.
Важная деталь внутренностей: CONFIRM и ROLLBACK применяются на реплике только после записи в её WAL, а не сразу по приёму. Иначе был бы баг - подтверждённые данные становятся видны, узел рестартует до окончания записи CONFIRM, и данные снова невидимы.
Правило порядка. Все транзакции (и синхронные, и асинхронные) коммитятся в том же порядке, в каком был вызван box.commit(). Асинхронная транзакция, попавшая за синхронную, ждёт её - но синхронной не становится: дождавшись CONFIRM предыдущей, она коммитится сразу, не ожидая своего кворума. Откат идёт в обратном порядке: если синхронная транзакция упала по таймауту, она откатывает и все более новые, что ждали за ней.
Raft: термы, голоса, кворум. Жизнь набора реплик делится на термы (term) - монотонно растущие числа. Каждый узел в состоянии follower, candidate или leader. Если follower не видит лидера дольше replication.timeout * 4 (нет heartbeat), он увеличивает терм, переходит в candidate, голосует за себя и рассылает запросы голосов. Узел голосует за первого попросившего в данном терме и больше в этом терме не голосует. Кто собрал кворум голосов (тот же synchro_quorum) - становится лидером и шлёт heartbeat остальным. При split vote (никто не набрал кворум) после случайного таймаута термы снова растут и round повторяется. Узлы предпочитают голосовать за тех, у кого свежее данные (больше vclock) - чтобы не потерять то, что старый лидер успел разослать. Термы и голоса персистятся на диск.
PROMOTE / DEMOTE - смена владельца лимба. Право писать синхронные транзакции принадлежит владельцу очереди. При смене лидера новый узел пишет служебную запись PROMOTE: она забирает владение лимбом и финализирует "висящие" транзакции прежнего лидера (подтверждает то, что успело набрать кворум, откатывает остальное). box.ctl.promote() делает это вручную, box.ctl.demote() - снимает владение.
Fencing - защита от двух лидеров. В классическом Raft лидер не следит за своей связностью и считает себя лидером, пока не увидит больший терм. Tarantool добавил fencing: если у лидера осталось меньше synchro_quorum живых соединений, он сам слагает полномочия (resign), становится follower и read-only. Режим soft (по умолчанию) считает связь мёртвой после 4 * replication.timeout; strict - после 2 * replication.timeout на лидере, давая больше шансов на единственного лидера.

Синхронный коммит через кворум и выборы лидера Raft
Ключевые команды и код
Декларативная конфигурация 3.x (config.yaml) - полный Raft на трёх узлах:
Код: Выделить всё
replication:
failover: election # включить выборы Raft
election_mode: candidate # узел может стать лидером
election_fencing_mode: soft # сам слагает полномочия при потере кворума
synchro_quorum: 'N / 2 + 1' # формула: для 3 узлов это 2
synchro_timeout: 1000 # сек; для строгого Raft - очень большой
timeout: 1 # база для heartbeat (timeout * 4)Код: Выделить всё
box.cfg{
replication = {'node1_uri', 'node2_uri', 'node3_uri'},
election_mode = 'candidate',
election_fencing_mode = 'soft',
replication_synchro_quorum = 'N / 2 + 1',
replication_synchro_timeout = 1000,
replication_timeout = 1,
}Код: Выделить всё
-- синхронность задаётся ПОСПЕЙСНО
box.schema.space.create('orders', {is_sync = true})
box.space.orders:create_index('pk')
-- состояние выборов
box.info.election
-- state: leader, term: 5, vote: 1, leader: 1
-- состояние очереди синхронных транзакций
box.info.synchro
-- queue: {owner: 1, len: 0, busy: false, term: 5}
-- quorum: 2Частые заблуждения и грабли
Главная ловушка: synchro_quorum меньше N/2+1. Тогда возможен split vote и ДВА лидера сразу. Пример из доков: 5 узлов, quorum=2 - node1 и node2 голосуют за node1, node3 и node4 за node5, оба побеждают. Всегда большинство: (N/2)+1.
- Выборы без синхронной репликации почти бесполезны для сохранности данных. Если репликация асинхронная, старый лидер после потери связи продолжает коммитить асинхронные транзакции (их никто не проверяет кворумом) - получаете расхождение данных.
- Синхронность - поспейсная, не глобальная. Только один узел (владелец лимба) может делать синхронные транзакции; топология master-slave. Анонимные реплики в кворуме не участвуют (с 2.10).
- Смешивание sync и async опасно. Старый лидер мог закоммитить async-транзакции, которых нет ни у кого. После восстановления связи защита целостности кинет ER_SPLIT_BRAIN и заставит перебутстрапить узел.
- synchro_quorum считает и саму лидера. Для 3 узлов quorum=2 означает "лидер плюс одна реплика", а не "две реплики помимо лидера".
- Менять quorum при добавлении узлов нужно ЗАРАНЕЕ - обновить значение на всех существующих узлах до подключения нового, иначе на момент перехода большинство посчитается неверно.
- election требует full mesh - прямое соединение между каждой парой узлов, иначе голосование не работает.
Поднимите набор из 3 узлов с failover: election и synchro_quorum: 'N / 2 + 1'. Создайте синхронный спейс, вставьте строку, на каждом узле выполните box.info.synchro и box.info.election - найдите owner лимба, term и quorum. Затем остановите текущего лидера (узнать его можно по box.info.election.state == 'leader'), подождите несколько секунд и проверьте box.info.election на оставшихся узлах: убедитесь, что term увеличился и появился новый leader. Верните прежний узел и проверьте, что он стал follower и read-only.
Контрольные вопросы
- Что такое лимб (limbo) и в какой момент лидер пишет запись CONFIRM в WAL?
- Почему synchro_quorum обязан быть не меньше N/2+1, и к чему ведёт меньшее значение при выборах?
- Чем режим fencing soft отличается от strict и какую проблему классического Raft он закрывает?
- Что делает запись PROMOTE при смене лидера и какая команда вызывает её вручную?