dbit.one© 2026
000
booting_
Loading experience0%
dbit.one

Падает мастер: что на самом деле происходит с вашими данными

Коротко: «у нас кластер из трёх узлов» — это описание конфигурации, а не свойство системы. Свойство появляется тогда, когда кто-то проверил: при разделении сети два узла не решат одновременно, что они главные, а запись, подтверждённая клиенту, не исчезнет вместе с упавшим лидером. Проверяется это симуляцией, а не выключением сервера руками.

9 мин чтения

Коротко

  • «Реплики есть» и «подтверждённые данные не теряются» — разные утверждения. Второе — про исполнения, а не про конфигурацию.
  • Ручная проверка «убьём мастер» разыгрывает одно расписание отказов из астрономического пространства и не воспроизводится.
  • Детерминированная симуляция задаёт разделения, падения и перестановку сообщений сидом: найденный отказ получает адрес.
  • Проверять нужно и сам тест. Музей багов выключает по одному правилу алгоритма и требует, чтобы харнесс поймал его с сидом.

Что вам продали и что вы купили

«Отказоустойчивый кластер», «репликация», «автоматическое переключение» — это описание того, как система собрана. Вопрос, который стоит денег, звучит иначе: если прямо сейчас упадёт узел, только что подтвердивший клиенту запись, останется ли эта запись в системе после того, как поднимется новый лидер.

Между «у нас три реплики» и «подтверждённое не пропадает» лежит алгоритм консенсуса — Raft, Paxos, ZAB. Он отвечает на два вопроса: кто сейчас главный и что считать зафиксированным. Ошибка в нём не выглядит как ошибка. Система работает, отвечает, метрики зелёные — просто в одном исполнении из миллиона клиенту сказали «сохранено», а записи нет.

Четыре способа потерять подтверждённую запись

Все четыре — не экзотика. У каждой в статье про Raft есть свой подраздел, а подразделы пишут не просто так: значит, очевидную версию кто-то уже отгружал в продакшн.

Что произошло
Чем это оборачивается
Голос не пережил перезапуск узла
два лидера в одном терме, две ветки истории, одна будет затёрта
Лидером стал узел со старым логом
подтверждённой записи нет у нового лидера — она исчезает
Лидер зачёл унаследованную запись по числу реплик
то, что считалось зафиксированным, перезаписывается после смены лидера
Ответ клиенту ушёл раньше кворума
у клиента «сохранено», в кластере — ничего

Третья строка — самая коварная: она требует четырёх смен лидерства в определённом порядке и появляется в отладке примерно никогда. В статье Raft это Figure 8, и ради неё в алгоритм добавлено отдельное правило.

Почему обычные тесты этого не ловят

  • Интеграционный тест гоняет счастливый путь: все живы, сеть работает, сообщения приходят по одному разу и в порядке отправки. Алгоритм консенсуса именно на этом пути и не нужен.
  • Хаос-инженерия убивает контейнеры в случайные моменты — но не записывает, что именно она сделала. Упало один раз, и повторить это уже нельзя.
  • Отказ, который нельзя повторить, не чинят: его списывают на инфраструктуру. Это тот же механизм, что и с плавающим тестом, только цена ошибки другая.
  • Времени мало. Чтобы поймать редкое расписание в реальном кластере, нужно ждать реальные секунды — а нужных расписаний тысячи.

Симуляция вместо стойки с серверами

Первый шаг — сделать узел машиной состояний без ввода-вывода. Узел не трогает ни часы, ни сокет, ни диск: tick двигает его таймеры, receive отдаёт сообщение, propose — команду, а всё, что он хочет сделать, забирают наружу takeMessages и takeApplied. Так устроен raft в etcd, и по той же причине: реализацию, которая сама вызывает setTimeout и пишет в сокет, можно тестировать только запуском и надеждой.

Второй шаг — кластер целиком существует внутри симуляции. Каждый тик, каждая задержка, потерянный пакет, дубликат, перестановка и падение узла — решение, вытянутое из сида. Проверки Raft рассчитаны ровно на такой мир: сообщения теряются, задерживаются, дублируются и приходят не по порядку, а узлы падают и возвращаются. Инъекция чего-то меньшего проверяет сеть, для которой алгоритм не предназначался.

ts
import { check } from 'unflake';
import { runScenario, SafetyMonitor, checkLinearizable, operationsFrom, HOSTILE } from 'bulwark';

await check('raft остаётся безопасным', async (sim) => {
  const { cluster } = await runScenario(sim, {
    size: 5,
    clients: 3,
    faults: HOSTILE,
    chaos: true, // падения, перезапуски, разделения сети
  });

  // Реплики сравниваются после того, как отказы прекратились.
  const disagreement = SafetyMonitor.replicasAgree(cluster);
  if (disagreement) sim.fail(disagreement);

  // История клиентских операций — на линеаризуемость.
  const report = checkLinearizable(operationsFrom(cluster.history));
  if (report.status === 'not-linearizable') sim.fail(report.reason);
}, { runs: 200 });
Прогон: пять узлов, три клиента, разделения и падения — всё из сида

