Consensus Landscape: тур по алгоритмам консенсуса через симулятор

Флагманский тур по ландшафту консенсуса: осваиваем Raft, Paxos, Zab и EPaxos, воспроизводя перевыборы, сетевые партиции и цену latency в интерактивном симуляторе.

Консенсус — тема, в которой удивительно легко утонуть. С одной стороны — академическая точность: доказательства, кворумы, term-номера, десятки страниц TLA+ спецификаций. С другой — маркетинг продуктов: «etcd использует Raft», «Kafka использует KRaft», «Cassandra — это Paxos», как будто одно слово исчерпывает разговор. Между этими полюсами теряется главное: алгоритмы консенсуса ведут себя по-разному не в теории, а в конкретный момент отказа узла, разрыва сети или скачка latency между дата-центрами. И эту разницу проще прочувствовать, чем вычитать из доказательства.

Поэтому вместо очередного текстового разбора Raft построчно — открытый интерактивный симулятор Consensus Landscape (исходники). В нём можно убить лидера, разрезать кластер партицией, растянуть узлы по континентам — и увидеть, что происходит с кворумом и задержкой на самом деле, а не в идеализированном описании. Эта статья — тур по симулятору: разбираем теоретическую рамку ровно настолько, чтобы дальше говорить не «Raft вообще», а конкретно про эксперимент, который вы только что воспроизвели.

Зачем вообще карта консенсуса

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

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

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

Три свойства и одна невозможность

Любой алгоритм консенсуса — это способ гарантировать три свойства одновременно:

  • Agreement (согласие). Все живые узлы в итоге выбирают одно и то же значение — не «похожее», а буквально одно.
  • Validity (валидность). Выбранное значение — не выдумано алгоритмом, а действительно было кем-то предложено.
  • Termination (завершение). Решение принимается за конечное время, а не «когда-нибудь в теории».

Каждое свойство по отдельности звучит как нечто само собой разумеющееся. Проблема в том, что гарантировать все три сразу в полностью асинхронной сети — то есть без каких-либо предположений о верхней границе задержки сообщений — невозможно, если хотя бы один узел может отказать. Это и есть теорема FLP (Fischer, Lynch, Paterson, 1985): в асинхронной модели детерминированный алгоритм консенсуса не может одновременно гарантировать безопасность (Agreement/Validity) и живость (Termination) при наличии даже одного отказа. Без доказательства: суть в том, что в чисто асинхронной сети нельзя отличить «узел медленный» от «узел упал» — а значит, алгоритм либо рискует ошибиться, либо обязан ждать бесконечно.

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

