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

Що таке партиція (partition) і навіщо вона потрібна

Кожна партиція являє собою суворо впорядковану, незмінну послідовність повідомлень (Append-Only Log), до якої нові події записуються виключно в кінець, отримуючи монотонно зрост...


🟢 Junior Level

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

Партиція (Partition) — це фундаментальна фізична одиниця масштабування, зберігання та паралелізму в Apache Kafka.

Кожна партиція являє собою суворо впорядковану, незмінну послідовність повідомлень (Append-Only Log), до якої нові події записуються виключно в кінець, отримуючи монотонно зростаючий порядковий номер — Offset.

Навіщо потрібні партиції:

  1. Паралелізм обробки: це ключовий закон Kafka: одну партицію в межах однієї Consumer Group може читати лише ОДИН потік/інстанс консьюмера. Якщо у топіка 10 партицій, ви можете запустити 10 паралельних інстансів мікросервісу та прискорити читання в 10 разів.
  2. Горизонтальне масштабування: топік розміром у 10 терабайтів неможливо розмістити на диску одного фізичного сервера. Партиції розподіляються по різних серверах-брокерах у кластері.
  3. Відмовостійкість: кожна партиція незалежно реплікується на інші брокери (Leader $\to$ Followers).

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

Партиції — це багатосмугова платна автомагістраль із касами оплати:

  • Топік — це вся траса цілком.
  • Партиції — це окремі виділені смуги руху (Смуга 0, Смуга 1, Смуга 2).
  • Консьюмери — це касири на пунктах збору оплати.
  • Якщо у вас лише 1 смуга (1 партиція), то скільки б касирів (консьюмерів) ви не найняли, на смузі зможе працювати лише один касир, а за ним вишикується гігантський затор. Зробивши 6 смуг, ви ставите 6 касирів і пропускаєте в 6 разів більше автівок одночасно!

Базовий приклад структури топіка

