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

Що таке топік (topic) в Kafka

Головна відмінність топіка Kafka від класичних черг повідомлень (RabbitMQ, ActiveMQ):


🟢 Junior Level

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

Топік (Topic) в Apache Kafka — це іменована логічна категорія (канал), що є розподіленим, незмінним журналом дозапису (Immutable Append-Only Commit Log), у який продюсери публікують події, а консьюмери їх вичитують.

Головна відмінність топіка Kafka від класичних черг повідомлень (RabbitMQ, ActiveMQ):

  • У традиційній черзі: повідомлення видаляється брокером одразу після того, як один отримувач підтвердив його обробку (destructive read).
  • У топіку Kafka: події НЕ видаляються під час читання. Вони зберігаються на диску протягом заданого періоду часу (retention policy, наприклад 7 днів). Безліч незалежних систем і мікросервісів можуть читати один і той самий топік паралельно, зі своєю власною швидкістю і навіть перечитувати старі дані заново з будь-якого моменту часу.

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

Топік — це щоденна друкована газета або журнал у бібліотеці:

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

Базовий приклад відправлення та читання в Spring Boot 3

@Service
public class OrderEventService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public OrderEventService(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    // Відправлення події в топік "orders" з ключем замовлення
    public void publishOrderCreated(String orderId, String payload) {
        kafkaTemplate.send("orders", orderId, payload);
    }

    // Незалежне читання з топіка групою мікросервісів
    @KafkaListener(topics = "orders", groupId = "billing-service-group")
    public void consumeForBilling(String message) {
        // Обробка платежу
    }

    @KafkaListener(topics = "orders", groupId = "notifications-service-group")
    public void consumeForNotifications(String message) {
        // Відправлення SMS клієнту (читає той самий топік паралельно)
    }
}
# Створення топіка через CLI: 3 партиції для паралелізму, фактор реплікації 3
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create \
  --topic orders \
  --partitions 3 \
  --replication-factor 3

🟡 Middle Level

Логічна та фізична архітектура топіка

Топік — це лише логічна абстракція. Фізично на серверах-брокерах топік поділяється на партиції (Partitions):

                            [ Логічний Топік: orders ]
                                         │
        ┌────────────────────────────────┼────────────────────────────────┐
        ▼                                ▼                                ▼
  [ Партиція 0 ]                   [ Партиція 1 ]                   [ Партиція 2 ]
  (Брокер 1, Лідер)                (Брокер 2, Лідер)                (Брокер 3, Лідер)
        │                                │                                │
  Файли на диску:                  Файли на диску:                  Файли на диску:
  orders-0/                        orders-1/                        orders-2/
   ├── 00000.log (Дані)             ├── 00000.log                    ├── 00000.log
   ├── 00000.index (Зсув/Offset)    ├── 00000.index                  ├── 00000.index
   └── 00000.timeindex (Час)        └── 00000.timeindex              └── 00000.timeindex
  • Каталог на диску: на файловій системі брокера створюється каталог <topic_name>-<partition_id>.
  • Сегменти журналу (.log): дані записуються в активний файл сегмента. При досягненні segment.bytes (за замовчуванням 1 ГБ) або segment.ms (за замовчуванням 7 днів) сегмент закривається (sealed) і створюється новий.
  • Індекси (.index, .timeindex): розріджені індекси (Memory-Mapped Files), що дозволяють миттєво знаходити фізичне зміщення байтів на диску за порядковим номером (offset) або міткою часу події без повного сканування файлу.

Політики очищення журналу: delete vs compact

Поведінка видалення даних визначається конфігурацією cleanup.policy:

Політика Механізм Призначення / Use Case
delete (default) Старі сегменти безповоротно видаляються за часом (retention.ms) або за загальним розміром топіка (retention.bytes). Подієві потоки: аудит, метрики, клікстрім, замовлення.
compact Фоновий процес LogCleaner видаляє застарілі дублікати записів з однаковим ключем, зберігаючи лише останній стан кожного ключа. Збереження агрегатів, профілі користувачів, кеш-таблиці (Kafka Streams KTable).

Гарантії порядку повідомлень

  • Всередині однієї партиції: гарантується суворий порядок FIFO (First-In, First-Out). Кожне повідомлення отримує монотонно зростаючий номер — Offset.
  • У масштабі всього топіка (між різними партиціями): глобального порядку НЕ існує. Якщо в топіку 3 партиції, події будуть зчитуватися паралельно і можуть перемішуватися.
  • Висновок: для забезпечення порядку пов’язаних бізнес-подій (наприклад, подій одного замовлення order-123) продюсер зобов’язаний вказувати однаковий Message Key. Усі повідомлення з одним ключем гарантовано потраплять в одну й ту саму партицію.

🔴 Senior Level