flowchart TB C["Консенсус"] --> A["Agreement
все живые узлы
выбирают одно значение"] C --> V["Validity
значение кем-то
предложено"] C --> T["Termination
решение за
конечное время"] T -. "FLP 1985:
недостижимо в полностью
асинхронной сети" .-> F["Практический обход"] F --> F1["Частичная синхронность"] F --> F2["Детекторы отказов"] F --> F3["Рандомизация / таймауты"]

flowchart TB
  C["Консенсус"] --> A["Agreement
все живые узлы
выбирают одно значение"] C --> V["Validity
значение кем-то
предложено"] C --> T["Termination
решение за
конечное время"] T -. "FLP 1985:
недостижимо в полностью
асинхронной сети" .-> F["Практический обход"] F --> F1["Частичная синхронность"] F --> F2["Детекторы отказов"] F --> F3["Рандомизация / таймауты"]
Три обязательных свойства консенсуса и практический обход FLP

Знакомство с симулятором

Под капотом Consensus Landscape — дискретно-событийный движок: никакого реального времени и реальной сети, только виртуальные часы и очередь событий, которые движок разбирает строго по порядку. Каждый узел кластера гоняет реализацию выбранного алгоритма — Raft, Paxos, Zab или EPaxos — и не знает, что происходит «на самом деле», только то, какие сообщения до него дошли и когда. Сеть между узлами — не абстрактная труба с нулевой задержкой, а один из профилей: LAN (доли миллисекунды, потерь почти нет), WAN (десятки миллисекунд, изредка — потеря пакета) и Global (сотни миллисекунд, ощутимый разброс задержки между парами узлов). Профиль сети можно сменить на лету — и увидеть, как один и тот же алгоритм на тех же пяти узлах ведёт себя совершенно по-разному.

Дальше — инструменты вмешательства, ради которых симулятор и существует. Можно точечно инъецировать задержку или потерю пакета между конкретной парой узлов, разделить кластер сетевой партицией на majority/minority готовым сценарием, выключить узел целиком (в отличие от потери пакетов — узел действительно перестаёт отвечать), отправить клиентскую команду на запись и наблюдать её путь через кворум. Скорость симуляции регулируется отдельно от реального времени: можно прогнать выборы лидера за секунду или растянуть их покадрово, чтобы разглядеть каждое сообщение.

Честная оговорка о детерминизме: движок использует seeded PRNG, так что при одном и том же seed сценарий в целом воспроизводим — но не абсолютно, поскольку часть модулей (в первую очередь генерация случайных таймаутов выборов) местами обращается напрямую к Math.random() в обход seeded-генератора. Это осознанный компромисс симулятора, а не баг: важен повторяющийся класс поведения, а не побитное совпадение запусков. Для сравнения алгоритмов бок о бок интерфейс держит до трёх независимых симуляций параллельно на экране — так видно, например, как Raft и EPaxos реагируют на одну и ту же партицию в одном профиле сети.

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

Общий вид симулятора Consensus Landscape: 5 узлов, профиль LAN

Эксперимент 1. Убить лидера

Что сделать. Разверните кластер из пяти узлов, алгоритм — Raft, профиль сети — LAN. Дождитесь, пока кластер стабилизируется и выберет лидера (в логе это видно по AppendEntries-heartbeat, идущим от одного и того же узла ко всем остальным с постоянным интервалом). После этого — не раньше — выключите узел-лидер целиком, тем же инструментом, что отключает узел от сети, а не просто теряет его пакеты.

Что увидеть. У всех фолловеров идут одинаковые часы electionTimeout, но не синхронные: у каждого — свой случайный сдвиг в заданном диапазоне. Heartbeat от лидера их постоянно сбрасывает; как только лидер замолкает, таймер первого узла, чей electionTimeout истёк раньше остальных, срабатывает. Этот узел переходит в состояние кандидата, увеличивает term на единицу и рассылает RequestVote всем остальным. Живые узлы, ещё не голосовавшие в этом term, отвечают VoteGranted. Как только кандидат набирает голоса большинства — три из пяти, с учётом собственного голоса — он становится новым лидером и тут же начинает слать AppendEntries как heartbeat, останавливая чужие таймеры на будущее. В логе это должно быть видно буквально по шагам: истечение таймаута → рост term → серия RequestVote/VoteGranted → смена роли → возобновившийся поток AppendEntries.

Что это значит. Три детали здесь не случайны. Первая из них — прямой ответ на FLP; две другие — фундамент корректности Raft, работающий независимо от неё. Во-первых, electionTimeout рандомизирован намеренно: если бы все фолловеры ждали ровно одинаковое время, они превратились бы в кандидатов одновременно, каждый проголосовал бы сам за себя первым, голоса раскололись бы между несколькими кандидатами — и ни один не набрал бы большинства в этом term (split vote). Рандомизация — это ровно тот компромисс с недетерминизмом, которым Raft обходит FLP: жертвуем гарантией «выборы решатся за один раунд» ради того, чтобы решились вообще. Во-вторых, term растёт строго монотонно и никогда не откатывается назад: это единственный способ для кластера договориться, какая из конкурирующих попыток выборов — самая свежая, без обращения к настенным часам. В-третьих, это уже про безопасность, а не про прогресс: ни одна подтверждённая клиенту запись не теряется при смене лидера. Новый лидер избирается только из числа узлов, чей лог не отстаёт от большинства (это часть протокола голосования, а не удача), поэтому всё, что успело закоммититься до отказа, остаётся в логе нового лидера.

sequenceDiagram participant F1 as Follower N2 participant F2 as Follower N3 participant C as Candidate N4 Note over C: electionTimeout истёк
term += 1 C->>F1: RequestVote(term) C->>F2: RequestVote(term) F1-->>C: VoteGranted F2-->>C: VoteGranted Note over C: большинство (3/5)
→ становится лидером C->>F1: AppendEntries(term, heartbeat) C->>F2: AppendEntries(term, heartbeat)

sequenceDiagram
  participant F1 as Follower N2
  participant F2 as Follower N3
  participant C as Candidate N4
  Note over C: electionTimeout истёк
term += 1 C->>F1: RequestVote(term) C->>F2: RequestVote(term) F1-->>C: VoteGranted F2-->>C: VoteGranted Note over C: большинство (3/5)
→ становится лидером C->>F1: AppendEntries(term, heartbeat) C->>F2: AppendEntries(term, heartbeat)
Перевыборы в Raft после отказа лидера: term растёт, кандидат собирает большинство

Перевыборы в Raft: после отказа лидера кластер избрал нового, term увеличился

Подробный разбор состояний, RPC и корректности алгоритма — в документации проекта по Raft.

Эксперимент 2. Разрезать кластер пополам

Что сделать. Оставьте тот же кластер из пяти узлов на Raft и профиле LAN, дождитесь стабильного лидера — и вместо отключения узла запустите сценарий «Partition и heal»: он разрезает кластер на две группы — 3 узла (в том числе — текущий лидер) и 2 узла. Партиция здесь — не потеря отдельных пакетов, а полный разрыв связи между группами: любое сообщение из одной группы в другую пропадает, а внутри каждой группы узлы по-прежнему видят друг друга.

Что увидеть. Сторона большинства почти не замечает разрыва: лидер по-прежнему рассылает AppendEntries, получает подтверждения от двух живых фолловеров в своей группе — итого три из пяти, кворум N/2+1 набран — и продолжает коммитить записи клиента как ни в чём не бывало. Сторона меньшинства ведёт себя иначе: если в её составе оказался бывший лидер — он перестаёт получать подтверждения от большинства узлов и в логе видно, что его записи повисают в состоянии «не закоммичено». Если же в меньшинстве остались только фолловеры — у них по истечении electionTimeout начинаются попытки перевыборов: term растёт, RequestVote уходит в разрыв и не возвращается, кандидат набирает максимум один голос из двух — и не становится лидером. Клиент, который стучится в меньшинство, получает не устаревшие данные и не тихое зависание, а явную ошибку — «нет лидера» или «не удалось набрать кворум». Затем снимите партицию: сеть между группами восстанавливается, узлы меньшинства почти сразу получают AppendEntries от актуального лидера с более высоким term, распознают его легитимность, откатывают всё незакоммиченное и подтягивают лог до состояния большинства.

Что это значит. Кворум N/2+1 — это не бюрократическая формальность, а единственная причина, по которой при разрыве сети не может одновременно существовать двух лидеров, каждый из которых считает себя главным и коммитит свою версию данных: две непересекающиеся группы одного кластера физически не могут набрать большинство одновременно. Именно поэтому чётное число узлов — плохая инженерная идея: кластер из четырёх узлов при разрыве 2|2 не даёт большинства ни одной из сторон, и весь кластер простаивает там, где нечётные пять или семь узлов продолжили бы работать. Отсюда же и практический вывод про доступность: Raft здесь осознанно выбирает согласованность важнее доступности для меньшинства — узел лучше откажется отвечать, чем ответит устаревшим значением. Для клиента это выглядит как частичная недоступность, но не как молчаливая потеря или расхождение данных — split-brain оказывается архитектурно невозможен, а не просто маловероятен.

flowchart LR subgraph Majority["Большинство — 3 узла"] L["Лидер"] --- N2["Узел"] --- N3["Узел"] end subgraph Minority["Меньшинство — 2 узла"] N4["Узел"] --- N5["Узел"] end Majority -. "разрыв сети" .- Minority L --> OK["Кворум 3/5 ✓
коммит проходит"] N4 --> FAIL["Кворум 2/5 ✗
коммита нет, ошибка клиенту"]

flowchart LR
  subgraph Majority["Большинство — 3 узла"]
    L["Лидер"] --- N2["Узел"] --- N3["Узел"]
  end
  subgraph Minority["Меньшинство — 2 узла"]
    N4["Узел"] --- N5["Узел"]
  end
  Majority -. "разрыв сети" .- Minority
  L --> OK["Кворум 3/5 ✓
коммит проходит"] N4 --> FAIL["Кворум 2/5 ✗
коммита нет, ошибка клиенту"]
Партиция 3|2: только сторона большинства набирает кворум и коммитит

Сетевая партиция 3|2: сторона меньшинства осталась без кворума и коммита

Подробнее про кворумы, чётность узлов и поведение при партициях — в документации проекта.

Эксперимент 3. Цена расстояния

Что сделать. Разверните тот же кластер из пяти узлов на Raft три раза подряд — параллельно, бок о бок в интерфейсе, меняя только профиль сети: LAN (1–5 мс), WAN (30–100 мс) и Global (100–300 мс). В каждой из трёх симуляций проделайте одно и то же: дождитесь стабильного лидера и отправьте клиентскую команду на запись — как в эксперименте 1, но без отключения узлов.

Что увидеть. Топология и алгоритм идентичны, разница — только в сетевой задержке, но время до коммита записи растёт вместе с ней прямо пропорционально: AppendEntries до фолловеров и обратные подтверждения идут по той же сети, что и heartbeat, поэтому на Global-профиле подтверждение кворума занимает на порядки дольше, чем на LAN. Ещё заметнее эффект на перевыборах: electionTimeout — фиксированная величина, заданная в конфигурации кластера, а не адаптирующаяся к текущей сети, и на Global-профиле сообщения RequestVote/VoteGranted иногда не успевают обернуться туда-обратно до истечения таймера. Кандидат стартует выборы, не набирает кворум вовремя, term увеличивается снова — и кластер входит в серию паразитных перевыборов на ровном месте, без единого реального отказа.

Что это значит. Ложные перевыборы здесь — не баг конкретной реализации, а прямое следствие нарушенного соотношения таймаутов: electionTimeout обязан быть намного больше heartbeatInterval, а тот, в свою очередь, — намного больше фактической networkDelay (electionTimeout ≫ heartbeatInterval ≫ networkDelay). Профиль, подобранный под LAN, не масштабируется автоматически при переносе кластера в другой регион — это ровно та эксплуатационная ловушка, в которую попадают реальные кластеры etcd или Kafka KRaft при неосторожном растягивании через дата-центры. Отсюда практический вывод шире, чем настройка одного параметра: география — не деталь конфигурации, которую можно подкрутить и забыть, а фундаментальный trade-off latency консенсуса. Чем шире кластер разнесён по континентам, тем дороже каждый коммит, требующий подтверждения от большинства, — и тем менее устойчив к задержке любой алгоритм, завязанный на единственного лидера, через которого обязана пройти каждая запись. Именно это ограничение и подталкивает часть систем к принципиально другой архитектуре — leaderless-схемам, где запись не ждёт одного координатора, а согласуется напрямую с кворумом узлов. О том, где проходит граница между алгоритмами одного семейства и где заканчивается сходство Raft, Paxos, Zab и EPaxos, — в следующем разделе.

Профили latency LAN/WAN/Global рядом: время до коммита растёт с задержкой сети

Эксперимент 4. Когда лидера можно не выбирать

Что сделать. Выберите алгоритм EPaxos, профиль сети — Global. Запустите готовый сценарий «Fast path vs slow path»: в симуляторе он бьёт двумя клиентами одновременно в разные узлы, так что часть команд неизбежно пересекается по времени. Для контраста разверните рядом сравнительный сценарий «Параллельные записи» с Raft и EPaxos бок о бок — интерфейс держит до трёх независимых симуляций параллельно на экране, как и в эксперименте 3.

Что увидеть. Независимые, не конфликтующие друг с другом команды EPaxos коммитит за 1 RTT через fast-quorum: без лидера, без дополнительного раунда согласования, и принять команду от клиента может любой узел кластера — не только заранее выделенный координатор. Как только две команды конфликтуют — то есть пересекаются по ключам или иначе не коммутируют между собой, — быстрый путь закрывается: узлам требуется дополнительный раунд (slow path), чтобы явно зафиксировать порядок между конфликтующими командами, прежде чем каждая из них будет закоммичена. Рядом, в параллельной симуляции с Raft, картина другая независимо от того, конфликтуют команды клиента или нет: любая запись обязана пройти через лидера, а на профиле Global это фиксированный лишний round-trip до лидера и обратно — даже когда команды между собой никак не пересекаются.

Что это значит. EPaxos убирает лидера как узкое место кластера: на гео-распределённом развёртывании с командами, которые конфликтуют редко, это не косметическая, а кратная разница в latency — 1 RTT против обязательного захода через лидера и обратно. Плата за это — заметно более сложный протокол разрешения зависимостей между командами и деградация до slow path именно в тот момент, когда конфликты действительно случаются. Это прямой ответ на вопрос, поставленный в конце эксперимента 3 («отсюда растут leaderless-схемы»): когда нагрузка в основном состоит из независимых команд, а кластер разнесён по континентам, отказ от единственного лидера окупается — но не бесплатно, и цена платится ровно там, где команды начинают пересекаться. Отсюда и переход к следующему разделу: единственного «правильного» алгоритма консенсуса не существует — выбор диктуют профиль нагрузки (много ли конфликтов) и топология (насколько широко разнесён кластер).

flowchart TB W["Клиентская команда
приходит на любой узел"] --> Q{"Конфликтует с другой
одновременной командой?"} Q -- "нет (независимые)" --> FP["Fast path
1 RTT: fast-quorum подтверждает
коммит без лидера и без доп. раунда"] Q -- "да (пересекаются)" --> SP["Slow path
+1 раунд: явно фиксируется порядок
между конфликтующими командами"] FP --> G["Global-профиль:
экономия RTT кратно ускоряет запись"] SP --> G

flowchart TB
  W["Клиентская команда
приходит на любой узел"] --> Q{"Конфликтует с другой
одновременной командой?"} Q -- "нет (независимые)" --> FP["Fast path
1 RTT: fast-quorum подтверждает
коммит без лидера и без доп. раунда"] Q -- "да (пересекаются)" --> SP["Slow path
+1 раунд: явно фиксируется порядок
между конфликтующими командами"] FP --> G["Global-профиль:
экономия RTT кратно ускоряет запись"] SP --> G
EPaxos: независимые команды коммитятся за 1 RTT (fast path), конфликтующие требуют дополнительного раунда (slow path)

EPaxos на Global, сценарий «Fast path vs slow path»: независимые команды коммитятся за 1 RTT, конфликтующие уходят в slow path

Подробнее про fast-quorum, разрешение зависимостей и slow path EPaxos — в документации проекта.

Одно семейство или разные

Пять названий — Basic Paxos, Multi-Paxos, Raft, Zab, EPaxos — легко свалить в одну кучу под общим ярлыком «консенсус», но эксперименты 3 и 4 показывают, зачем эту кучу разбирать. Ключевая ось, вдоль которой алгоритмы расходятся, — не «кто из них лучше», а leader-based против leaderless: должна ли каждая запись обязательно пройти через одного выделенного координатора, или узлы вправе согласовывать значение напрямую, без единой точки, через которую обязан идти весь трафик.

Basic Paxos исторически стоит у истоков — и он честно leaderless: любой узел, получивший клиентский запрос, может выступить «предлагающим» (proposer) и провести раунд Prepare/Accept с кворумом. Красота в отсутствии выделенной роли; цена — Basic Paxos решает ровно одно значение за раунд, а при конкуренции нескольких proposer’ов раунды легко зацикливаются в livelock, пока один не перебьёт другого более высоким номером предложения. На практике его почти никто не разворачивает как есть — Basic Paxos важен как теоретический фундамент, а не как production-протокол; сложность корректной реализации (в первую очередь — обработка edge cases с несколькими одновременными proposer’ами) вошла в индустрию как отдельная легенда под названием «Paxos сложно реализовать».

Multi-Paxos — прагматичный ответ на эту сложность: вместо того чтобы каждый раз заново разыгрывать полный раунд Prepare/Accept, кластер выбирает стабильного лидера, который на длинной серии решений пропускает фазу Prepare и сразу шлёт Accept. Это тот же протокол в основе, но уже не leaderless — лидерство здесь не архитектурная необходимость, а оптимизация пропускной способности для серии решений, а не одного. Именно этот сдвиг — «давайте зафиксируем лидера ради эффективности» — и есть мостик к следующему поколению алгоритмов.

Raft берёт ту же идею стабильного лидера, но меняет приоритет: где Multi-Paxos выводится из Paxos как оптимизация, Raft с самого начала проектировался ради понятности — явные роли (лидер/кандидат/фолловер), единый монотонный term вместо номеров предложений, явный лог с чёткими правилами коммита. Именно эта понятность, а не превосходство в теоретических гарантиях, объясняет, почему Raft захватил прод: то, что вы воспроизвели в экспериментах 1 и 2 — перевыборы по таймауту, кворум N/2+1, невозможность split-brain — читается из спецификации Raft напрямую, без реверс-инжиниринга академической статьи. Zab, протокол ZooKeeper, — тоже leader-based, но вырос из другой практической задачи: не «реплицировать лог», а гарантировать строгий порядок broadcast-сообщений всем узлам, причём именно в том порядке, в котором их видел лидер. Ради этого Zab вводит третью фазу поверх привычной пары «предложить/подтвердить» — синхронизацию нового лидера с историей кластера перед тем, как он начнёт принимать новые записи, — что делает восстановление после смены лидера более предсказуемым, но и более тяжеловесным, чем у Raft.

EPaxos возвращается к leaderless-модели, но не как компромисс ради простоты, а как осознанная ставка на другую метрику: если команды клиентов не конфликтуют между собой (не трогают одни и те же ключи), EPaxos способен закоммитить запись за один RTT, договорившись напрямую с кворумом, без промежуточного захода через единственного лидера. Плата — ощутимо более сложный протокол разрешения зависимостей между командами: как только конфликты появляются, EPaxos откатывается на дополнительный раунд согласования, а сама логика корректности труднее для интуиции, чем «один лидер решает порядок». Здесь и проявляется прямая связь с экспериментом 3: на LAN экономия одного RTT незаметна, но на Global-профиле, где каждый проход через лидера стоит сотни миллисекунд туда-обратно, тот же выигрыш превращается в кратное ускорение записи. Именно поэтому leaderless-архитектуры особенно привлекательны для гео-распределённых кластеров.

Выбор внутри этого ландшафта — не про «какой алгоритм правильный», а про то, какой trade-off устраивает конкретную систему: понятность и предсказуемость Raft, строгий порядок Zab, эффективность серии решений в Multi-Paxos или готовность к сложности ради latency на WAN в EPaxos.

Алгоритм Модель Ключевая идея Где силён
Basic Paxos leaderless исторический фундамент теория, единичные решения
Multi-Paxos стабильный лидер серия решений через одного лидера высоконагруженная репликация
Raft leader-based понятность и явные роли большинство продовых систем
Zab leader-based, 3 фазы строгий порядок broadcast ZooKeeper, координация
EPaxos leaderless 1 RTT на fast path гео-распределённые кластеры
flowchart TB Root["Алгоритмы консенсуса"] --> LB["Leader-based"] Root --> LL["Leaderless"] LB --> R["Raft
понятность"] LB --> MP["Multi-Paxos
стабильный лидер"] LB --> Z["Zab
строгий порядок"] LL --> BP["Basic Paxos
фундамент"] LL --> EP["EPaxos
1 RTT fast path"]

flowchart TB
  Root["Алгоритмы консенсуса"] --> LB["Leader-based"]
  Root --> LL["Leaderless"]
  LB --> R["Raft
понятность"] LB --> MP["Multi-Paxos
стабильный лидер"] LB --> Z["Zab
строгий порядок"] LL --> BP["Basic Paxos
фундамент"] LL --> EP["EPaxos
1 RTT fast path"]
Семейства подходов: leader-based против leaderless

Подробнее про каждый алгоритм — в документации проекта: Raft, Basic Paxos, Multi-Paxos, Zab, EPaxos.

Где это в реальной инфраструктуре

Всё, что воспроизведено в симуляторе, — не лабораторная абстракция, а буквально то, что происходит под капотом трёх систем, на которых держится значительная часть современной инфраструктуры. etcd — это Raft, и именно etcd служит control plane Kubernetes: в нём хранится всё состояние кластера, а kube-apiserver не может подтвердить запись, пока etcd не наберёт кворум. Отсюда прямое следствие эксперимента 2: etcd разворачивают нечётным числом узлов (обычно 3 или 5) не из суеверия, а потому что чётное число даёт худшее соотношение отказоустойчивости к стоимости кворума — 4 узла переживают тот же один отказ, что и 3, но при партиции 2|2 не набирают большинства ни на одной стороне и останавливают весь control plane. Гео-растянутый кворум etcd между дата-центрами — известная эксплуатационная ловушка ровно по причине, вскрытой в эксперименте 3: electionTimeout статичен, а networkDelay на WAN/Global-профиле съедает его запас, и кластер входит в паразитные перевыборы вместо стабильной работы.

Kafka прошла путь, зеркальный сюжету этой статьи: до недавнего времени она опиралась на внешний ZooKeeper-ансамбль (Zab) для метаданных и выбора контроллера, а с переходом на KRaft кворум метаданных стал внутренним Raft-кластером контроллеров — тот же алгоритм из экспериментов 1 и 2, но управляющий не пользовательскими данными, а самой топологией Kafka. ZooKeeper и его Zab по-прежнему остаётся рабочей лошадкой классической координации — распределённых блокировок, конфигурации, service discovery — там, где нужен строгий порядок broadcast, а не журнал команд.

Ниже — иллюстративные конфигурации bootstrap для 3-узлового кластера (точные версии и флаги стоит сверять с актуальной документацией перед продовым разворачиванием):

etcd --name n1 \
  --initial-cluster n1=http://10.0.0.1:2380,n2=http://10.0.0.2:2380,n3=http://10.0.0.3:2380 \
  --initial-cluster-state new
process.roles=controller
controller.quorum.voters=1@10.0.0.1:9093,2@10.0.0.2:9093,3@10.0.0.3:9093

Куда идти дальше

Карта, которую эта статья разворачивала от FLP до прода, не заменяет опыт — она задаёт рамку, внутри которой опыт становится понятнее. Прочитать про кворум N/2+1 и увидеть, как он не даёт кластеру расколоться на два лидера, — два разных знания, и второе держится в памяти дольше именно потому, что было прожито, а не законспектировано.

Поэтому лучший способ закрепить прочитанное — не перечитать разделы ещё раз, а вернуться в симулятор и воспроизвести все четыре эксперимента самостоятельно, шаг за шагом: убить лидера и проследить перевыборы по логу, разрезать кластер партицией и увидеть, как большинство продолжает коммитить, а меньшинство — честно отказывает, растянуть кластер по профилям сети и поймать паразитные перевыборы на Global, столкнуть на EPaxos независимые и конфликтующие команды и разглядеть разницу между fast path и slow path. А затем — самостоятельный пятый сценарий, уже без подсказок: например, поднять потери пакетов на LAN-профиле до заметного процента и посмотреть, как поведёт себя Raft, когда сеть не рвётся начисто, а просто ненадёжна.

Симулятор открыт: consensus.khorost.tech — сама песочница, consensus.khorost.tech/docs/ — документация по алгоритмам и внутреннему устройству движка, github.com/khorost-tech/consensus-landscape — исходники под MIT. Форки, issues и разбор реализации приветствуются — карта консенсуса становится точнее с каждым, кто в неё заглянул.

Источники

Первоисточники (алгоритмы и теория):

Продакшн-системы:

Обсуждение в Telegram

Присоединиться →

Комментарии