📨 Розділ 15 · Питання #19

Як Kafka забезпечує відмовостійкість

Відмовостійкість (Fault Tolerance) в Apache Kafka будується на трьох фундаментальних рівнях:


🟢 Junior Level

30-секундна відповідь

Відмовостійкість (Fault Tolerance) в Apache Kafka будується на трьох фундаментальних рівнях:

  1. Реплікація даних на рівні брокерів: кожна партиція топіка дублюється на кількох незалежних серверах (replication.factor >= 3). Один брокер виступає в ролі Лідера (приймає запити), а решта є Фолловерами (пасивно реплікують дані).
  2. Автоматичний вибір нового лідера (Failover): при падінні брокера-лідера контролер кластера миттєво обирає нового лідера з пулу актуальних синхронних реплік (ISR).
  3. Стійкість клієнтів: продюсери автоматично повторюють надсилання при тимчасових збоях (retries, acks=all, ідемпотентність), а консьюмери об’єднуються в групи (Consumer Groups), автоматично перерозподіляючи партиції при виході будь-якого екземпляра з ладу.

Наочна аналогія з життя

Уявіть банківське сховище з трьома клерками:

  • Головний клерк (Лідер) записує операції у свій журнал, а двоє помічників (Фолоcontent/Фоловери) сидять поруч і синхронно копіюють кожен рядок у власні гросбухи.
  • Якщо головний клерк раптово знепритомнів, один із помічників, чий журнал повністю збігається з головним (член активного ISR), негайно займає його місце за касою.
  • Клієнти (Продюсери та Консьюмери) чекають кілька секунд, доки зміниться табличка у віконці, і продовжують роботу без втрати жодної копійки.

Створення відмовостійкого топіка в CLI

# Створення топіка з фактором реплікації 3 на 3 партиції
kafka-topics.sh --bootstrap-server localhost:9092 --create \
  --topic critical-transactions \
  --partitions 3 \
  --replication-factor 3 \
  --config min.insync.replicas=2

🟡 Middle Level

«Золотий трикутник» відмовостійкості Kafka

Для забезпечення надійної гарантії збереження даних (Durability) на рівні продакшену використовується зв’язка трьох параметрів:

          ┌───────────────────────────────────┐
          │      replication.factor = 3       │
          │    (3 копії даних на брокерах)    │
          └─────────────────┬─────────────────┘
                            │
            ┌───────────────┴───────────────┐
            ▼                               ▼
┌───────────────────────┐       ┌───────────────────────┐
│ min.insync.replicas=2 │       │       acks = all      │
│ (мінімум 2 в ISR)     │◄─────►│ (підтвердження від    │
│                       │       │    усіх членів ISR)   │
└───────────────────────┘       └───────────────────────┘
  1. replication.factor = 3: дані зберігаються на 3 фізично різних брокерах (кластер витримує відмову до 2 брокерів без втрати даних).
  2. min.insync.replicas = 2: брокер заборонить запис, якщо в живих залишиться менше двох синхронізованих реплік. Це захищає систему від перетворення на єдину точку відмови.
  3. acks = all: продюсер вважає повідомлення відправленим лише після того, як усі учасники поточного ISR підтвердили запис на диск.

Вибір лідера (Leader Election)

  • Clean Leader Election: контролер призначає новим лідером репліку, що входить до ISR. Гарантується нульова втрата підтверджених даних ($RPO = 0$).
  • Unclean Leader Election (unclean.leader.election.enable):
    • Якщо всі члени ISR раптово вийшли з ладу, кластер може або чекати повернення репліки з ISR (false, пріоритет Consistency), або призначити лідером відсталого фолловера не з ISR (true, пріоритет Availability).
    • У продакшені unclean.leader.election.enable завжди встановлюють у false, щоб виключити втрату даних та відкат оффсетів.

🔴 Senior Level

Механізм Leader Epoch та усунення проблеми усікання логів

До появи механізму Leader Epoch (KIP-101) Kafka покладалася виключно на High Watermark (HW) для відновлення після збоїв. Це призводило до двох катастрофічних сценаріїв при аварійному перезапуску брокерів:

  1. Log Truncation (Усікання лога): фолловер перезавантажувався, урізав свій лог до старого HW, після чого ставав лідером і назавжди стирав дані, які вже були закомічені.
  2. Log Divergence (Розходження логів): два брокери мали однакові зсуви (offsets), але абсолютно різні повідомлення всередині них.
Leader Epoch розв'язує цю проблему:
Кожна зміна лідера інкрементує Leader Epoch (монотонно зростаюче число: 0, 1, 2...).
Кожен запис на диску маркується парою [Epoch + Offset].
При перепідключенні фолловер не урізає свій лог до свого локального HW, а відправляє лідеру
запит LeaderEpochRequest("Який твій останній оффсет для епохи N?").
Лідер відповідає точним оффсетом завершення епохи, гарантуючи ідеальне узгодження логів.

Розподіл реплік з урахуванням стійок (broker.rack)

Для запобігання втраті даних при аварії серверної стійки або цілого дата-центру (Availability Zone у хмарі) використовується параметр broker.rack:

# server.properties на кожному брокері
broker.rack=eu-central-1a  # Брокер 1
broker.rack=eu-central-1b  # Брокер 2
broker.rack=eu-central-1c  # Брокер 3
  • Контролер під час створення топіка розподіляє репліки так, щоб копії однієї партиції гарантовано потрапляли на різні стійки / зони доступності.
  • Починаючи з Kafka 2.4, консьюмери можуть читати дані з найближчої репліки (fetch.max.wait.ms, rack-aware follower fetching), заощаджуючи міжзональний мережевий трафік.

4 Tricky Questions