Zero-Copy та операційна система: чому Kafka пише на диск швидше, ніж інші в пам’ять

Поширене запитання на співбесідах: «Як Kafka примудряється обробляти мільйони повідомлень на секунду, якщо вона кожне повідомлення пише на повільний диск?»

Секрет криється в архітектурі ядра Linux і відмові від кешування всередині JVM:

  1. Послідовне введення-виведення (Sequential I/O): Kafka лише дописує дані в кінець файлу (Append-Only). Швидкість послідовного запису на сучасний NVMe SSD сягає 3–5 ГБ/с, що порівнянно зі швидкістю оперативної пам’яті, виключаючи довільне позиціювання головки (Random I/O).
  2. Page Cache ядра Linux: брокер Kafka не зберігає повідомлення в Java Heap (що виключає накладні витрати на збирання сміття GC та оверхед службових заголовків об’єктів JVM). Усі записані дані залишаються в системному кеші сторінок ОС.
  3. Системний виклик sendfile() (Zero-Copy Transfer): Під час передавання даних консьюмеру Kafka не копіює байти в користувацький простір Java. Ядро передає дані безпосередньо з кешу сторінок диска в мережевий сокет:
    Класичний підхід: Диск ──► Ядро (PageCache) ──► JVM Heap ──► Буфер сокета ──► NIC (4 копіювання)
    Zero-Copy (sendfile): Диск ──► Ядро (PageCache) ──────────────────────────────► Мережева карта (NIC DMA)
    

Log Compaction, Tombstones і ризик «Воскресіння даних»

При використанні cleanup.policy=compact:

  • Якщо клієнт хоче видалити запис із ключем userId=42, продюсер відправляє в топік спеціальний маркер видалення — Tombstone: повідомлення з ключем key="42" і порожнім тілом value=null.
  • Фоновий потік LogCleaner під час проходу стискає сегменти, видаляє всі старі версії значення ключа 42 і тимчасово залишає сам маркер Tombstone.
  • Небезпечна помилка (Data Resurrection): параметр delete.retention.ms (за замовчуванням 24 години) визначає, як довго Tombstone фізично зберігається на диску. Якщо повільний або аварійно зупинений консьюмер був вимкнений довше, ніж delete.retention.ms, при повторному запуску він не побачить Tombstone і відновить старий стан сутності з попередніх резервних копій!

Контракт надійності: Тріада acks=all, min.insync.replicas та RF

Для запобігання втраті даних у критичних фінансових топіках використовується сувора зв’язка налаштувань:

  • replication.factor = 3 (3 фізичні копії партиції на різних серверах).
  • min.insync.replicas = 2 (мінімум 2 живі репліки повинні підтвердити запис).
  • acks = all (або -1) на стороні Producer.

Граничний сценарій (Edge Case): Якщо в кластері з 3 брокерів два вузли виходять з ладу, у строю залишається лише лідер.

  • Продюсер з acks=all намагається надіслати повідомлення.
  • Лідер бачить, що розмір пулу синхронних реплік (ISR) впав до 1, що менше ніж min.insync.replicas: 2.
  • Лідер відхиляє запис і викидає продюсеру NotEnoughReplicasException. Це захищає систему від втрати даних під час подальшого перезапуску кластера.

4 Tricky Questions

1. Чому в Kafka категорично заборонено ЗМЕНШУВАТИ кількість партицій у наявного топіка?

Відповідь:

  • У Kafka дані всередині партиції є незмінним послідовним логом, упорядкованим за зміщеннями (offsets).
  • Якби брокер дозволив зменшити кількість партицій (наприклад, з 4 до 2), виникла б нерозв’язна архітектурна дилема: що робити з даними з партицій 2 і 3?
    • Об’єднати їх у партиції 0 і 1 неможливо: порушиться сувора монотонна послідовність зміщень (offsets), виникнуть колізії та дублікати індексів.
    • Видалити ці дані не можна: це призведе до катастрофічної втрати даних.
  • Тому утиліта kafka-topics.sh --alter вміє лише збільшувати кількість партицій. Якщо зменшення все ж необхідне, єдиний шлях — створити новий топік і запустити міграцію даних через Kafka Connect або MirrorMaker.

2. Що станеться з порядком повідомлень у системі, якщо адміністратор збільшить кількість партицій з 3 до 6 на бойовому топіку, куди продюсери пишуть дані з ключем?