Что именно проверяется

Пять свойств безопасности из статьи Raft проверяются после каждого перехода состояния — после тика, после доставленного сообщения, — а не опросом раз в секунду. Тогда нарушение приписывается тому переходу, который его вызвал, а не тому месту, где тест наконец решил посмотреть.

Свойство
Что оно запрещает
Election Safety
больше одного лидера в одном терме
Leader Append-Only
лидеру переписывать собственный лог, а не дописывать
Log Matching
расхождение логов при совпадении индекса и терма
Leader Completeness
зафиксированной записи отсутствовать у следующего лидера
State Machine Safety
двум репликам применить разные команды на одном индексе

Отдельно — линеаризуемость истории клиентских операций: каждая операция должна выглядеть выполненной мгновенно в некоторый момент между вызовом и ответом, и все вместе — в одном порядке, согласованном с реальным временем. Система может соблюсти все пять свойств и всё равно показать клиенту невозможное: применить повторно присланную команду дважды. Консенсус при этом цел, а клиент увидел то, чего не бывает — ровно поэтому в платёжных системах существует идемпотентность операций.

Задача о линеаризуемости NP-полна, поэтому у поиска есть бюджет. Когда бюджет кончается, честный ответ — inconclusive, а не «всё хорошо». Разница между «не нашли» и «не существует» здесь такая же, как в выборке расписаний.

Проверка проверки: музей багов

Набор тестов, который всегда зелёный, может ловить ошибки, а может не смотреть вообще — по цвету это неразличимо. Поэтому берётся работающая реализация, в ней выключается ровно одно правило алгоритма, и от харнесса требуется поймать это с сидом и с именем нарушенного свойства.

text
нет проверки актуальности лога  → Leader Completeness: n2 стал лидером в терме 4
                                  без зафиксированной записи r6            (прогон 2)
нет проверки предыдущей записи  → Log Matching: индекс 1 терм 1 держит noop:n1:1
                                  в одном месте и r2 на n4                 (прогон 1)
конфликтующая запись остаётся   → State Machine Safety: индекс 8: n5 применил
                                  noop:n5:6, n2 применил r7                (прогон 4)
нет пустой записи лидера        → реплики разошлись: n1={"a":"p1.2"}
                                  против n4={"a":"p1.2","b":"p1.3"}        (прогон 25)
игнорирует более новый терм     → клиент 2 сдался на операции cas          (прогон 1)
Figure 8                        → Leader Completeness: n5 стал лидером в терме 4
                                  без зафиксированной записи entry-A       (прогон 1)
голос не пережил перезапуск     → Election Safety: n2 и n3 — лидеры в терме 1
Семь экспонатов: что выключено — и чем это поймано

Важно не то, что все семь ловятся, а что ловятся по-разному. Три ломают свойство безопасности прямо. Один оставляет все пять свойств целыми и молча лишает кластер сходимости. Один не стоит безопасности вообще — только прогресса. Свести это к «тест покраснел» значит выбросить самое содержательное.

Два последних экспоната — направленные, а не случайные. Figure 8 требует четырёх смен лидерства в заданном порядке с заданными разделениями; окно, в котором голос не успевает попасть на диск, шириной в один тик. Надеяться набрести на такое случайно — не план.

Баг, который харнесс нашёл в самой реализации

Это случай, ради которого всё и строится. Все пять свойств держались на любом расписании — и хаос-прогон всё равно упал на двадцать пятом сиде: две реплики с разными значениями. Логи при этом совпадали. Никто не падал. Ни одно правило из статьи нарушено не было.

Причина — §5.4.2, работающий ровно так, как написано. Лидер не имеет права фиксировать унаследованную от прошлого терма запись подсчётом реплик: он обязан зафиксировать запись своего терма, которая утянет за собой предыдущие. Но если клиенты замолчали сразу после смены лидера, такая запись не появляется никогда — и унаследованные остаются незафиксированными навсегда. Реплики, успевшие применить их при прошлом лидере, оказываются впереди тех, кто сигнала о фиксации не дождался.

Лекарство описано в статье одной фразой в §8: новый лидер сразу дописывает пустую запись своего терма. Теперь она в реализации есть, а её отсутствие — четвёртый экспонат музея.

Чего этот метод не умеет

  • Прохождение — не доказательство. Сотни сидов — это сотни расписаний из пространства, которое астрономически больше. Очень хороший фаззинг, но не теорема; TLA+-спецификации Raft доказывают то, чего симуляция доказать не может.
  • Модель отказов конечна. Потеря, задержка, дублирование, перестановка, разделение и падение с сохранением диска — да. Порча диска, частичная запись, расхождение часов и византийское поведение — нет.
  • Это не готовая база данных. Нет изменения состава кластера, нет компакции лога и снапшотов — а именно они превращают корректный алгоритм в эксплуатируемую систему, и у каждого свой отдельный аргумент безопасности.
  • Чекер может сдаться. Линеаризуемость NP-полна, бюджет конечен, inconclusive — реальный исход, и он сообщается как есть.