[ Топік "payments" (3 партиції) ]
  ├── [ Партиція 0 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance A
  ├── [ Партиція 1 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance B
  └── [ Партиція 2 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance C
# Створення топіка з явним зазначенням 6 партицій
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create \
  --topic payments \
  --partitions 6 \
  --replication-factor 3

🟡 Middle Level

Анатомія партиції: Offset, LEO та High Watermark

Усередині кожної партиції існує чітке розмежування станів повідомлень:

[ Фізичний лог Партиції 0 ]
 Offset:    0     1     2     3     4     5     6     7
         ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
         │ msg │ msg │ msg │ msg │ msg │ msg │ msg │ msg │
         └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
                                       ▲                 ▲
                                       │                 │
                             [ High Watermark (HW) ]   [ Log End Offset (LEO) ]
                              (Підтверджено ISR)        (Записано на Лідері)
                                       ▲
                                       │
                        Консьюмерам ДОСТУПНО лише ДО HW!
  1. Offset: унікальний 64-бітний порядковий номер повідомлення всередині даної партиції. У партиції 0 є offset=42, і в партиції 1 є offset=42 — вони не мають один до одного жодного відношення.
  2. Log End Offset (LEO): зміщення наступного повідомлення, яке буде записано в лог лідера партиції.
  3. High Watermark (HW): зміщення останнього повідомлення, яке було успішно скопійоване всіма репліками з пулу ISR (In-Sync Replicas).
    • Критичне правило: консьюмери ніколи не побачать повідомлення з offset між HW та LEO! Це гарантує, що клієнти не прочитають «брудні» дані, які можуть бути втрачені в разі падіння поточного лідера.

Як брокер розподіляє дані по партиціях

Коли продюсер надсилає ProducerRecord<Key, Value>:

  • Якщо ключ вказано (key != null): брокер обчислює хеш за алгоритмом Murmur2: \(\text{partition} = \text{murmur2}(\text{key}) \pmod{\text{totalPartitions}}\) Усі повідомлення з однаковим ключем (наприклад, userId = "usr-889") гарантовано потрапляють в одну й ту саму партицію, забезпечуючи суворий порядок FIFO.
  • Якщо ключ відсутній (key == null): у сучасних версіях Kafka (починаючи з 2.4+) працює алгоритм Sticky Partitioner: продюсер пакує повідомлення пачками (batches) в одну партицію до її заповнення, після чого перемикається на наступну, багаторазово знижуючи мережеві затримки та підвищуючи ступінь стиснення.

Правило зіставлення консьюмерів і партицій

У межах однієї групи споживачів (Consumer Group):

  • Якщо Partitions == Consumers (наприклад, 4 партиції та 4 консьюмери) $\to$ ідеальний баланс: кожному дістається рівно по 1 партиції.
  • Якщо Partitions > Consumers (наприклад, 6 партицій та 2 консьюмери) $\to$ кожен консьюмер обробляє по 3 партиції.
  • Якщо Consumers > Partitions (наприклад, 3 партиції та 5 консьюмерів) $\to$ 2 консьюмери будуть бездіяльно простоювати в холостому режимі (Idle), оскільки Kafka забороняє паралельне читання однієї партиції двома інстансами однієї групи!

🔴 Senior Level

Файлова структура сегментів на файловій системі Linux

На сервері брокера кожна партиція являє собою звичайний каталог у log.dirs (наприклад, /var/lib/kafka/data/orders-0/):

/var/lib/kafka/data/orders-0/
├── 00000000000000000000.log         # Фактичні стислі бінарні записи
├── 00000000000000000000.index       # Розріджений індекс зміщень: Offset -> File Position
├── 00000000000000000000.timeindex   # Часовий індекс: Timestamp -> Offset
├── leader-epoch-checkpoint          # Контрольні точки зміни лідерів (JEP/KIP-101)
└── partition.metadata               # UUID партиції топіка

Розріджений індекс (Sparse Index):

Kafka не індексує кожне повідомлення окремо. Запис у .index вноситься через кожні index.interval.bytes (за замовчуванням 4096 байтів / 4 КБ):

  1. Консьюмер запитує повідомлення з offset = 1530.
  2. Брокер відкриває файл .index (відображений у пам’ять через mmap) і виконує двійковий пошук ($O(\log N)$), знаходячи найближчу мітку (наприклад, offset 1500 = байт 61440).
  3. Брокер переходить до зміщення 61440 у файлі .log і виконує коротке лінійне сканування лише 4 КБ даних до потрібного зміщення.

Захист від втрати даних: Leader Epoch проти High Watermark Truncation

До версії Kafka 0.11 реплікація спиралася виключно на High Watermark (HW). Це призводило до прихованих катастроф при збоях брокерів:

Сценарій збою без Leader Epoch:

  1. Репліка A (Лідер) і Репліка B (Follower) записали повідомлення з offset=1, але Репліка B впала до того, як оновила свій HW.
  2. При перезавантаженні Репліка B усікала свій лог до старого відомого HW (видаляла offset=1).
  3. Якщо в цей момент Лідер A падав, Репліка B ставала новим лідером і записувала нове повідомлення в offset=1.
  4. У результаті лог розходився (Log Divergence), а підтверджені дані безповоротно втрачалися!

Рішення — Leader Epoch (leader-epoch-checkpoint):

Кожна зміна лідера інкрементує монотонний номер епохи (Leader Epoch, наприклад Epoch 1, Epoch 2). Замість автоматичного усікання логу за HW, повернутий після збою Follower запитує поточного Лідера: «Яке останнє зміщення було записане у твоїй епосі?». Лідер відповідає точним значенням, і фолловер усікає лог лише до межі епохи, виключаючи випадкову втрату узгоджених записів.


Проблема «Гарячої партиції» (Hot Partition & Key Skew)

У продакшені часто виникає ситуація, коли один ключ генерує 80% усіх транзакцій (наприклад, у маркетплейсі ключ великого продавця «Apple» або «Samsung» генерує мільйони замовлень, а дрібні ФОП — по 5 замовлень на день):

  • Усі повідомлення з ключем «Apple» потрапляють суворо в одну партицію (наприклад, партицію 2).
  • Брокер, що обслуговує партицію 2, впирається в 100% CPU та IOPS диска.
  • Консьюмер, що читає партицію 2, накопичує величезний Consumer Lag, тоді як інші 15 консьюмерів простоюють без роботи!

Архітектурні рішення проблеми Key Skew:

  1. Соління ключа (Key Salting): до популярного ключа додається випадковий префікс або суфікс: apple_0, apple_1, apple_2. Повідомлення рівномірно розподіляються по 3 партиціях.
  2. Складені ключі (Compound Keys): використання як ключа зв’язки sellerId_orderId замість одного sellerId.

4 Tricky Questions

1. У чому фундаментальна різниця між Log End Offset (LEO) та High Watermark (HW), і чому консьюмер не може прочитати повідомлення між ними?

Відповідь:

  • LEO (Log End Offset): зміщення останнього повідомлення, фізично записаного на диску лідера партиції (включаючи повідомлення, які ще не встигли реплікуватися на фолловерів).
  • HW (High Watermark): зміщення останнього повідомлення, яке було успішно скопійоване та підтверджене ВСІМА репліками із синхронного пулу ISR (In-Sync Replicas).
  • Чому консьюмер не бачить повідомлення між HW та LEO: якби консьюмер прочитав повідомлення, що знаходиться вище за HW (наприклад, записане на лідері, але ще не відрепліковане на вузол B), а лідер у цей момент аварійно згас, новим лідером став би вузол B. На вузлі B цього повідомлення немає, і воно буде замінене іншим повідомленням під час наступного запису продюсера. Клієнт зіткнувся б з ілюзією читання «повідомлення-привида» (Dirty Read / Data Loss). Тому ізоляція за High Watermark гарантує узгодженість читання (Read Committed Isolation).

2. Чому наявність 50 000 партицій на одному брокері здатна паралізувати весь кластер Kafka, особливо в архітектурах із ZooKeeper?

Відповідь: Велика кількість партицій створює колосальні накладні витрати за 4 напрямками:

  1. Дескриптори файлів ОС (ulimit -n): кожна партиція містить мінімум 3 відкритих файли на сегмент (.log, .index, .timeindex). 50 000 партицій миттєво з’їдають сотні тисяч дескрипторів файлів ОС.
  2. Пам’ять ядра (OS Page Cache & JVM Metaspace): кожен індекс партиції утримується в оперативній пам’яті через mmap. Брокер вичерпує віртуальну пам’ять.
  3. Паузи при перевиборах лідера (Leader Election Storm): при падінні брокера контролер повинен вибрати нових лідерів для 50 000 партицій. У кластерах із ZooKeeper це викликало шквал ZK-запитів і заморожування кластера на кілька хвилин! (Проблема частково вирішена в KRaft з апаратним батчингом метаданих, але ліміт у 2000–4000 партицій на брокер залишається найкращою практикою).
  4. Затримки реплікації: потокам ReplicaFetcherThread доводиться по черзі опитувати тисячі черг, що різко збільшує затримки наскрізної реплікації (Replication Lag).

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

Відповідь:

  • 4 інстанси отримають по 1 партиції та будуть повноцінно обробляти події, що надходять.
  • 2 інстанси, що залишилися, перейдуть у стан бездіяльності (Idle/Cold Standby) і не отримають жодного повідомлення.
  • Kafka фундаментально забороняє одночасний поділ однієї партиції між кількома консьюмерами однієї групи, оскільки це миттєво зруйнувало б суворий порядок зміщень (offset order).
  • Проте ці 2 простоюючі інстанси слугують «гарячим резервом»: якщо один із 4 працюючих інстансів впаде через OOM або вийде з ладу сервер, координатор групи ініціює Rebalance, і звільнена партиція моментально перепризначиться на один із вільних інстансів без необхідності очікування запуску нового пода в Kubernetes.

4. Чи можна примусово спрямувати повідомлення в конкретний номер партиції в обхід стандартного Partitioner?

Відповідь: Так, можна. У класі ProducerRecord у Java Kafka Client конструктор перевантажений і приймає явний номер партиції:

// Явне зазначення цільової партиції: Integer partition = 2
ProducerRecord<String, String> record = 
    new ProducerRecord<>("orders", 2, System.currentTimeMillis(), "order-123", jsonPayload);
kafkaTemplate.send(record);

Якщо номер партиції передано явно (partition != null), стандартний DefaultPartitioner (хешування ключа або sticky batching) повністю ігнорується, і повідомлення надсилається суворо у вказану партицію. Проте це вважається серйозним архітектурним антипатерном (code smell), оскільки жорстко прив’язує топологію кластера до вихідного коду бізнес-логіки та призведе до помилок IllegalArgumentException при перенесенні сервісу в середовище з іншою кількістю партицій.


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

Головне про партиції

  • Сутність: фундаментальна одиниця паралелізму, фізичного шардування та зберігання в Kafka.
  • Лог FIFO: незмінний append-only лог із суворим порядком зміщень (Offset) всередині кожної партиції.
  • Закон консьюмера: 1 партиція = максимум 1 консьюмер у межах однієї Consumer Group.
  • High Watermark (HW): межа видимості даних консьюмерами; повідомлення вище за HW клієнтам недоступні до підтвердження репліками.
  • Leader Epoch: запобігає втраті підтверджених даних та розходженню логів при зміні лідерів.
  • Key Skew (Hot Partition): перекіс навантаження через один популярний ключ; вирішується солінням ключів (Salting).
  • Розріджений індекс (.index): двійковий пошук по пам’яті mmap кожні 4 КБ логу.
  • Обмеження: зменшити кількість партицій не можна; рекомендований ліміт — до 2 000–4 000 партицій на брокер.

Зведення метрик партиції

| Термін | Визначення | Доступність для читання | | :— | :— | :— | | Offset | Монотонний номер події в партиції | Так (якщо $\le$ HW) | | LEO (Log End Offset) | Зміщення наступного запису на Лідері | Ні (якщо > HW) | | HW (High Watermark) | Зміщення, підтверджене всіма ISR | Межа читання консьюмерів | | ISR | Пул реплік, що встигають за лідером | Беруть участь у формуванні HW |

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

  • ❌ «Якщо консьюмери не справляються, ми просто запустимо 20 інстансів на топік з 4 партицій» — 16 інстансів будуть простоювати в Idle.
  • ❌ «Offset повідомлення унікальний у всьому топіку» — він унікальний лише в межах конкретної партиції.
  • ❌ «Консьюмер одразу читає будь-яке повідомлення, щойно лідер записав його до себе на диск» — консьюмер бачить лише повідомлення до High Watermark (HW).

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