How does Kafka use memory-mapped files (mmap) for its offset and time indexes, and how do consumers find a record by offset?
answer
- .index offset->position, .timeindex ts->offset
- mmap = MappedByteBuffer, page cache
- sparse: entry per index.interval.bytes (4KB)
- binary search index, then sequential scan .log
- crash -> rebuild index from .log on startup
basics
~20 sEach log segment has sparse index files (.index for offsets, .timeindex for timestamps) that Kafka memory-maps (mmap) so lookups happen in RAM. To find an offset, Kafka binary-searches the sparse index to get a nearby file position, then scans forward in the .log file.
solid answer
~50 sAlongside each `.log` segment, Kafka maintains an `.index` file (relative-offset -> physical byte position) and a `.timeindex` file (timestamp -> offset). These index files are **memory-mapped** via `MappedByteBuffer` (mmap), so the broker reads and updates them as if they were in-memory arrays while the kernel pages them through the page cache. The indexes are **sparse**: they hold an entry only every `index.interval.bytes` (default 4 KB) of log, keeping them tiny. To locate a record at offset N, Kafka picks the segment whose base offset brackets N (the segment file is named by its base offset), **binary-searches** the mapped offset index to find the largest indexed offset <= N and its byte position, then **scans the `.log` sequentially** from there to the exact record. The time index supports `offsetsForTimes` and time-based retention. mmap avoids explicit read/write syscalls per lookup and gives near-memory-speed access; the trade-off is that mmap'd dirty pages rely on the OS to flush them, so an unclean crash can lose recent index entries (rebuilt on restart).
go deeper
Know Kafka has index files so it can jump near an offset instead of scanning from the start.
Describe .index/.timeindex, sparseness, and the binary-search-then-scan lookup.
Explain mmap/MappedByteBuffer, index.interval.bytes, segment base-offset naming, and crash rebuild.
Reason about memory/recovery trade-offs of sparse mmap indexes and tuning (segment.index.bytes, recovery threads).
## What needs indexing A partition segment is a big `.log` file of records in offset order. A consumer asks 'give me records starting at offset N'. Scanning the whole file from the start to find N would be O(file size). Kafka needs a fast offset -> file-position lookup. It also needs timestamp -> offset for `offsetsForTimes()` and time-based retention. ## The index files For each segment Kafka keeps: - **`.index`** — maps a **relative offset** (offset minus the segment base offset, stored as 4 bytes) to a **physical byte position** in the `.log` (4 bytes). 8 bytes per entry. - **`.timeindex`** — maps a **timestamp** (8 bytes) to a **relative offset** (4 bytes). - (`.txnindex` for transactions, separately.) ## Sparse, not dense The indexes are **sparse**: Kafka adds an entry only after roughly every `index.interval.bytes` (default 4096) of appended log data, not for every record. This keeps the index small enough to stay resident in memory and mapped cheaply. The cost is that a lookup lands you *near* the target, and you finish with a short sequential scan of the `.log`. ## Memory mapping (mmap) Kafka opens these index files as **`MappedByteBuffer`** (Java's mmap). `mmap` maps the file's bytes into the process's virtual address space; reads/writes become ordinary memory accesses, and the kernel transparently pages data in/out through the **page cache**. Benefits: - No explicit `read()`/`write()` syscall per index access — just pointer arithmetic into the buffer. - The index lives in page cache, so hot indexes are effectively RAM-resident. - Appending an index entry is a memory write; the OS handles persistence. The **active segment's** index is mapped read-write (entries are appended as the log grows); rolled (inactive) segment indexes are typically mapped read-only and trimmed to their exact size. ## The lookup algorithm (offset -> record) 1. **Select segment**: segment files are named by their base offset (e.g. `...0000000368.log`). Kafka finds the segment whose base offset is the largest <= N (a lookup over a sorted skip-list of segments). 2. **Binary search the index**: within that segment's mapped `.index`, binary-search for the largest indexed (relative) offset <= N; read its byte position P. 3. **Sequential scan**: open the `.log` at position P and scan forward record-by-record until reaching offset N (or the first offset >= N). The scan is short because entries are at most `index.interval.bytes` apart. Time lookups (`.timeindex`) work the same way: binary-search by timestamp to get an offset, then resolve the offset via the offset index. ## Edge cases & gotchas - **Crash recovery**: mmap'd dirty index pages are flushed by the OS lazily. After an unclean shutdown, the tail of an index may be missing or corrupt; on startup Kafka **sanity-checks and rebuilds** indexes from the `.log` if needed (recovery can be slow for large logs; `num.recovery.threads.per.data.dir` helps). - **Index size**: `segment.index.bytes` (default 10 MB) caps the index; a segment can roll early if its index fills. - **Why sparse + scan, not a dense map**: a dense per-record index would bloat memory; sparse + sequential scan exploits the fact that the `.log` is already sequential and page-cache-friendly. - mmap on 32-bit address spaces is limited, but Kafka runs on 64-bit so address space is a non-issue.
- Why are Kafka's indexes sparse instead of one entry per record?A dense index would consume far more memory and disk; sparse entries (every index.interval.bytes) keep the index tiny and RAM-resident, and the small leftover gap is closed by a short sequential scan of the already-sequential log.
- What happens to mmap'd indexes after an unclean broker crash?The tail may be unflushed or corrupt; on restart Kafka validates and rebuilds the affected indexes from the .log segment, which is why recovery of large dirty logs can be slow.
saying these in an interview costs you the question
- Saying the index maps every single record's offset (it is sparse).
- Claiming a lookup is purely a hash-map O(1) with no scan.
- Confusing mmap (used for indexes) with sendfile (used for the read-to-socket path).
- Thinking the .log file itself is mmap'd for serving consumer fetches (it is read via FileChannel/sendfile, not mmap'd for that path).