1. Чому одного High Watermark було недостатньо для запобігання втраті даних при збоях, і як Leader Epoch виправив цей архітектурний недолік?

Відповідь:

  • High Watermark оновлюється асинхронно: фолловер підтверджує запис лідеру, але дізнається про нове значення HW тільки з наступною відповіддю FetchResponse.
  • Сценарій збою без Leader Epoch: Брокер А (лідер) та Брокер Б (фолловер) отримали повідомлення з offset=1. Брокер Б записав його на диск, але його локальний HW залишився рівним 0. Якщо Брокер Б перезавантажиться, він за старими правилами урізав свій лог до свого локального HW (0), видаляючи повідомлення 1. Якщо Брокер А одразу після цього падав, Брокер Б ставав новим лідером, і повідомлення 1 безслідно зникало, хоча продюсер уже отримав підтвердження запису.
  • З Leader Epoch: після рестарту Брокер Б не чіпає свій лог. Він запитує у Брокера А: «До якого оффсету сягає епоха 1?». Брокер А відповідає «offset=2», і Брокер Б нічого не урізає.

2. У кластері з RF = 3 та min.insync.replicas = 2 впало 2 брокери. Що відбувається з операціями запису та читання?

Відповідь:

  • Запис (ProduceRequest з acks=all): повністю блокується. Брокер-лідер повертає помилку NotEnoughReplicasException, оскільки поточний розмір ISR дорівнює 1, що менше налаштованого мінімуму 2.
  • Читання (FetchRequest): продовжує працювати. Єдиний живий брокер продовжує віддавати консьюмерам усі повідомлення в межах поточного зафіксованого High Watermark.
  • Відновлення: щоб відновити прийом нових повідомлень, необхідно або підняти хоча б один із впалих брокерів, або адміністративно тимчасово зменшити min.insync.replicas до 1 (що пов’язане з високими ризиками втрати даних у разі падіння останнього брокера).

3. Як Kafka захищається від проблеми Split-Brain, якщо ізольований брокер-лідер вважає, що він усе ще керує партицією?

Відповідь: Kafka захищається на двох рівнях:

  1. Raft Quorum (KRaft): контролер кластера обирається суворим кворумом більшості $\frac{N}{2} + 1$. Ізольована меншість не здатна обрати альтернативний контролер.
  2. Generation/Leader Epoch в RPC-запитах: коли кворум контролерів призначає нового лідера, епоха партиції інкрементується. Якщо старий ізольований брокер («зомбі-лідер») спробує надіслати запити реплікації або прийняти запити від клієнтів, нові клієнти та брокери відхилять їх із помилкою FencedLeaderEpochException або NotLeaderOrFollowerException.

4. У чому різниця у стійкості між реплікацією всередині одного кластера Kafka та міжкластерною реплікацією через MirrorMaker 2?

Відповідь:

  • Внутрішньокластерна реплікація:
    • Є квазісинхронною (при acks=all запис фіксується лише після реплікації на кворум брокерів).
    • Забезпечує $RPO = 0$ (Zero Data Loss) при дотриманні min.insync.replicas.
    • Оффсети суворо детерміновані.
  • Міжкластерна реплікація (MirrorMaker 2):
    • Є повністю асинхронною.
    • Будується на стандартних Consumer та Producer API поверх мережі WAN.
    • Завжди має затримку реплікації ($RPO > 0$). При катастрофічному падінні основного дата-центру повідомлення, що знаходяться в мережевому буфері MirrorMaker, будуть втрачені.
    • Оффсети повідомлень у цільовому кластері не збігаються з вихідними (трансляція оффсетів здійснюється через допоміжний топік heartbeats / checkpoints).

🎯 Шпаргалка для інтерв’ю

Головне про відмовостійкість Kafka

  • Базовий стандарт: replication.factor = 3, min.insync.replicas = 2, acks = all.
  • RPO = 0 (No Data Loss): досягається завдяки ISR та unclean.leader.election.enable = false.
  • Leader Epoch: механізм узгодження логів, що повністю усуває розходження та фантомне усікання даних при падіннях брокерів.
  • Rack Awareness: розподіл копій партицій по фізичних стійках і хмарних зонах доступності через broker.rack.
  • Клієнтський захист: ідемпотентні продюсери (PID + Sequence Number) усувають дублікати при retry після мережевих збоїв.

Матриця доступності при відмові брокерів (RF=3, min.insync=2)

| Стан кластера | Розмір ISR | Доступність на запис | Доступність на читання | Ризик втрати даних | | :— | :— | :— | :— | :— | | Усі 3 брокери працюють | 3 | ✅ Повна | ✅ Повна | 🟢 0% | | Упав 1 брокер | 2 | ✅ Повна | ✅ Повна | 🟢 0% | | Упало 2 брокери | 1 | ❌ Заблокована | ✅ Доступна до HW | 🟡 Лише якщо впаде 3-й | | Упали всі 3 брокери | 0 | ❌ Недоступна | ❌ Недоступна | 🔴 Залежить від збереження дисків |

Червоні прапорці на співбесіді (Чого говорити не можна)

  • ❌ «Kafka гарантує стовідсоткову відсутність втрати даних при будь-яких налаштуваннях із коробки» — за замовчуванням acks=1 (до Kafka 3.0), при якому падіння лідера призводить до втрати даних.
  • ❌ «Дзеркалювання через MirrorMaker 2 забезпечує синхронну реплікацію без втрати повідомлень між дата-центрами» — MirrorMaker завжди асинхронний, $RPO > 0$.
  • ❌ «Якщо в ISR залишився 1 брокер, консьюмери перестають вичитувати топік» — читання закомічених даних ніколи не блокується через зменшення розміру ISR.

Пов’язані запитання