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

Що таке offset в Kafka

Ключові властивості:


🟢 Junior Level

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

Offset (зміщення) в Apache Kafka — це монотонно зростаюче 64-бітне ціле число (long), що присвоюється брокером кожному вхідному повідомленню в момент запису. Воно позначає незмінну логічну позицію запису в журналі партиції.

Ключові властивості:

  • Зміщення унікальне лише в межах конкретної партиції (у партиції 0 і в партиції 1 можуть одночасно існувати свої повідомлення з offset = 42).
  • Зміщення починається з нуля, суворо впорядковане і ніколи не перераховується назад (навіть якщо старі повідомлення видаляються збирачем retention).
  • Консьюмери використовують offset як закладку: вони зберігають свій прогрес у брокері і при перезапуску продовжують читання суворо з того місця, де зупинилися.

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

Уявіть багатотомну енциклопедію із закладками:

  • Топік — це вся енциклопедія.
  • Партиція — окремий том енциклопедії (Том 1, Том 2…).
  • Offset — це номер сторінки в конкретному томі. У кожному томі є своя 10-та сторінка, але зміст на них абсолютно різний.
  • Consumer Group — це читач, який читає енциклопедію і залишає кольорову закладку (Committed Offset) на тій сторінці, яку він прочитав і зрозумів. Прийшовши наступного дня, він відкриває том на сторінці Закладка + 1.

Візуалізація структури партиції

Партиція 0:
┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐
│ Offset 0  │ Offset 1  │ Offset 2  │ Offset 3  │ Offset 4  │ Offset 5  │  ... (нові повідомлення)
└───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘
      ▲                       ▲                       ▲           ▲
      │                       │                       │           │
Log Start Offset       Committed Offset       High Watermark     LEO (Log End Offset)
(найстаріше)         (прогрес консьюмера)   (доступно читачам)    (записано лідером)

🟡 Middle Level

Термінологія зміщень у партиції

Для грамотної роботи з Kafka Middle-інженер повинен розрізняти п’ять ключових маркерів зміщення:

  1. Log Start Offset: зміщення найстарішого доступного повідомлення в партиції. Повідомлення ліворуч від нього вже видалені політикою зберігання (retention.ms / retention.bytes).
  2. Current Position: зміщення наступного повідомлення, яке консьюмер отримає при наступному виклику poll().
  3. Committed Offset: останнє підтверджене зміщення, зафіксоване консьюмер-групою у службовому топіку __consumer_offsets.
  4. High Watermark (HW): зміщення останнього повідомлення, успішно реплікованого на всі репліки зі списку ISR (In-Sync Replicas). Звичайні консьюмери мають право читати дані тільки до High Watermark!
  5. Log End Offset (LEO): зміщення наступного повідомлення, яке буде записано в журнал лідера. LEO завжди $\ge$ HW.
\[\text{Consumer Lag} = \text{Log End Offset (LEO)} - \text{Committed Offset}\]

Програмна робота з Offset у Java

Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "billing-service");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
    TopicPartition partition = new TopicPartition("payments", 0);
    consumer.assign(List.of(partition));

    // Перевірка поточної позиції читання
    long nextOffset = consumer.position(partition);
    System.out.println("Наступний offset для читання: " + nextOffset);

    // Ручне перенесення позиції (Seek)
    consumer.seek(partition, 1500L); // Перейти на конкретний offset
    // consumer.seekToBeginning(List.of(partition)); // Скинути на початок
    // consumer.seekToEnd(List.of(partition));       // Перемотати в самий кінець

    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
    for (ConsumerRecord<String, String> record : records) {
        processPayment(record.value());
        
        // Ручна фіксація зміщення наступного запису (record.offset() + 1)
        consumer.commitSync(Map.of(
            partition, new OffsetAndMetadata(record.offset() + 1, "metadata-v1")
        ));
    }
}

🔴 Senior Level

Внутрішнє сховище: топік __consumer_offsets

До Kafka 0.9 офсети консьюмерів зберігалися в ZooKeeper. Це призводило до лавиноподібних навантажень на ZooKeeper і падіння кластерів при частих комітах. Починаючи з версії 0.9 офсети зберігаються у спеціальному внутрішньому топіку __consumer_offsets:

  • Топологія: за замовчуванням 50 партицій із фактором реплікації 3.
  • Очищення: топік налаштований на Log Compaction (зберігає лише останній актуальний стан для кожного ключа).
  • Структура ключа: [Group ID, Topic, Partition].
  • Структура значення: [Offset, LeaderEpoch, Metadata, CommitTimestamp].

Визначення партиції топіка __consumer_offsets для конкретної групи: \(\text{Partition ID} = |\text{hashCode}(\text{groupId})| \pmod{50}\) Брокер, який є лідером цієї партиції, стає Group Coordinator для даної групи консьюмерів.


Архітектура розрідженого індексу (.index)

Як Kafka миттєво знаходить запис на диску за його 64-бітним офсетом серед терабайтів даних? Кожен сегмент партиції складається з трьох файлів:

  1. 00000000000000000000.log: сирі бінарні повідомлення.
  2. 00000000000000000000.index: розріджений індекс (Sparse Index) зміщень.
  3. 00000000000000000000.timeindex: індекс за часовими мітками.

Механіка Memory-Mapped пошуку:

  • Файл .index не містить записів для кожного офсету. Запис додається через кожні 4 КБ даних (index.interval.bytes).
  • В індексі зберігається пара: [Відносний Offset (4 байти) -> Фізична позиція в .log файлі (4 байти)].
  • Пошук офсету виконується через двійковий пошук (Binary Search) по відображеному в оперативну пам’ять файлу .index (mmap), після чого виконується коротке лінійне сканування кількох кілобайтів у файлі .log.

