
Введение
Консенсус в распределённой системе это согласие множества узлов относительно общего решения или порядка операций, сохраняющееся при сбоях узлов и задержках сети. Задача выглядит тривиальной ровно до того момента, пока не выясняется, что узел не может отличить упавшего соседа от медленного, а сеть вправе доставить сообщение через час после отправки.
Paxos, Raft и Zab расходятся почти во всём: в ролях узлов, в способе выбрать лидера, в том, что происходит после сбоя. Общее у них одно требование: решение должно приниматься группой узлов, любые две такие группы обязаны пересекаться, и простейший способ это гарантировать - большинство. Из этого требования вырастает всё остальное, включая различия между etcd, ZooKeeper и Consul, которые при общем фундаменте дают разным клиентам разные гарантии чтения.
Paxos: чем платят за безопасность
Paxos – базовый алгоритм консенсуса, гарантирующий достижение единого решения (одного значения) в асинхронной сети при сбоях узлов, если работает хотя бы большинство. Paxos оперирует тремя ролями узлов – предлагающие (proposers), акцепторы (acceptors) и обучающиеся (learners) – которые могут сочетаться на одних и тех же серверах. Предлагающие выдвигают значения, акцепторы голосуют за них, а обучающиеся узнают результат выбора. Ключевое свойство Paxos – безопасность (safety): он гарантирует, что два разных узла не зафиксируют разные решения, даже если сообщения теряются или узлы перезагружаются. Однако прогресс (liveness) возможен только при доступности кворума узлов и при отсутствии бесконечной дуэли предлагающих.
У этого ограничения есть строгое обоснование. Теорема FLP (Fischer, Lynch, Paterson, 1985) доказывает, что в полностью асинхронной сети, где нет верхней границы на задержку сообщений и скорость узлов - детерминированный алгоритм консенсуса не может гарантировать завершение даже при отказе одного-единственного узла. Причина в неразличимости: получатель не способен отличить упавший узел от очень медленного, а любое конечное время ожидания оказывается произвольным.
Paxos обходит это тем, что разделяет свои гарантии. Безопасность соблюдается безусловно, при любых задержках и потерях сообщений: два узла никогда не зафиксируют разные решения. А вот прогресс гарантируется только когда сеть ведёт себя достаточно предсказуемо - то есть в модели частичной синхронности. Отсюда и дуэль предлагающих (dueling proposers): два узла могут по очереди перебивать друг друга всё бо́льшими номерами, и каждый раз обнулять работу другого. Сам протокол от этого не защищает, на практике проблему решают выделенным лидером и рандомизированной задержкой перед повторной попыткой, чтобы предлагающие не стартовали синхронно.
Частичная синхронность - не единственный выход из FLP. Второй путь: отказаться от детерминизма. Рандомизированные протоколы вроде Ben-Or подбрасывают монету там, где детерминированный алгоритм застревает, и завершаются с вероятностью 1 - но без верхней границы на число раундов. Гарантия «когда-нибудь почти наверняка» плохо ложится на систему, от которой ждут ответа за десятки миллисекунд, поэтому в инфраструктуре общего назначения победили таймауты. Рандомизация осталась там, где таймаутам верить нельзя в принципе - в византийских протоколах.
Это разделение справедливо для всех трёх алгоритмов статьи. Ни Paxos, ни Raft, ни Zab не обходят FLP, они лишь по-разному выбирают, где поставить таймауты и как быстро сходиться к единственному лидеру.
Фазы протокола Paxos и Multi-Paxos
Базовый Paxos (иногда называемый Single-Decree Paxos) состоит из двух фаз протокола с обменом сообщениями между предлагающими и акцепторами. Эти фазы зачастую обозначают как Prepare (подготовка) и Accept (принятие):
Фаза 1: Prepare (подготовка). Предлагающий выбирает уникальный номер предложения n выше всех использованных ранее и рассылает запрос Prepare(n) всем акцепторам. Акцептор, получив Prepare с номером больше всех ранее увиденных, отвечает согласием Promise(n) и обещает не принимать предложения с меньшим номером. Он также сообщает предлагающему, какое значение v_a с каким номером n_a он уже принял, если принял.
Фаза 2: Accept (принятие). Получив ответы большинства акцепторов (кворума) на фазе Prepare, предлагающий определяет значение для принятия, и выбора у него нет. Если хотя бы один акцептор в кворуме сообщил о ранее принятом значении, предлагающий обязан взять то из них, у которого номер
n_aмаксимален. Собственное значение он вправе предложить, только если в кворуме не принято ничего. Отступление от этого правила ломает безопасность: предложение с большим номером затрёт уже выбранное решение. Единственность решения следует из пересечения кворумов: любые два большинства имеют хотя бы один общий акцептор, а он не может принять два разных значения, не нарушив своё обещание. Если значение уже было выбрано, любое предложение с бо́льшим номером обязано предлагать именно его - это и не даёт решению потеряться при смене лидера. Поэтому правило выбора значения в фазе 2 сформулировано как обязанность, а не как оптимизация.
Для последовательности решений (например, записи журнала команд) базовый Paxos применяется многократно для разных «номеров слотов» журнала. На практике используется оптимизированный режим Multi-Paxos, при котором после первоначального раунда один узел выполняет роль лидера (координатора) для всех последующих записей, пока не произойдет сбой. Лидер, получив клиентские операции, сам выступает предлагающим для новых значений, и благодаря доверию от акцепторов может пропускать фазу Prepare для каждого значения, сразу рассылая Accept-запросы (то есть решения принимаются быстрее, аналогично непрерывной работе с одним координатором). Так достигается эффективность, близкая к репликации с одним ведущим узлом: сообщения фаз Prepare редки, пока лидер не меняется.
Отдельного механизма выборов в Paxos нет, и это принципиальное отличие от Raft и Zab. Лидерство здесь возникает де-факто: лидером оказывается тот предлагающий, чьё предложение с наибольшим номером прошло фазу Prepare на кворуме. Ничто не мешает двум узлам считать лидером себя одновременно - безопасность от этого не страдает, страдает только прогресс. Поэтому все практические реализации Multi-Paxos достраивают протокол сверху: назначают координатора административно, выбирают его детектором отказов или отдельным протоколом лидерства, добавляют аренду (lease) с таймаутом. Что именно делать - стандарт не говорит, и каждая система решает по-своему. Этот незакрытый зазор и есть та часть «сложности Paxos», о которой обычно говорят: сам алгоритм короткий, а всё, что нужно вокруг него для рабочей системы, не описано.
При сетевом разделе система на Paxos сохраняет консистентность ценой доступности: меньшая часть кластера перестаёт продвигаться, но ничего противоречивого не запишет. Разрыв между строгим ядром и недосказанной обвязкой и породил Raft - попытку описать не только алгоритм, но и всё, что вокруг него нужно для работающей системы.
Обработка сбоев и восстановление в Paxos
Алгоритм Paxos устойчив к отказам части узлов: пока работоспособно хотя бы большинство акцепторов, новое значение может быть выбрано. Если текущий лидер (координатор Multi-Paxos) выходит из строя или теряет связь, другие предлагающие могут инициировать новые раунды Prepare с более высоким номером предложения, что фактически означает выбор нового лидера. За счёт системы кворумов старый и новый лидеры никогда не смогут каждый обеспечить большинство голосов одновременно, поэтому консистентность не нарушается. Paxos допускает длительные остановки (например, если менее половины узлов доступны, прогресс приостанавливается, но ранее достигнутое решение не отменится). После восстановления узлы синхронизируют свои журналы на основе принятых значений: уже принятые (committed) операции будут доставлены от обучающихся или лидера. Протокол требует, чтобы каждый акцептор хранил на диске свой последний обещанный номер n_p и принятое значение v_a с номером n_a. Это гарантирует, что узел, восстановившись, не нарушит ранее данные обещания не голосовать за «старые» предложения. Таким образом, Paxos обеспечивает живучесть данных: ни сбои узлов, ни произвольные задержки сообщений не приводят к противоречиям – система либо продлевает время выбора, либо приостанавливается до восстановления кворума, но не принимает некорректных решений.
Пример псевдокода Paxos
Ниже приведён упрощённый псевдокод работы ключевых ролей Paxos – предлагающего и акцептора – демонстрирующий описанные фазы алгоритма (на основе классического описания):
function proposer(node_id, value): round = last_round_seen + 1 n = (round, node_id) send Prepare(n) to all acceptors # --- Фаза 1 --- wait for Promise/Nack with timeout if received Nack(n_p_other): last_round_seen = max(last_round_seen, n_p_other.round) # иначе дуэль proposer'ов до бесконечности sleep(randomized_backoff) retry if not quorum_of(Promise): sleep(randomized_backoff); retry # --- Выбор значения --- # Обязаны переиспользовать уже принятое значение, иначе теряется safety. accepted = [r for r in promises if r.v_a is not None] if accepted: # сравнение ПАР, не чисел v = v_a of response with maximal n_a else: v = value # --- Фаза 2 --- send Accept(n, v) to all acceptors wait for Accepted/Nack with timeout if quorum_of(Accepted(n)): send Decided(v) to learners else: sleep(randomized_backoff); retry with higher round # Акцептор (Acceptor) - хранит состояния n_p, n_a, v_a on receive Prepare(n): # предложение новее всех ранее виденных if n > n_p: n_p = n fsync(n_p) # сообщаем свое последнее принятое значение (если было) reply Promise(n, n_a, v_a) else: reply Nack(n_p) on receive Accept(n, v): # предложение не "устарело" if n >= n_p: n_p = n n_a = n # принимаем новое значение с номером n v_a = v fsync(n_p, n_a, v_a) # подтверждаем принятие reply Accepted(n) else: reply Nack(n_p)
Этот протокол обеспечивает, что акцептор, обещавший не принимать старые номера, не примет значение от старого лидера после того, как увидел более новое предложение. А предлагающий, узнав о ранее принятых значениях (через v_a), повторно предлагает их, чтобы не потерять уже подтверждённое. В итоге достигается единогласие: как только большинство акцепторов примет какое-либо значение, оно становится решением.
Raft: тот же результат, но с явным лидером
Raft (Diego Ongaro, John Ousterhout, 2014) был создан как более простой для понимания альтернативный консенсус-алгоритм, обеспечивающий те же свойства, что и Paxos. Raft также относится к классу алгоритмов повторяющегося консенсуса для реплицированного журнала (replicated log) – т.е. поддерживает реплицированную машину состояний: все узлы применяют одни и те же команды в одном порядке, благодаря чему состояние на них остаётся согласованным. В отличие от классического Paxos, Raft явно разделяет подзадачи: (1) выбор лидера, (2) репликация логов и (3) безопасность (safety). Основная идея Raft – всегда иметь явно выбранного лидера, через которого проходят все записываемые изменения, что упрощает понимание протокола.
В кластере Raft каждый узел может находиться в одном из трёх состояний: Follower (ведомый), Candidate (кандидат) или Leader (лидер). В нормальном режиме лидер один, остальные узлы - follower'ы, пассивно реплицирующие его журнал. Если лидер выходит из строя или связь с ним теряется, узлы переходят к выборам нового лидера. Ниже показана схема состояний узла в Raft и переходов между ними при выборах лидера:

Выбор лидера в Raft
Raft добивается согласованного выбора лидера с помощью раундов голосования, называемых термы (term). Термы пронумерованы и хранятся на каждом узле. В начале работы все узлы – Follower'ы и не имеют лидера. Каждый Follower запускает таймер случайной продолжительности (election timeout). Если таймаут истёк, не получив heartbeat-сообщения от лидера, узел считает, что лидера нет, и сам переходит в состояние Candidate, увеличивает свой терм и начинает выборы нового лидера. Кандидат голосует за себя и рассылает всем остальным узлам запрос RequestVote с указанием своего терма и индекса последней записи в своём журнале. Каждый узел (Follower), получив такой запрос, принимает решение дать голос или отказать, в соответствии с правилами:
Голосующий Follower сравнивает терм кандидата со своим текущим. Если терм кандидата меньше (устарел) – голос отклоняется. Если терм кандидата не меньше, Follower обновляет у себя текущий терм и может проголосовать.
Каждый Follower имеет право голоса только за одного кандидата в рамках одного терма (он запоминает
votedFor= candidateId). Повторные запросы от других кандидатов в том же терме отклоняются.Follower также проверяет «актуальность журнала» кандидата: голосует только если журнал кандидата не отстаёт от его собственного (по индексу и терму последней записи). Это гарантирует, что выбран будет кандидат с наиболее продвинутым журналом, что облегчает последующую синхронизацию логов.
Кандидат, собравший голоса большинства узлов (кворум > N/2), побеждает и становится новым лидером (переходит в состояние Leader). С этого момента он отправляет всем остальным периодические сообщения Heartbeat (пустые AppendEntries RPC) с целью уведомить их о своём лидерстве и предотвратить новые выборы. Выборы могут завершиться неуспешно в двух случаях. Либо кандидат не набрал большинство - например, голоса разделились между несколькими кандидатами, тогда он ждёт нового таймаута и начинает следующий терм. Либо он получил AppendEntries от действующего лидера с термом не меньше своего, тогда он возвращается в Follower и признаёт этого лидера. Использование рандомизированных таймаутов предотвращает ситуации вечной ничьи – обычно один из узлов успеет первым запустить выборы. Выборы в Raft проходят быстро (за время порядка двух таймаутов) и гарантируют, что в каждый момент не появится два лидера в одном терме (благодаря голосованию по большинству и правилу одного голоса). Каждый новый терм отмечается в логах, записи предыдущих термов могут быть не завершены, но Raft гарантирует, что они не повлияют на консистентность.
Репликация журнала и коммит записи
Как только выбран лидер, все клиентские запросы на изменение состояния проходят через него. Лидер принимает, например, команду «записать значение X», добавляет соответствующую запись в свой журнал (log) локально и присваивает ей текущий терм и следующий порядковый индекс. Далее лидер рассылает RPC AppendEntries своим follower'ам с новыми записями. В запрос AppendEntries лидер указывает предыдущий индекс и терм для контроля целостности последовательности. Каждый Follower, получив такой запрос, проверяет: если у него в журнале нет записи с указанным предыдущим индексом и термом (т.е. локальный журнал отстаёт или содержит конфликтующее окончание), то Follower отказывает в AppendEntries. Этот механизм, вместе с проверкой актуальности журнала при голосовании, обеспечивает сходимость журналов: рано или поздно журналы всех узлов станут префиксно-одинаковыми последовательностями команд.
После того как запись реплицирована на большинстве узлов (лидер получил подтверждения AppendEntries от кворума), лидер может пометить её как зафиксированную (committed) но только если эта запись принадлежит его текущему терму. Подсчёт реплик сам по себе права на коммит не даёт. Запись, унаследованную от предыдущего лидера, нельзя коммитить по числу копий даже находясь на большинстве узлов, она всё ещё может быть перезаписана будущим лидером. Такие записи фиксируются косвенно: как только лидер закоммитил хотя бы одну запись своего терма, весь предшествующий префикс журнала считается закоммиченным автоматически, это следует из свойства совпадения логов. Поэтому на практике новый лидер сразу после избрания дописывает в журнал пустую no-op запись: она разблокирует коммит унаследованного хвоста, не дожидаясь клиентских операций.
Зафиксированная запись считается окончательно применённой лидер выполняет команду в своей машине состояний и возвращает результат клиенту. Также в каждом AppendEntries лидер передаёт индекс последнего зафиксированного им entry (commitIndex), получив такое сообщение, follower тоже помечает у себя соответствующие записи как зафиксированные. Поэтому момент, когда запись становится видимой, на разных узлах не совпадает, и сама по себе репликация на большинство линеаризуемости чтений не даёт. Raft отдаёт приоритет консистентности над доступностью: если недостаточно узлов для кворума (например, сеть разделилась и у лидера нет связи с большинством), лидер не сможет продвинуть commitIndex - система приостанавливает обработку изменений, сохраняя ранее достигнутую согласованность. В терминах CAP это CP-система, но сама рамка тут довольно грубая: она описывает поведение только в момент раздела, а раздел - событие редкое. Всё остальное время, когда сеть исправна, выбор никуда не девается, просто он проходит между задержкой и согласованностью: кворум из трёх датацентров согласован, но каждая запись оплачена межрегиональным round-trip. Это фиксирует расширение PACELC - при разделе (P) выбирай между A и C, иначе (E) между latency (L) и consistency (C). Дальше в статье видно, что решают вторую половину: ReadIndex против lease read, sync в ZooKeeper против чтения из локальной реплики, уровни консистентности Cosmos DB.
Зато чтения можно обслуживать без записи в лог, но не наивно «с лидера»: изолированный лидер не знает о своём смещении и вернёт устаревшие данные. Для линеаризуемого чтения нужен ReadIndex (подтверждение лидерства раундом heartbeat перед ответом) либо lease read с опорой на ограниченный дрейф часов, чтение прямо с follower'а допустимо, только если приемлемо отставание.
Raft использует более жёсткую модель лидерства, чем Paxos: лидер полностью управляет потоком команд, Follower'ы не участвуют в предложении новых значений. Это упрощает понимание - система сводится к классической репликации ведущий-ведомый, дополненной безопасным переизбранием лидера в случае сбоя. Свойство совпадения логов Raft: если две записи на разных узлах имеют одинаковый индекс и терм, то гарантировано весь префикс журнала до этого индекса совпадает. Это достигается механизмом отката конфликтующих записей на Follower'ы: если запись на follower не соответствует по терму записи на лидере (означает, что в прошлом она была записана под руководством другого лидера, который не смог довести её до commit), лидер заставит follower удалить эту «осиротевшую» часть журнала и заменить записями из своего журнала. Тем самым консистентность логов восстанавливается.
Одного согласованного журнала для корректной работы мало, и это регулярно упускают в самодельных реализациях. Клиент отправил команду, лидер закоммитил её и упал, не успев ответить. Клиент по таймауту повторяет запрос новому лидеру - и команда применяется второй раз. Для «установить X = 5» безобидно, для «списать 100 рублей» нет. Raft решает это на уровне машины состояний, а не журнала: каждый клиент получает уникальный идентификатор и нумерует свои запросы. Машина состояний хранит для каждого клиента номер последней применённой команды и её результат. Повторный запрос с уже виденным номером не применяется - возвращается сохранённый ответ. Журнал при этом содержит обе копии команды, дублирование отсекается при применении. Записи о сессиях приходится чистить по таймауту, иначе таблица растёт бесконечно, а истёкшие сессии обязаны отклоняться явной ошибкой, а не молча.
Обработка сбоев и восстановление в Raft
Raft спроектирован так, чтобы сохранять безопасность (не противоречить уже зафиксированным записям) даже при произвольных отключениях узлов, сетевых задержках или разделениях. Отказоустойчивость достигается, пока работоспособно большинство узлов, то есть ⌊N / 2⌋ + 1. Это минимально необходимое условие: два непересекающихся множества узлов не должны иметь возможность независимо принять решения, а пересечение любых двух большинств гарантировано непусто. При падении лидера обнаружение происходит через таймаут Follower'ы – как описано, они инициируют выборы нового лидера. Каждый узел хранит на диске currentTerm и votedFor, а также весь свой журнал. Это означает, что узел, перезагрузившись, не «забывает» о том, кому он отдал голос в данном терме, а также не теряет записанные записи журнала. Как отмечено выше, лидер не коммитит записи предыдущих термов по числу реплик - только косвенно, через коммит записи собственного терма. Это правило вместе с проверкой актуальности журнала при голосовании гарантирует, что новый лидер всегда содержит все записи, зафиксированные его предшественниками. Благодаря этому, новый лидер продолжит с корректного состояния.
В случае, если узел отстал (например, долго был недоступен), механизм догонки журнала позволяет ему нагнать текущее состояние. Лидер в сообщениях AppendEntries указывает prevLogIndex/prevLogTerm для проверки, и если у отставшего follower обнаружится рассинхрон, он получит отказ AppendEntries. Лидер будет понижать prevLogIndex, пока не найдёт общую точку с журналом follower'a. Если нужные записи лидер уже отбросил при сжатии собственного журнала, поиск общей точки невозможен, тогда вместо AppendEntries отправляется InstallSnapshot (см. ниже о сжатии журнала).
Raft включает встроенный механизм сжатия журнала (log compaction). Снапшоты делает каждый узел независимо: это не операция лидера и не согласуемое через консенсус решение. Узел берёт текущее состояние своей машины, записывает его на диск вместе с индексом и термом последней включённой в снимок записи, после чего отбрасывает весь предшествующий префикс журнала. Момент снятия снапшота узел выбирает сам, обычно по размеру журнала. Это предотвращает неограниченный рост лога и ускоряет перезапуск узла: он восстанавливается из своего снимка, а не проигрывает журнал с нуля.
Если нужные follower'y записи уже отброшены, лидер отправляет ему InstallSnapshot RPC: свой снимок целиком (при необходимости порциями), после чего follower заменяет им своё состояние и продолжает получать обычные AppendEntries.
Также Raft определяет процедуру безопасного изменения состава кластера через совместный консенсус: вводится переходная конфигурация C_old,new, в которой каждое решение требует большинства отдельно в старом и отдельно в новом составе - не увеличенного большинства в объединении, а именно двух независимых большинств. Это исключает появление двух лидеров в момент перехода. После коммита C_old,new лидер фиксирует C_new, и старая конфигурация выводится из игры. На практике чаще используют более простой механизм из диссертации Онгаро - добавление или удаление ровно одного узла за раз, при котором большинства старой и новой конфигураций пересекаются автоматически и переходная конфигурация не нужна.
В результате Raft предоставляет разработчикам относительно простую модель: единственный лидер, реплицирующий команды на Follower'ы. Он гарантирует линеаризуемость операций (при использовании ReadIndex или lease read) и сохраняет доступность при отказе до ⌊(N−1)/2⌋ узлов, то есть кластер из 3 узлов переживает отказ одного, из 5 - двух, из 7 - трёх.
Отсюда практическое следствие: чётное число узлов не даёт выигрыша. Кластер из 4 узлов требует кворума в 3 и потому переживает отказ лишь одного узла - ровно как кластер из 3, но при этом каждая запись должна дойти до большего числа реплик, а вероятность отказа хотя бы одного узла выше. Поэтому Raft-кластеры почти всегда разворачивают нечётными: 3, 5 или 7 узлов. Больше 7 берут редко: отказоустойчивость растёт медленно, а латентность коммита определяется самым медленным узлом кворума. Реализации Raft показывают производительность порядка тысяч-десятков тысяч операций в секунду на кластер в зависимости от нагрузки и оборудования. Упирается всё обычно в скорость fsync на диске лидера и сетевую задержку до кворума, а не в свойства самого алгоритма.
Пример реализации Raft (фрагмент кода)
Ниже приведён упрощённый пример обработки RPC запроса голосования (RequestVote) на языке Python, иллюстрирующий логику выбора лидера в Raft. Каждый узел хранит current_term, идентификатор voted_for (кому отдал голос в текущем терме или None), а также индекс и терм последней записи журнала (last_log_index, last_log_term):
class RaftNode: def __init__(self, node_id, storage): self.node_id = node_id self.storage = storage # --- персистентное состояние --- self.current_term = 0 self.voted_for = None self.log = [] # --- волатильное --- self.state = "follower" def _persist(self): self.storage.save(current_term=self.current_term, voted_for=self.voted_for) self.storage.fsync() def _last_log(self): if not self.log: return (0, 0) return (len(self.log), self.log[-1].term) def _step_down(self, term): self.current_term = term self.voted_for = None self.state = "follower" def _log_is_up_to_date(self, cand_index, cand_term): my_index, my_term = self._last_log() if cand_term != my_term: return cand_term > my_term return cand_index >= my_index # ---------- RPC ---------- def on_request_vote(self, term, candidate_id, last_log_index, last_log_term): term_changed = False # 1. Устаревший терм - отказ. Свой терм возвращаем всегда, # чтобы кандидат мог понизиться сам. if term < self.current_term: return (self.current_term, False) # 2. Терм больше своего - обновляем терм и ОБНУЛЯЕМ voted_for # ДО проверки права голоса. Пропустить сброс здесь # классическая ошибка: узел навсегда откажет всем # кандидатам следующих термов. if term > self.current_term: self._step_down(term) term_changed = True # 3. Голосуем, если ещё не голосовали в этом терме # (или это повторный запрос от того же кандидата) # и журнал кандидата не отстаёт. granted = False if (self.voted_for in (None, candidate_id) and self._log_is_up_to_date(last_log_index, last_log_term)): self.voted_for = candidate_id granted = True self.reset_election_timer() # 4. Персистим, если что-то изменилось до ответа по сети. if term_changed or granted: self._persist() return (self.current_term, granted)
Zab: протокол, написанный под один продукт
Zab (ZooKeeper Atomic Broadcast) – протокол консенсуса, разработанный специально для системы Apache ZooKeeper. По замыслу, Zab обеспечивает атомарную (неделимую) широковещательную рассылку сообщений с тотальным порядком и устойчивостью к отказам. Это означает, что все узлы ZooKeeper применяют все транзакции (изменения состояния) в одном и том же порядке – в итоге их состояния идентичны. Zab близок по целям к Paxos, но спроектирован как узкоспециализированный протокол мастер-репликации (primary-backup) с упором на простоту реализации в реальном продукте - и появился на несколько лет раньше Raft, решая ту же проблему с Multi-Paxos независимо от него. Он использует модель с одним лидером (primary) и набором последователей (followers) - ту же схему, к которой позже независимо пришёл Raft. Zab работает в четырёх фазах, и их стоит различать, потому что в описаниях их часто сливают. Leader Election - узлы договариваются, кто будет лидером. Discovery - избранный лидер собирает у кворума их последние эпохи, вычисляет новую и добивается, чтобы кворум её признал. Synchronization - лидер приводит журналы последователей в соответствие со своим: дошлёт недостающее, заставит откатить лишнее. И только после этого Broadcast - приём клиентских запросов. Первые три фазы вместе называют восстановлением, в реализации ZooKeeper Discovery и Synchronization частично слиты, но логически это разные шаги: первый устанавливает эпоху, второй выравнивает историю. Пока кластер имеет лидера и кворум живых узлов, все клиентские операции последовательно проходят через лидера и распространяются на последователей.
Восстановление: выборы, эпоха, синхронизация
Процесс запуска или восстановления кластера начинается с выборов. ZooKeeper использует для этого FastLeaderElection: узлы обмениваются голосами и сравнивают кандидатов по кортежу (epoch, zxid, sid) - сначала эпоха, при равенстве последний zxid, и лишь при полностью совпадающей истории побеждает узел с бо́льшим идентификатором. В итоге лидером становится узел с самой полной историей среди кворума, а sid нужен только чтобы разрешить ничью детерминированно. Требование строгого максимума отличает Zab от Raft, где достаточно журнала «не хуже» - к этому вернёмся ниже. После выборов один узел становится лидером, остальные его последователями.
Discovery: установление эпохи. Лидер не начинает сразу принимать операции - сначала он должен закрепить за собой новую эпоху. Он собирает у последователей их последние известные эпохи (сообщение CEPOCH), берёт максимум, увеличивает на единицу и рассылает результат обратно (NEWEPOCH). Последователь, принявший новую эпоху, тем самым обязуется не принимать ничего от лидеров прежних эпох. Когда новую эпоху признал кворум, у прежнего лидера, даже если он ещё жив и не знает о своей отставке, больше нет возможности что-либо закоммитить, его предложения будут отвергнуты как устаревшие.
Это ровно та работа, которую в Paxos делает фаза Prepare. Разница в частоте: Paxos проводит её для каждого значения (кроме оптимизированного Multi-Paxos), Zab - один раз на смену лидера.
Synchronization: выравнивание журналов. Только теперь лидер приводит историю кворума к общему виду. Каждый узел хранит последний zxid - идентификатор транзакции, состоящий из номера эпохи и счётчика внутри неё. Лидер запрашивает у каждого последователя его zxid и сверяет со своим журналом:
Если у последователя не хватает каких-то транзакций, лидер посылает ему недостающие записи журнала, чтобы подтянуть его до своей последней транзакции.
Если у последователя есть «лишние» записи (с более высокими
zxid, что может быть результатом работы предыдущего лидера, не успевшего подтвердить эти записи), то лидер указывает последователю откатить эти несогласованные изменения. Это необходимо, так как раз они не были зафиксированы большинством ранее, то они не должны влиять на состояние системы после восстановления.
Когда лидер и follower достигли одинакового последнего zxid, лидер включает этого последователя в число синхронизированных. Собрав кворум таких, он рассылает NEW_LEADER - сигнал, что история выровнена и можно работать. С этого момента все последователи в кворуме имеют идентичный префикс истории, и лидер переходит к фазе broadcast.
Порядок здесь не произволен. Сначала эпоха, потом журналы - потому что откат чужих записей это разрушительная операция, и право на неё нужно получить заранее. Если бы лидер начал выравнивать историю до того, как кворум признал его эпоху, он мог бы удалить транзакции у последователей, а затем сам оказаться отставленным - и данные, за которые уже отчитались клиенту, исчезли. Признание эпохи кворумом это гарантия, что происходящее дальше не будет отменено более старым лидером.
Атомарная широковещательная рассылка (Broadcast)
На фазе broadcast текущий лидер принимает клиентские запросы (транзакции, например изменения узлов ZK) и распределяет их всем последователям. Каждый новый запрос получает уникальный увеличивающийся zxid (состоящий из номера эпохи лидера и счетчика). Лидер посылает предложение (proposal) с этим zxid всем followers (по сути, запись транзакции). Последователи, получив предложение, записывают его в свой лог и отправляют лидеру подтверждение (ACK). Когда лидер собирает ACK от кворума узлов, он рассылает всем команду коммита для данного zxid - с этого момента транзакция применяется на всех. Структурно это половина Paxos: proposal играет роль Accept-запроса, а фаза Prepare не нужна, потому что лидер уже установил свою эпоху на этапе восстановления - ровно как Multi-Paxos пропускает Prepare, пока лидер не меняется. Отдельного сообщения о коммите в базовом Paxos нет, уведомление learners частью протокола не считается, в Zab это явный шаг. В ZooKeeper все изменения проходят через такое согласование, что обеспечивает линейный порядок: если одна транзакция A была отправлена раньше B и подтверждена, то все узлы применят A перед B. Заботится об этом лидер, упорядочивая исходящие предложения, а общая очередь подтверждения поддерживается требованием кворума.
Zab гарантирует надёжную доставку (каждая подтверждённая лидером транзакция дойдёт до всех корректных узлов) и тотальный порядок доставки. Кроме того, соблюдается каузальный порядок: если транзакция B отправлена после доставки A на том же узле-инициаторе, то B будет упорядочена после A. Эти гарантии соответствуют семантике ZooKeeper: все узлы видят изменения в одном порядке, а клиент, последовательно отправляющий запросы, видит их в порядке отправки.
Почему ZooKeeper не взял Paxos
Напрашивающийся вопрос: зачем писать свой протокол, если Multi-Paxos решает ту же задачу. Ответ в том, что ZooKeeper нужна гарантия, которой Paxos не даёт.
Paxos обеспечивает согласие по каждому слоту журнала независимо. Слоты могут заполняться в произвольном порядке, и один лидер вправе начать работу над слотом 5, не дожидаясь исхода по слоту 4. Для базы данных, применяющей команды по готовности всего префикса, это нормально. Для ZooKeeper - нет: его транзакции инкрементальны и записаны относительно состояния. Запись «увеличить версию znode /config с 7 до 8» осмысленна, только если состояние действительно на версии 7. Переставь две такие транзакции местами или потеряй одну из середины - и результат будет не «немного другим», а бессмысленным.
Поэтому Zab требует primary order - два условия сверх обычного тотального порядка. Первое: предложения одного лидера доставляются в том порядке, в котором он их выдал, без пропусков в середине. Второе: если лидер эпохи e мог видеть транзакцию - то есть она была предложена в эпохе меньше e и потенциально видима, - то она упорядочена раньше всего, что предлагает он сам. Ради второго условия фаза Discovery выбирает лидером узел с максимальным zxid и заставляет остальных откатить хвосты, которых у лидера нет.
Отсюда и отличие в правилах выборов. Raft голосует за кандидата, чей журнал «не хуже» журнала голосующего, и допускает, что победит не самый продвинутый узел из всех - лишь бы он содержал все зафиксированные записи, недостающее у остальных он потом просто затрёт. Zab требует у лидера строго максимальный zxid среди кворума и переносит выравнивание в отдельную фазу до начала работы. Оба подхода безопасны, но распределяют работу по-разному: Raft быстрее переходит к обслуживанию клиентов и выравнивает журналы на ходу, Zab делает это заранее.
Отказоустойчивость и восстановление в Zab
Механика восстановления разобрана выше, практический итог такой: транзакция, застигнутая падением лидера, либо восстанавливается, если успела дойти до большинства, либо отбрасывается. Монотонно растущий номер эпохи внутри zxid играет ту же роль, что терм в Raft и номер предложения в Paxos, он не даёт прошлому лидеру вмешаться в работу нового.
Zab писался под конкретную реализацию, и это видно по деталям. Лидер записывает транзакцию на диск до рассылки предложения, а не после - иначе он мог бы подтвердить клиенту то, чего сам не переживёт. Предложения и ACK батчатся: при высокой нагрузке за один fsync проходит десяток транзакций, и пропускная способность на практике определяется этим батчингом. Обратная сторона такой заточенности - узость. Zab предполагает единственного лидера и полную реплику на каждом узле, и за пределами координационных сервисов почти не встречается: это протокол одного продукта, а не переиспользуемый компонент.
Сравнительная таблица алгоритмов
Три протокола решают одну задачу и расходятся в том, где платить. Сведём различия, разобранные выше, дальше пойдёт речь о том, где каждый из них применяется на практике.
Paxos (Multi-Paxos) | Raft | Zab | |
|---|---|---|---|
Выборы лидера | Механизма нет, лидерство де-факто, достраивается реализацией | Голосование по термам, рандомизированные таймауты | FastLeaderElection, отдельная фаза |
Требование к лидеру | Любой предлагающий, прошедший Prepare на кворуме | Журнал не хуже, чем у голосующего | Строго максимальный zxid среди кворума |
Кто выравнивает журналы | Лидер переоткрывает незакрытые слоты по мере надобности | Лидер затирает расхождения на ходу, во время работы | Отдельная фаза до начала обслуживания |
Раундов на запись | 1 (Prepare пропускается, пока лидер стабилен) | 1 (AppendEntries + ACK) | 1 (proposal + ACK), плюс явный commit |
Порядок записей | Слоты независимы, возможны дыры | Строгий префикс, дыр нет | Строгий префикс + primary order |
Записи прошлой эпохи | Переоткрываются с новым номером | Не коммитятся по числу реплик, только косвенно - через коммит записи своего терма | Восстанавливаются или откатываются в фазе Synchronization |
Смена состава | Не специфицирована | Joint consensus либо «по одному узлу» | Динамическая реконфигурация (ZK 3.5+) |
Основные носители | Spanner, Chubby, Ceph Monitors | etcd, Consul, CockroachDB, TiKV, KRaft | Только ZooKeeper |
Области применения Paxos, Raft, Zab в реальных системах
Дальше - где эти протоколы работают на практике, по алгоритмам. Сами задачи при этом почти всегда одни и те же: хранилище конфигурации кластера, координатор с выбором мастера и блокировками, репликация данных внутри СУБД. Различается лишь то, какой протокол оказался под рукой в момент, когда систему писали.
Paxos в сервисах Google и распределённых базах данных. Paxos стал основой для ряда внутренних систем Google. В частности, глобально распределённая база данных Google Spanner применяет Multi-Paxos для синхронной репликации между датацентрами. Каждый фрагмент данных (split) хранится с несколькими репликами, образующими отдельную Paxos-группу со своим лидером, при сбое лидера группа выбирает нового, и сервис остаётся доступен. Однако внешняя согласованность (external consistency, то есть строгая сериализуемость с соблюдением реального порядка транзакций глобально) из одного лишь Paxos не следует. Paxos упорядочивает операции внутри одной группы реплик, а транзакция в Spanner может затрагивать несколько групп - их координирует двухфазный коммит поверх Paxos-групп. Порядок же между транзакциями, которые вообще не пересекаются по данным и выполняются в разных регионах, обеспечивает TrueTime - API времени с явно ограниченной погрешностью, построенный на атомных часах и GPS-приёмниках в датацентрах. TrueTime возвращает не момент, а интервал
[earliest, latest], гарантированно содержащий реальное время. На этом строится приём commit wait: транзакция, получив коммит-таймстемп, выжидает, пока интервал неопределённости не истечёт, и лишь затем считается зафиксированной. Так гарантируется, что транзакция, начавшаяся после завершения другой, получит строго больший таймстемп. Без TrueTime Spanner был бы обычной Paxos-репликацией, и упоминание его как примера глобальной консистентности теряло бы смысл.Ещё один известный пример - Chubby, распределённый сервис блокировок от Google, также реализует консенсус на основе Paxos. Chubby хранит небольшие, но критически важные данные конфигурации (например, координаты мастер-узлов) и использует Paxos для высокой доступности: даже при сбое узлов, остаётся консистентное хранилище для клиентов. Paxos применяется и в репликации метаданных распределённых файловых систем - правда, чаще опосредованно: GFS полагался на Chubby, то есть на Paxos через внешний сервис блокировок, а не реализовывал его сам. HDFS пошёл другим путём и использует для журнала правок собственный кворумный механизм - Quorum Journal Manager. Формально это не Paxos, но процедура восстановления в нём построена на той же идее: новый писатель сначала закрепляет за собой номер эпохи на кворуме JournalNode'ов и лишь затем выравнивает историю. Разница скорее в объёме задачи: QJM согласовывает не произвольные значения, а последовательность сегментов журнала при единственном активном писателе. Из файловых систем Paxos напрямую использует Ceph - в сервисе мониторинга (Ceph Monitors), для согласования карты кластера.
Отдельный случай когда консенсус не как фундамент системы, а как разовая операция. Cassandra построена на eventual consistency и лидера не имеет, но для условных записей («вставить, только если такого ключа ещё нет») ей нужно настоящее согласие: два клиента не должны одновременно решить, что ключа нет. Механизм Lightweight Transactions проводит для каждой такой операции полноценный раунд Paxos поверх реплик конкретного ключа - четыре круга сообщений против одного при обычной записи. Цена настолько заметна, что LWT применяют точечно, а не как режим по умолчанию, в 4.1 её пытались сбить переписанной реализацией Paxos (v2), но природа накладных расходов от этого не изменилась. Радикальное решение - Accord, протокол без выделенного лидера, дающий транзакцию за один раунд, когда операции не конфликтуют. Он вошёл в ветку 6.0, которая на данный момент (август 2026) существует в статусе альфы, в стабильной 5.0 его нет.
Cassandra здесь показывает границу: консенсус не обязан быть архитектурой всей системы, он может быть локальным инструментом на одну операцию - и в таком виде его стоимость заметна отчётливее всего.
Raft в системах оркестрации, сервис-дискавери и новых СУБД. За десять лет Raft вытеснил Paxos почти из всех новых open-source проектов, которым нужен консенсус.
Самый заметный пример - etcd, где Raft реплицирует лог изменений между узлами. Через etcd проходит всё состояние кластера Kubernetes, так что каждое развёртывание контейнера это, по сути, коммит записи через Raft. Consul использует Raft только для той части данных, которой нужна согласованность: каталог сервисов, ключи конфигурации, сессии и блокировки. Всё остальное живёт вне консенсуса, к этому разделению вернёмся ниже.
В распределённых СУБД Raft лёг в основу репликации на уровне отдельных диапазонов данных. Причина в том, что один Raft по записи не масштабируется: все команды проходят через единственного лидера, и его диск с сетевой картой становятся потолком для всего кластера. Решение - не один Raft на систему, а много: CockroachDB и YugabyteDB держат отдельную группу на каждый диапазон, лидеры разных групп живут на разных узлах, и нагрузка размазывается по кластеру. Каждая группа при этом даёт линеаризуемость внутри своего диапазона и автоматически переключает лидера при сбое узла; транзакции, затрагивающие несколько диапазонов, координируются отдельно, поверх групп. TiKV (хранилище под TiDB) устроен так же, а его реализация Raft вынесена в самостоятельную библиотеку raft-rs на Rust, её переиспользуют и сторонние проекты.
Zab и ZooKeeper в системах координации. Zab используется практически только внутри ZooKeeper - это протокол, написанный под конкретный продукт. Зато сам ZooKeeper десятилетиями был точкой опоры для экосистемы Hadoop и Apache: на нём строились распределённые блокировки, выбор мастера, конфигурационные узлы и очереди. Более десяти лет он служил координатором для Apache Kafka: хранил сведения о брокерах, конфигурацию топиков и разделов, обеспечивал выбор контроллера кластера и гарантировал, что лидером конкретного раздела является ровно один брокер. Эта зависимость уже устранена. В рамках KIP-500 Kafka получила собственный встроенный консенсус KRaft, где выделенные controller-узлы образуют кворум и хранят метаданные во внутреннем логе
__cluster_metadata. KRaft был признан готовым к продакшену в версии 3.3 (2022), ZooKeeper-режим объявлен устаревшим в 3.5, а 3.9 стала последней версией с его поддержкой. В Kafka 4.0, вышедшей 18 марта 2025 года, поддержка ZooKeeper удалена полностью, KRaft остался единственным режимом работы. Прямое обновление с ZooKeeper-кластера на 4.0 невозможно: требуется промежуточная миграция на KRaft в одной из версий 3.x. Тем не менее в эксплуатации остаётся большое число установок на 3.x, где ZooKeeper всё ещё используется, так что связка Kafka + ZK будет встречаться на практике ещё долго.Другой пример - HDFS NameNode HA: ZooKeeper используется для автоматического failover между Active- и Standby-NameNode. Работает это через отдельный процесс ZKFailoverController (ZKFC) рядом с каждым NameNode: он держит в ZK эфемерный znode-блокировку, и исчезновение сессии упавшего узла запускает переключение. Сами метаданные при этом в ZooKeeper не хранятся - журнал правок (edit log) реплицируется через Quorum Journal Manager, тот самый механизм с эпохами, о котором шла речь выше. Не следует путать HA с HDFS Federation: последняя решает совсем другую задачу - горизонтальное масштабирование через несколько независимых пространств имён, и ZooKeeper для неё не нужен. В HBase (NoSQL база на Hadoop) ZooKeeper хранит информацию о ведущих регион-серверах.
Где кончается алгоритм
Знание Raft не означает понимания etcd. Протокол определяет, как согласовать журнал между узлами. Всё, что видит пользователь системы - модель данных, гарантии чтений, поведение при потере кворума, границы роста базы лежит за его пределами и решается реализацией. Ниже несколько вопросов, на которые протокол ответа не даёт, а etcd, ZooKeeper и Consul отвечают по-разному.
Кто входит в кворум
Протокол говорит «большинство узлов», но не говорит, какие узлы вообще считаются.
В etcd и ZooKeeper голосуют все члены кластера, и это ставит потолок на его размер: три или пять узлов, больше невыгодно. Consul устроен иначе. Raft-кворум образуют три-пять серверов, а на каждой машине приложения работает агент-клиент, который в консенсусе не участвует: он регистрирует локальные сервисы, выполняет health-check'и, кэширует ответы и проксирует запросы серверам. Кластер из трёх машин и кластер из трёх тысяч - один и тот же Raft.
ZooKeeper пришёл к похожему решению другим путём. Observers получают поток обновлений от лидера, но не голосуют, и их можно ставить в удалённых датацентрах - чтения масштабируются, кворум не замедляется.
Ни того ни другого нет ни в Raft, ни в Zab: это слой поверх протокола, и масштаб системы он определяет сильнее, чем выбор самого протокола.
Сюда же относится модель данных. etcd v3 хранит плоское отсортированное пространство ключей: привычная иерархия /registry/pods/default/nginx - соглашение об именовании, а «получить поддерево» выражается range-запросом по префиксу. ZooKeeper оставил настоящее дерево znodes с эфемерными и последовательными узлами, и на этих примитивах строятся распределённые блокировки, очереди и барьеры. Consul поверх KV держит отдельный каталог сервисов со своей схемой. Три модели данных на двух алгоритмах потому что консенсус согласовывает журнал команд, а что за команды в этом журнале, его не касается.
Что видит читатель
Ни Raft, ни Zab не дают линеаризуемых чтений даром: коммит на кворуме не означает, что запись видна на всех узлах одновременно. Кто отвечает за свежесть и в какой момент - решение продукта, и здесь три системы расходятся сильнее всего.
etcd по умолчанию отдаёт свежее. Обычное чтение проходит через подтверждение лидерства, клиент гарантированно видит результат последней успешной записи. Для тех, кому важнее скорость, есть режим serializable - ответ отдаёт локальная реплика, возможно устаревшая.
ZooKeeper по умолчанию свежести не обещает. Чтение обслуживает любой сервер из своей локальной реплики, поэтому клиент может увидеть состояние, отставшее от последней зафиксированной записи. Гарантируется только то, что он не увидит историю задом наперёд: порядок операций одного клиента сохраняется, однажды увиденная ревизия не откатится. Это sequential consistency, а не линеаризуемость. Разница в том, что линеаризуемость требует не только общего порядка операций, но и его согласованности с реальным временем: подтверждённую запись обязано увидеть любое последующее чтение, с какого бы узла оно ни пришло. ZooKeeper даёт первое и не даёт второго. Чтобы прочитать заведомо свежее, клиент вызывает sync перед чтением. Способность ZooKeeper масштабировать чтения на все узлы держится ровно на этом ослаблении: узлы отвечают локально потому, что не обязаны быть свежими.
Consul выносит выбор на уровень отдельного запроса: consistent идёт через кворум с подтверждением лидерства, stale разрешает ответить любому серверу из своей реплики, а режим по умолчанию - промежуточный: лидер отвечает сразу, опираясь на свой lease, поэтому в теории может отдать устаревшие данные, если он уже смещён, но ещё не знает об этом. Отдельно есть агентское кэширование, но оно включается явным флагом, а не работает по умолчанию.
Практическое следствие простое: код, который читает ключ сразу после чужой записи и рассчитывает её увидеть, работает в etcd и молча ломается в ZooKeeper.
Что происходит при потере кворума
Протокол говорит только «прогресс останавливается». Что при этом делает система, он не описывает.
etcd отвергает записи, а в линеаризуемом режиме и чтения узел не может подтвердить лидерство и отвечает ошибкой. ZooKeeper останавливает записи, но продолжает отдавать чтения из локальных реплик: они и так не были свежими, деградация получается плавной. Consul останавливает запись в каталог, но чтения в режиме stale продолжают обслуживаться серверами, а агенты - выполнять health-check'и локально. Результаты проверок в каталог не попадут, и приложения будут получать всё более устаревшую картину.
Разница здесь в форме отказа. В первом случае приложение получит ошибку и узнает о проблеме сразу. В третьем получит правдоподобный, но устаревший ответ и узнает о проблеме позже, иногда сильно позже.
Что происходит с журналом в проде
Raft описывает механику сжатия журнала, но не политику: когда снимать снапшот, что делать со старыми данными, где границы роста. Это оставлено реализации, и здесь чаще всего ломается эксплуатация.
MVCC, дающий etcd историю ревизий и возможность подписаться на изменения с заданной точки, оборачивается его главной особенностью: старые ревизии не удаляются сами. Их убирает компакция, а освобождённое место возвращает файловой системе только дефрагментация - отдельная операция, блокирующая узел на время выполнения. Без обеих база растёт, и при достижении --quota-backend-bytes (по умолчанию 2 ГБ) кластер уходит в read-only с alarm NOSPACE. Для Kubernetes это означает, что кластер продолжает работать, но перестаёт принимать любые изменения - состояние, из которого выходят вручную.
Второй источник роста в etcd - watch-подписки: каждый контроллер, следящий за ключами, держит поток, и на крупных кластерах их тысячи.
ZooKeeper пишет снапшоты и журнал транзакций на диск, но старые файлы не удаляет: за этим следит либо autopurge в конфигурации, либо администратор. Забытая настройка - классическая причина заполнения диска на ZK-узле.
Что придётся настроить руками
Ни в одной статье про Raft не написано, каким должен быть election timeout. Слишком короткий - кластер переизбирает лидера на каждом всплеске сетевой задержки, слишком длинный - простой при реальном отказе растягивается на секунды. Отправная точка обычно на порядок выше типичного RTT между узлами, дальше подбирается по метрикам.
Второе - диск. Коммит в Raft упирается в fsync на лидере, поэтому SSD обязателен: сетевое хранилище с непредсказуемой задержкой превращает кластер в генератор ложных переизбраний.
Третье - мониторинг. Кластер из трёх узлов, где один давно лежит, выглядит здоровым: кворум есть, записи проходят. Но запаса больше нет, и следующий отказ остановит запись. Наблюдать нужно за числом живых узлов и за задержкой коммита - факт «отвечает или нет» о запасе прочности ничего не говорит.
Чего нет ни у кого
Ни один из трёх алгоритмов не рассчитан на кворум, растянутый между регионами: каждая запись требует сетевого раунда до большинства, и межрегиональная задержка становится задержкой коммита напрямую.
Поэтому в etcd нет встроенной гео-репликации - кластер держат в пределах одного датацентра или близких зон. Consul для multi-DC поднимает независимые Raft-кворумы в каждом датацентре и связывает их слабо, обменом регистрациями сервисов, без общего консенсуса. Глобальная согласованность в системах, которым она нужна, строится не консенсусом, а поверх него: у Spanner - через TrueTime и двухфазный коммит между Paxos-группами, у Cosmos DB - отдельным механизмом межрегиональной репликации с выбираемым уровнем консистентности.
Когда консенсус не нужен
Прежде чем поднимать кластер, стоит проверить, нужен ли он вообще.
Eureka, реестр сервисов Netflix, обходится без консенсуса вовсе: каждый узел принимает записи независимо и реплицирует их остальным best-effort. При разделе сети узлы по обе стороны расходятся, но продолжают отвечать. Для реестра, где клиент всё равно перепроверяет доступность инстанса при обращении, это разумный размен - устаревший список адресов полезнее недоступного реестра.
Тот же приём внутри Consul. Членство узлов в кластере и обнаружение их отказов работают на gossip-протоколе Serf, где рассинхронизация на секунды допустима и обходится дешевле кворума, каталог сервисов при этом пишется через Raft. Граница проходит там, где расхождение перестаёт быть безобидным: то, что узел жив, можно узнать с опозданием, а вот кто владеет блокировкой - нельзя
Консенсус дорог, и зрелые системы применяют его выборочно
Заключение
Практический выбор сегодня выглядит просто. Для нового кода - Raft: реализаций больше, экосистема живее, а понятность реально окупается при отладке. ZooKeeper берут в двух случаях: вы внутри Hadoop-экосистемы или обслуживаете те самые установки Kafka 3.x, о которых шла речь выше - либо вам нужны его примитивы на дереве znodes: эфемерные узлы и watch дают блокировки и очереди почти даром, а собирать это поверх KV-хранилища заметно дороже. Paxos почти не берут в новые открытые проекты. Он живёт внутри больших систем, которые писались до Raft и с тех пор его унаследовали - Spanner, Chubby, Ceph. Это всегда собственная реализация внутри продукта, а не готовый компонент, который можно подключить.
Но перед выбором стоит задать вопрос раньше: нужен ли консенсус вообще. Он покупает согласованность ценой задержки на каждую запись и остановки при потере кворума. Если расхождение на секунды безобидно - gossip дешевле. Если операции коммутативны - CRDT обойдётся без координации. Если нагрузка укладывается в один узел, реплика с ручным переключением честнее кластера из трёх машин, который никто не умеет чинить.
Там же, где консенсус действительно нужен, он всё чаще спрятан внутри чего-то другого: Kubernetes несёт etcd, Kafka несёт KRaft, CockroachDB несёт Raft-группу на каждый диапазон данных. Знать, как это устроено, приходится ради отладки: чтобы понимать, почему кластер встал и что сломалось.