Відповідь: Порядок обробки повідомлень за конкретним бізнес-ключем буде фатально порушено! За замовчуванням продюсер Kafka обчислює номер партиції за формулою: \(\text{partition} = \text{murmur2}(\text{key}) \pmod{\text{totalPartitions}}\)

  • При totalPartitions = 3 події за замовленням order-99 стабільно потрапляли, наприклад, у партицію 1.
  • Щойно адміністратор збільшив кількість партицій до 6, дільник формули змінився: нові події за тим самим замовленням order-99 почнуть потрапляти в партицію 4!
  • У результаті подія OrderCancelled у партиції 4 може бути оброблена консьюмером раніше, ніж подія OrderCreated у партиції 1.
  • Висновок: кількість партицій у ключових топіках необхідно розраховувати і закладати з багаторазовим запасом на етапі проєктування системи.

3. Чому Kafka не кешує повідомлення в оперативній пам’яті Java-процесу (JVM Heap), а повністю покладається на Page Cache ядра Linux?

Відповідь: Це блискуче архітектурне рішення Джея Крепса з трьох причин:

  1. Оверхед об’єктів Java: кожен об’єкт у Java має службовий заголовок (Mark Word + Klass Word) і вирівнювання пам’яті. Повідомлення розміром 100 байт у JVM Heap може займати до 300–400 байт. Page Cache зберігає сирі стислі бінарні байти без оверхеду.
  2. Паузи збирача сміття (GC STW): кешування десятків гігабайтів повідомлень у Heap призвело б до колосальних пауз Full GC, що паралізують брокер.
  3. Збереження даних при перезапуску брокера: якщо процес Kafka аварійно завершиться (crash / OOM / kill -9) або перезапуститься під час оновлення, JVM Heap миттєво знищується, а Page Cache ОС залишається «гарячим» в оперативній пам’яті сервера, і брокер миттєво повертається до пікової швидкості віддавання даних.

4. Що таке «Tombstone» у топіку з політикою cleanup.policy=compact, і що станеться, якщо значення delete.retention.ms буде виставлено занадто малим?

Відповідь:

  • Tombstone — це спеціальне повідомлення-маркер видалення, де key заповнений, а value = null. Воно сигналізує брокеру про необхідність повного видалення ключа під час наступного циклу компактифікації логу.
  • Параметр delete.retention.ms (за замовчуванням 24 години) визначає інтервал часу, протягом якого Tombstone гарантовано зберігається в лозі перед тим, як збирач сміття зітре його з диска.
  • Якщо виставити delete.retention.ms занадто малим (наприклад, 10 секунд), то під час збою або мережевої затримки консьюмера він не встигне вичитати маркер Tombstone до його видалення з диска. У результаті консьюмер ніколи не дізнається про видалення сутності й продовжить вважати її активною у своїй локальній базі даних.

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

Головне про топік Kafka

  • Сутність: розподілений, персистентний, незмінний журнал дозапису подій (Immutable Append-Only Log).
  • Відмінність від черг: події НЕ видаляються після прочитання; їх паралельно читають незалежні групи консьюмерів.
  • Топік vs Партиція: топік — логічна назва; партиція — фізична директорія на сервері брокера.
  • Порядок: суворий FIFO гарантується лише всередині однієї партиції; глобального порядку в топіку немає.
  • Zero-Copy (sendfile): ядро ОС передає дані безпосередньо з Page Cache у мережевий сокет, оминаючи пам’ять JVM.
  • Політики очищення: delete (за часом/розміром) та compact (зберігає останній стан за ключем).
  • Партиції не можна зменшити: тільки збільшувати (але збільшення ламає порядок ключів через хешування).
  • Формула надійності: RF=3 + min.insync.replicas=2 + acks=all.

Топік Kafka vs Черга RabbitMQ

| Характеристика | Топік Kafka | Черга RabbitMQ | | :— | :— | :— | | Модель зберігання | Незмінний персистентний лог на диску | Черга в пам’яті (RAM), очищається за фактом ACK | | Видалення під час читання | ❌ Ні (зберігається заданий retention) | ✅ Так (видаляється після ACK) | | Перечитування (Replay) | ✅ Так (переміщення offset назад) | ❌ Ні (дані вже знищено) | | Споживачі | Багато незалежних груп консьюмерів | Конкуруючі споживачі (Compete) | | Пропускна здатність | Екстремальна (мільйони подій/сек) | Помірна (десятки тисяч повідомлень/сек) |

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

  • ❌ «Топік — це звичайна черга, щойно консьюмер прочитав повідомлення, брокер його видаляє» — грубе нерозуміння архітектури Kafka.
  • ❌ «Kafka гарантує ідеальний глобальний порядок повідомлень у всьому топіку» — порядок існує лише всередині конкретної партиції.
  • ❌ «Якщо нам не вистачає швидкості, ми просто зменшимо кількість партицій командою alter» — партиції неможливо зменшити.
  • ❌ «Kafka швидша за інші системи, тому що зберігає всі повідомлення в JVM Heap» — Kafka взагалі не зберігає повідомлення в Heap, а використовує Linux Page Cache та Zero-Copy.

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