4 Tricky Questions

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

Відповідь:

  • LEO (Log End Offset) — це зміщення останнього запису в локальному журналі лідера партиції. Брокер записав повідомлення в пам’ять/диск, але репліки фолловерів його ще не забрали.
  • HW (High Watermark) — це зміщення, до якого повідомлення успішно відрепліковане на всі активні репліки зі списку ISR (In-Sync Replicas).
  • Консьюмер не має права читати повідомлення між HW та LEO. Якби консьюмер прочитав повідомлення з офсетом LEO, а потім вузол-лідер згорів до реплікації на фолловерів, новий обраний лідер не мав би цього повідомлення. Консьюмер обробив би «фантомне» повідомлення, яке назавжди зникло з кластера!
  • Обмеження читання рівнем HW гарантує абсолютну узгодженість даних при будь-яких аваріях брокерів.

2. Як механізм Leader Epoch (KIP-101) вирішив проблему прихованої втрати даних (Silent Data Loss) та розходження журналів при зміні лідерів?

Відповідь:

  • До Kafka 0.11 узгодження реплік спиралося виключно на High Watermark.
  • Сценарій старого бага: Репліка B відстала і перезавантажується. При рестарті вона усікала (truncate) свій лог до останнього відомого їй HW. Якщо лідер A в цей момент падав до того, як B встигала наздогнати лог, вузол B ставав лідером з усіченим журналом, а повернутий вузол A також усікав свої дані до HW вузла B. У результаті повідомлення, які підтверджувалися клієнту, назавжди видалялися з кластера без будь-яких помилок.
  • Рішення з Leader Epoch: Разом із кожним повідомленням зберігається номер епохи лідера (Leader Epoch, що інкрементується при кожних виборах). При рестарті фолловер запитує в діючого лідера OffsetForLeaderEpochRequest. Усікання відбувається не за старим HW, а суворо за зміщенням, з якого почалася нова епоха лідера, що математично виключає втрату підтверджених даних.

3. Що станеться, якщо консьюмер закомітив offset = 500, але через довгий простій сервісу повідомлення з 0 по 800 були видалені збирачем retention, і як на це впливає auto.offset.reset?

Відповідь:

  • При спробі консьюмера запросити дані з офсетом 500 брокер бачить, що мінімальний доступний офсет у партиції (Log Start Offset) дорівнює 801.
  • Брокер повертає клієнту помилку OffsetOutOfRangeException.
  • Поведінка консьюмера в цей момент повністю визначається параметром auto.offset.reset:
    • earliest: консьюмер автоматично скидає свою позицію на мінімально доступний Log Start Offset (801) і продовжує читання. Дані з 500 по 800 вважаються безповоротно втраченими.
    • latest: консьюмер перемотує позицію в самий кінець партиції на актуальний LEO і чекає лише нових повідомлень.
    • none: консьюмер викидає фатальний виняток у потік застосунку, зупиняючи обробку, поки адміністратор вручну не скоригує позицію через kafka-consumer-groups.sh.

4. Чи може офсет у партиції Kafka переповнитися (Overflow), і що відбувається при досягненні максимального значення?

Відповідь:

  • Offset у Kafka зберігається в типі long (знакове 64-бітне ціле число, максимум $2^{63} - 1 \approx 9.22 \times 10^{18}$).
  • Навіть якщо відправляти в одну партицію 1 мільйон повідомлень на секунду безперервно, зміщення переповниться лише через: \(\frac{9.22 \times 10^{18}}{10^6 \times 3600 \times 24 \times 365} \approx 292\,471 \text{ років}\)
  • Таким чином, практичне переповнення офсету в реальних системах неможливе. У специфікації Kafka переповнення призведе до неможливості запису нових повідомлень у дану партицію і вимагатиме перестворення топіка.

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

Головне про Offset

  • Offset: монотонний 64-бітний лічильник, що визначає позицію запису всередині конкретної партиції.
  • Область видимості: суворо одна партиція (у різних партиціях офсети незалежні).
  • Зберігання прогресу: топік __consumer_offsets (стислий топік, 50 партицій, ключ [group, topic, partition]).
  • High Watermark (HW): межа реплікації на всі ISR; консьюмери не можуть читати праворуч від HW.
  • Consumer Lag: різниця між Log End Offset і закоміченим офсетом групи.
  • Індекси Kafka: розріджений .index + відображення в пам’ять mmap забезпечують $O(1)$ пошук байтового зміщення на диску.
  • Leader Epoch: запобігає втраті даних і розходженню логів при зміні лідерів.

Ієрархія зміщень у партиції

| Зміщення | Що означає | Хто контролює | | :— | :— | :— | | Log Start Offset | Найстаріше живе повідомлення | Retention cleaner | | Committed Offset | Остання зафіксована позиція | Consumer group | | Current Position | Точка наступного читання | Клієнтський poll() | | High Watermark | Межа безпечних узгоджених даних | Кворум ISR брокерів | | Log End Offset | Останній запис у локальному лозі лідера | Продюсер і Лідер |

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

  • ❌ «Offset — це глобальний унікальний ідентифікатор повідомлення в топіку» — він унікальний лише всередині партиції.
  • ❌ «Офсети консьюмерів зберігаються в ZooKeeper» — з часів Kafka 0.9 вони зберігаються в топіку __consumer_offsets.
  • ❌ «Консьюмер читає найсвіжіші дані одразу, щойно продюсер записав їх у лідер» — читання обмежене High Watermark.
  • ❌ «При видаленні старих повідомлень retention зсуває офсети нових повідомлень до нуля» — офсети ніколи не зсуваються і не перераховуються.

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