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

Связанные вопросы