Що таке 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-інженер повинен розрізняти п’ять ключових маркерів зміщення:
- Log Start Offset: зміщення найстарішого доступного повідомлення в партиції. Повідомлення ліворуч від нього вже видалені політикою зберігання (
retention.ms/retention.bytes). - Current Position: зміщення наступного повідомлення, яке консьюмер отримає при наступному виклику
poll(). - Committed Offset: останнє підтверджене зміщення, зафіксоване консьюмер-групою у службовому топіку
__consumer_offsets. - High Watermark (HW): зміщення останнього повідомлення, успішно реплікованого на всі репліки зі списку ISR (In-Sync Replicas). Звичайні консьюмери мають право читати дані тільки до High Watermark!
- Log End Offset (LEO): зміщення наступного повідомлення, яке буде записано в журнал лідера. LEO завжди $\ge$ HW.
Програмна робота з 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-бітним офсетом серед терабайтів даних? Кожен сегмент партиції складається з трьох файлів:
00000000000000000000.log: сирі бінарні повідомлення.00000000000000000000.index: розріджений індекс (Sparse Index) зміщень.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 зсуває офсети нових повідомлень до нуля» — офсети ніколи не зсуваються і не перераховуються.