Каждый раз, когда producer отправляет сообщение в Kafka, оно не просто «кладётся в очередь» — оно записывается на диск брокера в строго определённую физическую структуру. Понимание этой структуры критически важно для правильного конфигурирования Kafka, диагностики проблем с производительностью и объяснения гарантий, которые даёт платформа.
Партиция как директория на диске
Каждая партиция топика — это директория на файловой системе брокера. Если у вас топик orders с тремя партициями, на диске брокера вы увидите три директории:
/kafka-logs/ orders-0/ orders-1/ orders-2/
Если топик реплицирован, каждый брокер, хранящий реплику партиции, будет иметь соответствующую директорию у себя. Брокер-лидер и фолловеры хранят одинаковую структуру файлов.
Внутри директории партиции находятся лог-сегменты — группы файлов, которые хранят данные определённого диапазона офсетов.
Три типа файлов сегмента
Каждый сегмент состоит из трёх файлов с одинаковым базовым именем (base offset) и разными расширениями:
Сегмент 1 (base=0)Закрытый сегмент 1: сегмент заполнен и запечатан. Базовое смещение (base offset) = 0 — первая запись в этом сегменте. Имена файлов: 00000000000000000000.log, .index, .timeindex. Сегмент закрывается когда его размер превышает log.segment.bytes (1 ГБ по умолчанию) или истекает log.roll.ms.
.log файлoffsets 0–999.log файл сегмента 1: содержит сами записи в бинарном формате. Каждая запись включает: offset (8 байт), timestamp (8 байт), key length, key bytes, value length, value bytes, headers. Записи хранятся последовательно. Формат: RecordBatch (батч записей) с magic byte=2 (Kafka 0.11+).
.index файлsparse index.index файл: разреженный индекс смещений. Отображает относительный offset записи на физическую позицию в .log файле. Не содержит запись для каждого offset — только каждые log.index.interval.bytes байт (4096 по умолчанию). Позволяет бинарным поиском найти приблизительную позицию в .log файле.
.timeindex файлtimestamp→offset.timeindex файл: индекс по времени. Отображает timestamp на смещение записи. Используется при поиске по времени (--reset-offsets --to-datetime). Разреженный: одна запись на каждые log.index.interval.bytes байт .log файла. Тип timestamp определяется message.timestamp.type: CreateTime (по умолчанию) или LogAppendTime.
Сегмент 2 (base=1000)Закрытый сегмент 2: следующий полный сегмент. Базовое смещение = 1000 (первая запись этого сегмента). Файлы: 00000000000000001000.log, .index, .timeindex. Закрытые сегменты доступны только для чтения. Могут быть сжаты (log compaction) или удалены по log.retention.bytes или log.retention.ms.
.log файлoffsets 1000–1999.log файл сегмента 2: содержит записи с offset 1000 по 1999. Log compaction (если включён) удалит старые версии записей с одинаковым ключом, оставив только последнюю. cleanup.policy=compact,delete — комбинирует compaction и удаление по времени.
.index файлsparse index.index файл сегмента 2: разреженный индекс для диапазона offset 1000-1999. Записи хранят relative_offset (4 байта) и position в .log файле (4 байта). При поиске offset N: находим ближайший индексный entry <= N, затем сканируем .log файл с найденной позиции.
.timeindex файлtimestamp→offset.timeindex файл сегмента 2: временной индекс для этого диапазона. Используется командой kafka-consumer-groups.sh --reset-offsets --to-datetime '2024-01-01T00:00:00'. Запись: timestamp (8 байт) + relative_offset (4 байта). Всегда возрастающий по времени внутри сегмента.
АКТИВНЫЙ сегмент (base=2000)Активный сегмент (active segment): единственный сегмент открытый для записи в каждой партиции. Новые записи добавляются только в конец активного сегмента (append-only). Базовое смещение = 2000. Имя файла: 00000000000000002000.log. Активный .index и .timeindex файлы тоже открыты для записи.
.log файлoffsets 2000–....log файл активного сегмента: открыт для записи. Новые RecordBatch добавляются в конец. Физически данные могут быть в OS page cache и ещё не сброшены на диск (контролируется flush.messages и flush.ms). Kafka полагается на репликацию для надёжности, не только на fsync.
.index файлрастёт.index файл активного сегмента: индекс пополняется по мере добавления записей. Каждые log.index.interval.bytes байт в .log файле добавляется новая запись в индекс. Максимальный размер индексного файла ограничен log.index.size.max.bytes (10 МБ по умолчанию).
.timeindex файлрастёт.timeindex файл активного сегмента: пополняется синхронно с .index файлом. Только при LogAppendTime временная метка гарантированно монотонно возрастает. При CreateTime временная метка устанавливается продюсером и может нарушать порядок.
HW = offset 2087High Watermark (HW): смещение 2087. До этого offset все ISR-реплики подтвердили получение данных. Консьюмеры читают записи только до HW — это гарантирует, что прочитанные данные не будут потеряны при отказе лидера. HW хранится в памяти лидера и передаётся фолловерам в FetchResponse.
LEO = offset 2095Log End Offset (LEO): смещение 2095 — следующий offset для записи. LEO лидера всегда >= HW. Разница LEO - HW называется «uncommitted records» — записи, которые лидер получил, но ISR ещё не подтвердила. При acks=all продюсер ждёт пока HW достигнет offset его записи.
Retention: удаление закрытых сегментовПолитика удаления сегментов (log.retention): закрытые сегменты удаляются по одному из критериев: log.retention.ms (7 дней по умолчанию), log.retention.bytes (нет лимита по умолчанию). Сегмент удаляется целиком когда его последняя запись старше retention период. LogCleaner (для компактных топиков) работает иначе — удаляет отдельные устаревшие ключи.
Compaction: cleanup.policy=compactLog compaction (cleanup.policy=compact): альтернатива удалению по времени. LogCleaner периодически сканирует закрытые сегменты и для каждого ключа оставляет только самую последнюю запись (или запись-томстоун с null value). Используется для changelog топиков (Kafka Streams, consumer offsets). Активный сегмент никогда не компактируется.
.log — данные сообщений
Файл .log содержит сами сообщения в формате записей (records). Записи добавляются последовательно в конец файла (append-only). Каждая запись содержит:
Размер batch (4 байта)
CRC32 контрольную сумму
Magic byte (версия формата)
Атрибуты (compression codec, timestamp type)
Timestamp
Offset delta (смещение относительно base offset batch)
Key length + key bytes
Value length + value bytes
Headers
Записи в .log файле читаются последовательно — это намеренное архитектурное решение. Последовательные чтения с диска работают в десятки раз быстрее случайных, что позволяет Kafka обслуживать миллионы сообщений в секунду на обычных жёстких дисках.
.index — индекс смещений
Файл .index содержит разреженное (sparse) отображение: offset → позиция в байтах в .log файле.
Индекс разреженный специально. Kafka не записывает позицию каждого офсета — только каждого N-го. Параметр log.index.interval.bytes (по умолчанию 4096) контролирует, через сколько байт данных добавляется новая запись в индекс. Это баланс между размером индекса и скоростью поиска.
Когда consumer запрашивает данные начиная с offset X, Kafka выполняет бинарный поиск по .index файлу, находит ближайший меньший индексированный offset, а затем читает .log файл последовательно с найденной позиции.
.timeindex — индекс по времени
Файл .timeindex содержит отображение: timestamp → offset. Используется при запросе данных по времени (операция seekToBeginningByTime):
Это позволяет Kafka найти ответ на вопрос: «Где в логе находится первое сообщение, чей timestamp не меньше заданного значения?» Применяется при операции --reset-offsets --to-datetime в инструментах командной строки.
Именование сегментов
Каждый сегмент называется по базовому офсету первой записи, которую он содержит:
orders-0/ 00000000000000000000.log ← сегмент с offset 0..N 00000000000000000000.index 00000000000000000000.timeindex 00000000000000005000.log ← сегмент с offset 5000..M 00000000000000005000.index 00000000000000005000.timeindex 00000000000000012000.log ← активный сегмент (пишется прямо сейчас) 00000000000000012000.index 00000000000000012000.timeindex
Имена файлов — это 20-значные числа с ведущими нулями, представляющие base offset сегмента.
Активный сегмент
В каждой партиции всегда есть ровно один активный сегмент (active segment) — тот, в который в данный момент записываются новые сообщения. Его .log файл открыт для записи. Все остальные сегменты закрытые (passive/closed) — они доступны только для чтения.
Активный и закрытые сегменты
Сегмент 0 (закрытый)Закрытый сегмент: offset 0-4999. Файл .log закрыт на запись. Доступен для чтения consumers. Будет удалён согласно политике retention.
Сегмент 5000 (закрытый)Закрытый сегмент: offset 5000-11999. Хранит 7000 записей. Flush на диск выполнен полностью.
Сегмент 12000 (активный)Активный сегмент: offset 12000+. Файл .log открыт для записи. Producer-ы пишут в этот сегмент прямо сейчас. .index и .timeindex обновляются по мере накопления записей.
время / офсет
Роллинг сегмента
Kafka закрывает активный сегмент и создаёт новый (roll) при выполнении одного из условий:
Параметр
Значение по умолчанию
Описание
log.segment.bytes
1 ГБ (1073741824)
Максимальный размер .log файла
log.roll.ms
168 часов (7 дней)
Максимальное время жизни активного сегмента
log.roll.hours
168
Альтернативный способ задать log.roll.ms
log.index.size.max.bytes
10 МБ
Максимальный размер .index файла
Когда .log файл достигает log.segment.bytes, активный сегмент закрывается и создаётся новый. Новый сегмент именуется текущим LEO (Log End Offset) партиции.
TIP
Уменьшение log.segment.bytes увеличивает количество файлов и может замедлить поиск по времени (больше сегментов для проверки). Увеличение замедляет инициализацию брокера. Значение 512 МБ — разумный компромисс для топиков с высоким throughput.
Retention: удаление старых сегментов
Kafka не удаляет отдельные записи из сегмента — она удаляет целые сегменты. Удаление применяется только к закрытым сегментам. Активный сегмент никогда не удаляется.
Политики retention:
Параметр
Описание
log.retention.hours
Удалять сегменты старше N часов (по умолчанию 168 = 7 дней)
log.retention.bytes
Удалять сегменты, если суммарный размер превышает N байт
log.retention.ms
Альтернатива log.retention.hours (миллисекунды)
Когда применяется log.retention.bytes, Kafka начинает удалять самые старые сегменты, пока суммарный размер данных партиции не опустится ниже лимита.
WARNING
Гранулярность удаления — это целый сегмент, а не отдельные сообщения. Если сегмент содержит сообщение, которому только что исполнилось retention.hours, но он содержит также более свежие сообщения — сегмент не удаляется, пока все его сообщения не устареют. Поэтому фактическое время хранения может быть до retention + segment.bytes/throughput дольше указанного.
leader-epoch-checkpoint
В директории партиции также хранится файл leader-epoch-checkpoint. Он записывает историю смены эпох лидера:
20 01 5000
Каждая строка — это (epoch, startOffset). Этот файл используется при восстановлении реплики: фолловер сравнивает свою историю эпох с лидером и определяет, нужно ли выполнить truncation для восстановления консистентности.
Проверка знанийKnowledge check
Почему .index файл в Kafka разреженный (sparse), а не содержит позицию каждого офсета?
ОтветAnswer
Хранение позиции каждого офсета потребовало бы памяти O(N), где N — число сообщений. Для миллиардов сообщений это сотни гигабайт только для индексов. Разреженный индекс: (1) хранится компактно — каждые 4096 байт данных одна запись; (2) полностью помещается в RAM брокера; (3) бинарный поиск находит ближайший индексированный offset за O(log N), после чего линейное чтение .log файла добирает нужный offset. Потеря в скорости поиска минимальна, выигрыш в памяти — многократный.
Доступ закрыт
Требуется вход
Для доступа к материалам курса необходимо войти через Telegram
Проверьте понимание
Результат: 0 из 0
Концептуальный
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс