Розподілені системи · Алгоритми · Комп'ютерні науки
📅 Квітень 2026 ⏱ ≈ 14 хв читання 🎯 Середній–Просунутий · Останнє оновлення: 28 травня 2026 р.

Алгоритм консенсусу Raft — вибори лідера й реплікація журналу

Як кластер серверів узгоджує одну й ту саму послідовність операцій, коли деякі сервери можуть падати, а повідомлення затримуватися? Це фундаментальна задача розподіленого консенсусу — і вона напрочуд складна. Raft, розроблений Дієго Онгаро та Джоном Оустергаутом у 2014 році як альтернатива Paxos, досягає строгої узгодженості у спосіб, явно спроєктований так, щоб бути зрозумілим. Він живить etcd (Kubernetes), CockroachDB, TiKV та багато інших промислових розподілених систем.

1. Задача консенсусу

Розподілена система є узгодженою, якщо всі справні вузли погоджуються щодо однієї й тієї ж послідовності зафіксованих значень (реплікованого журналу). Клієнти надсилають команди; система має забезпечити:

Теорема неможливості FLP (Фішер, Лінч, Патерсон, 1985) доводить, що в повністю асинхронній системі жоден детермінований алгоритм не може гарантувати водночас безпеку й живучість, коли навіть один вузол може відмовити. Практичні системи обходять це за допомогою тайм-аутів (припущення слабкої синхронності) — Raft не виняток.

Вимога кворуму (більшості): з N серверами Raft витримує ⌊(N-1)/2⌋ одночасних відмов. Кластер із 3 витримує 1 відмову; 5 серверів витримують 2. Поступ потребує більшості (N/2 + 1) голосів чи підтверджень. Це запобігає «розщепленню мозку» (split-brain) — коли дві групи одночасно обирають різних лідерів.

2. Ролі серверів і терміни

У будь-який момент кожен сервер Raft перебуває в одному з трьох станів:

Час поділено на терміни — монотонно зростаючі цілі числа. Кожен термін починається з виборів. Якщо кандидат перемагає, він служить лідером до кінця терміну. Якщо жоден кандидат не перемагає (наприклад, розщеплення голосів), термін завершується без лідера й починається новий термін.

Терміни слугують логічними годинниками: - Кожен RPC містить поточний термін відправника. - Якщо сервер отримує повідомлення з терміном T > за власний термін → оновлює термін до T, за потреби перетворюється на послідовника. - Якщо сервер отримує повідомлення з терміном T < за власний термін → відхиляє повідомлення. Це гарантує, що застарілі лідери негайно складають повноваження, коли виявлено новий термін.

3. Вибори лідера

Коли в послідовника спливає тайм-аут виборів (не отримано серцебиття), він переходить у стан кандидата, збільшує свій поточний термін, голосує за себе й надсилає RequestVote RPC усім іншим серверам:

// Аргументи RPC RequestVote
type RequestVoteArgs struct {
  Term         int    // термін кандидата
  CandidateId  int    // кандидат, що просить голос
  LastLogIndex int    // індекс останнього запису журналу кандидата
  LastLogTerm  int    // термін останнього запису журналу кандидата
}

// Голосуючий віддає голос, якщо:
// 1. ще не голосував у цьому терміні (або голосував за цього кандидата)
// 2. журнал кандидата щонайменше такий же актуальний, як журнал отримувача
func handleRequestVote(args RequestVoteArgs) bool {
  if args.Term < currentTerm { return false }
  logUpToDate := args.LastLogTerm > lastLogTerm ||
    (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= lastLogIndex)
  if (votedFor == -1 || votedFor == args.CandidateId) && logUpToDate {
    votedFor = args.CandidateId
    return true
  }
  return false
}

Якщо кандидат отримує голоси більшості серверів, він стає новим лідером і негайно надсилає серцебиття у вигляді RPC AppendEntries, щоб придушити подальші вибори.

Тайм-аут виборів рандомізований (зазвичай 150–300 мс), щоб уникнути розщеплення голосів, коли багато кандидатів стартують одночасно. Якщо розщеплення голосів усе ж стається, усі кандидати вичерпують тайм-аут і починають нові вибори.

4. Реплікація журналу

Щойно обраний, лідер приймає команди клієнтів, додає їх до свого журналу й реплікує послідовникам через RPC AppendEntries:

  1. Клієнт надсилає команду лідеру.
  2. Лідер додає запис до локального журналу з номером поточного терміну.
  3. Лідер надсилає AppendEntries усім послідовникам паралельно (також слугує серцебиттям, коли порожній).
  4. Щойно більшість серверів записали запис у свої журнали, лідер фіксує запис — застосовує його до своєї скінченної машини станів і відповідає клієнту.
  5. Лідер включає commitIndex у наступні AppendEntries; послідовники фіксують аж до цього індексу.
Перевірка узгодженості AppendEntries: Кожен RPC AppendEntries містить: - prevLogIndex: індекс запису журналу, що безпосередньо передує новим записам; - prevLogTerm: термін того запису. Послідовник відхиляє, якщо його журнал не має запису на prevLogIndex з prevLogTerm. → Лідер повторює спробу зі зменшеним prevLogIndex, доки не знайдеться збіг. → Щойно знайдено узгоджений префікс, послідовник перезаписує будь-які конфліктні записи. Це гарантує: якщо два журнали мають запис з однаковими (індекс, термін), усі записи з ранішими індексами ідентичні — «властивість збігу журналів» (Log Matching Property).

5. Гарантії безпеки

Коректність Raft спирається на властивість повноти лідера (Leader Completeness Property): якщо запис журналу зафіксований у певному терміні, цей запис буде присутнім у журналах усіх лідерів для всіх термінів з вищими номерами.

Це забезпечується обмеженням виборів: кандидат не може перемогти у виборах, якщо його журнал не є щонайменше таким же актуальним, як у будь-якого іншого сервера в більшості, що голосує за нього. «Актуальність» визначається порівнянням (lastLogTerm, lastLogIndex) — вищий термін перемагає; за рівності перемагає довший журнал.

Чому зафіксовані записи безпечні при зміні лідерів: зафіксований запис записано на більшість серверів. Будь-який новий лідер має отримати голоси більшості, щоб перемогти. Дві більшості перетинаються щонайменше в одному сервері — цей сервер має зафіксований запис і голосуватиме лише за кандидата, чий журнал його містить. Тому журнал нового лідера вже містить усі зафіксовані записи.

Raft також запобігає тонкій помилці: лідер не може фіксувати записи з попередніх термінів, рахуючи репліки — він може зафіксувати їх лише, зафіксувавши новий запис зі свого власного терміну (що приводить до неявної фіксації старіших записів через властивість збігу журналів).

6. Зміни складу кластера

Додавання чи видалення серверів потребує обережності — наївне миттєве перемикання могло б одночасно створити дві більшості. Спочатку Raft пропонував підхід спільного консенсусу (joint consensus) з переходом через об'єднану конфігурацію стара+нова. Сучасні реалізації зазвичай використовують простіший протокол зміни складу по одному серверу:

Новододані сервери стартують як учні без права голосу (non-voting learners), що реплікують журнал, не беручи участі у виборах і не зараховуючись до кворуму — це запобігає затримці виборів, поки вони наздоганяють потенційно петабайти історичного журналу.

7. Порівняння з Paxos

Властивість Paxos (multi-Paxos) Raft ────────────────────────────────────────────────────── Зрозумілість Складний, багато варіантів Явно спроєктований бути зрозумілим Вибори лідера Неявні (найвищий бюлетень) Явна фаза виборів Структура журналу Незалежні від порядку слоти Строго впорядкований журнал Передача лідерства Не визначена Вбудований RPC LeaderTransfer Знімки Не визначені Вбудований RPC InstallSnapshot Промислове використання Chubby, Spanner, ZAB* etcd, CockroachDB, TiKV, consul * ZAB натхненний Paxos, а не чистий Paxos

Raft і multi-Paxos розв'язують одну й ту саму задачу й мають подібні характеристики продуктивності. Головна перевага Raft — ясність специфікації; оригінальна стаття Онгаро й Оустергаута містить формальний доказ коректності та дослідження зручності, що показує: за однаковий час вивчення студенти розуміли Raft краще за Paxos. Ця зрозумілість виливається у менше помилок у промислових реалізаціях.

Сховище ключ-значення etcd використовує Raft і утворює координаційне серце Kubernetes — щоразу, коли планується под, вузол виводиться з обслуговування чи оновлюється ConfigMap, консенсус Raft гарантує, що зміна безпечно збережена на всіх репліках etcd ще до того, як API-сервер підтвердить успіх.

🔗 Дослідити розподілені системи →