Что спрашивать, когда вам обещают отказоустойчивость

Пять вопросов подрядчику, команде или вендору
  • Что именно проверяет ваш тест отказоустойчивости: что кластер поднялся — или что подтверждённые клиенту записи на месте?
  • Последний найденный отказ воспроизводится по идентификатору прогона — или существует в виде «мы такое однажды видели»?
  • Отвечает ли система клиенту до того, как запись принята кворумом?
  • Есть ли проверка, что тест вообще способен поймать ошибку, — например, прогон с намеренно выключенным правилом?
  • Что произойдёт с клиентским запросом в момент, когда кворум недостижим: отказ с внятной ошибкой или тихое ожидание?

Последний вопрос практичнее прочих: чаще всего теряют не консенсус, а границу вокруг него — повтор запроса без ключа идемпотентности, ответ «ок» до фиксации, ретрай, который дублирует платёж. Мы разбираем это в материалах о финансовых системах и облачной инфраструктуре, а весь список технических разборов — в рубрике «Инженерия».

Частые вопросы

Мы не пишем свой Raft, мы берём готовую базу. Это всё ещё про нас?

Да, в двух местах. Первое — выбор и настройка: обещание «HA из коробки» имеет цену, и её платят кворумом и задержкой, а параметры по умолчанию редко совпадают с вашими требованиями к потере данных. Второе — ваш собственный код вокруг: ответ клиенту, повторы, ключи идемпотентности. Большинство потерь происходит там, а не внутри алгоритма.

Чем это отличается от Jepsen?

Jepsen атакует настоящий кластер снаружи и проверяет наблюдаемую историю на линеаризуемость — это про готовые системы. Симуляция работает изнутри и детерминированно: тот же сид даёт тот же прогон, а виртуальное время делает сотни расписаний дешёвыми. Подходы дополняют друг друга: один находит то, что происходит в реальной сети, другой — то, что происходит редко.

Сколько прогонов достаточно?

Двести — разумное значение по умолчанию, потому что время в симуляции виртуальное и прогон стоит миллисекунды. Но количество не заменяет разнообразия: направленные сценарии вроде Figure 8 не выпадают случайно, их нужно строить руками.

Что даёт клиенту то, что вы написали свой Raft?

Не сам Raft — его писали многие. Даёт метод: реализация и харнесс уличают друг друга, и это переносится на обычные проекты, где консенсуса нет, а гонки, повторы и потерянные подтверждения есть. Код открыт: bulwark, MIT, ссылка в источниках.

Источники

Утверждения из статьи можно проверить: ниже первоисточники, а не пересказ.

  1. 01Ongaro, Ousterhout. In Search of an Understandable Consensus AlgorithmРасширенная версия статьи: Figure 3 со свойствами, §5.4.2 и §8
  2. 02etcd raftУзел без ввода-вывода — та же форма и по той же причине
  3. 03FoundationDB: TestingБаза данных, корректность которой держится на детерминированной симуляции
  4. 04JepsenПроверка линеаризуемости настоящих распределённых систем снаружи
  5. 05bulwarkРеализация и харнесс из этой статьи: MIT, музей багов внутри
Автор
Дониёр Ботиров
Основатель dbit.one · автор материала

Этот материал подписан именем, в отличие от остальных: за ним не стоит клиент. Код, о котором идёт речь, открыт целиком и лежит под тем же именем — его можно прочитать, запустить и проверить утверждения статьи, не поверив ни одному из них на слово.

Услуги по теме

Читать дальше

Инженерия8 мин чтения

Тест падает раз в триста прогонов. Это не погода

Коротко: тест, который падает раз в триста прогонов, уже нашёл настоящую гонку — вы просто не можете её повторить. Случаен не баг, а порядок выполнения, который выбрала операционная система. Отберите у неё этот выбор — и падение из анекдота превращается в адрес.

Инженерия6 мин чтения

Повтор запроса не должен списывать деньги дважды

Коротко: сеть не сообщает, прошёл платёж или нет, — она просто молчит. Клиент нажимает кнопку второй раз, мобильное приложение повторяет запрос само, и без защиты вы списываете дважды. Разбираем, как устроена идемпотентность на уровне таблиц и кода и где её обычно ломают.

Инженерия10 мин чтения

Тесты не доказывают отсутствие бага. Что доказывает

Коротко: набор тестов — это поиск. Он умеет сказать «нашёл» и не умеет сказать «нет». Для большей части кода этого достаточно, но не для протокола оплаты, конечного автомата заказа и любого места, где две задачи трогают общее состояние: там редкое сочетание шагов и есть баг. Проверка моделей обходит все достижимые состояния и отвечает на вопрос, на который тесты не отвечают в принципе.

Остались вопросы по вашему проекту?

Опишите задачу — в течение 24 часов вернёмся с оценкой, сроками и планом.

[email protected]