Что такое 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 сдвигает оффсеты новых сообщений к нулю» — оффсеты никогда не сдвигаются и не пересчитываются.