2.2 Log Segments, Indexes & Storage Layout
Module Overview
Goal: Deep dive into Kafka's storage engine — how data is actually stored and retrieved.
What You'll Learn:
- Segment file structure and rolling
- Offset index internals
- Time index internals
- File layout on disk
- Fast lookup mechanics
- Segment lifecycle
What's NOT Covered Here:
- Compaction algorithm (covered in Module 2.3)
- Replication mechanics (covered in Module 3.x)
- Topic configuration strategy (covered in Operations)
Key Insight: Kafka's storage engine is highly optimized for sequential I/O and fast offset lookups. Understanding segments and indexes explains why Kafka performs so well.
1. Why Segments?
The Problem with a Single File
IF PARTITION USED ONE HUGE FILE:
Problems:
├─ File grows unbounded
├─ Deletion of old data is expensive (rewrite entire file)
├─ Index becomes huge (slow lookups)
├─ Recovery takes forever (must scan entire file)
└─ OS limits on file size
SOLUTION: Split log into segments
Benefits:
├─ Delete old segments cheaply (just rm file)
├─ Smaller indexes (per segment)
├─ Faster recovery (only active segment needs validation)
└─ Easier to manage (rotate segments)
2. Segment File Structure
Segment Components
EACH SEGMENT CONSISTS OF 3 FILES:
00000000000012345678.log
└─ Actual message data (records)
00000000000012345678.index
└─ Offset index (offset → file position)
00000000000012345678.timeindex
└─ Timestamp index (timestamp → offset)
Filename = base offset (20 digits, zero-padded)
Log Segment File Layout
DIRECTORY VIEW:
/var/kafka/data/orders-0/
├─ 00000000000000000000.log # Base offset: 0
├─ 00000000000000000000.index
├─ 00000000000000000000.timeindex
│
├─ 00000000000001000000.log # Base offset: 1000000
├─ 00000000000001000000.index
├─ 00000000000001000000.timeindex
│
├─ 00000000000002000000.log # Base offset: 2000000 (active segment)
├─ 00000000000002000000.index
└─ 00000000000002000000.timeindex
Active segment:
└─ Currently being written to
└─ All new records appended here
Older segments:
└─ Immutable (no more writes)
└─ Candidates for deletion or compaction
Log File Format
LOG FILE CONTENTS (.log):
┌─────────────────────────────────────────────────────┐
│ Record Batch 1 │
│ ├─ baseOffset: 12345678 │
│ ├─ length: 1024 bytes │
│ ├─ crc: checksum │
│ ├─ attributes: compression, timestamp type │
│ ├─ lastOffsetDelta: 9 (10 records in batch) │
│ ├─ baseTimestamp: 1234567890000 │
│ └─ records: │
│ ├─ Record 0 (offsetDelta=0, key, value) │
│ ├─ Record 1 (offsetDelta=1, key, value) │
│ └─ ... │
├─────────────────────────────────────────────────────┤
│ Record Batch 2 │
│ ├─ baseOffset: 12345688 │
│ └─ ... │
└─────────────────────────────────────────────────────┘
Record Batch = multiple records compressed together
└─ Produced by producer batching
└─ Kafka stores batches as-is (no re-batching)
3. Segment Rolling
When to Create New Segment
SEGMENT ROLLING TRIGGERS:
1. Size-based (segment.bytes):
└─ Current segment >= 1 GB (default)
2. Time-based (segment.ms):
└─ Current segment open for >= 7 days (default)
3. Index full (segment.index.bytes):
└─ Index file reaches max size (default: 10 MB)
When triggered:
├─ Close current active segment
├─ Create new segment with next offset as base
└─ New segment becomes active
Rolling Process
STEP-BY-STEP:
Before rolling:
├─ Active segment: 00000000000001000000.log (LEO: 1500000)
└─ Writing new records
Rolling triggered (size limit reached):
1. Close active segment
├─ Flush to disk
└─ Mark as immutable
2. Create new segment files:
├─ 00000000000001500000.log (base offset = LEO)
├─ 00000000000001500000.index
└─ 00000000000001500000.timeindex
3. New segment becomes active
└─ All new writes go to this segment
After rolling:
├─ Old segment: 00000000000001000000.* (immutable)
└─ Active segment: 00000000000001500000.* (accepting writes)
4. Offset Index Internals
Purpose
PROBLEM:
To find record at offset 1234567:
└─ Without index: Must scan log from beginning (slow!)
SOLUTION:
Offset index: Maps offset → file position
└─ Quickly jump to approximate location in log file
Index File Structure
OFFSET INDEX FORMAT (.index):
Binary file with fixed-size entries (8 bytes each):
┌──────────────┬──────────────┐
│ Relative │ File │
│ Offset (4B) │ Position (4B)│
├──────────────┼──────────────┤
│ 0 │ 0 │
│ 100 │ 12480 │
│ 200 │ 24960 │
│ 300 │ 37440 │
│ ... │ ... │
└──────────────┴──────────────┘
Relative offset:
└─ Offset relative to base offset of segment
└─ Saves space (4 bytes instead of 8)
Example:
├─ Segment base offset: 1000000
├─ Index entry: relative offset 100
└─ Actual offset: 1000100
Index Building
INDEX ENTRY CREATION:
Not every record has index entry!
└─ Index is sparse (sampled)
Index entry created every log.index.interval.bytes (default: 4096 bytes):
├─ After writing 4 KB of data
├─ Add entry: (current offset, current file position)
└─ Continue writing
Example:
Write records:
├─ Offset 1000: Position 0 → Index entry [0, 0]
├─ Offsets 1001-1100: No index entries (< 4 KB)
├─ Offset 1101: Position 4200 → Index entry [101, 4200]
├─ Offsets 1102-1200: No index entries
└─ ... continue
Result:
└─ Sparse index (not every offset indexed)
└─ Small index size
Lookup Algorithm
TO FIND OFFSET 1234567:
1. Determine segment
└─ Find segment with base offset <= 1234567
└─ Result: 00000000000001000000.log (base: 1000000)
2. Calculate relative offset
└─ 1234567 - 1000000 = 234567
3. Binary search in index file
└─ Find largest relative offset <= 234567
└─ Result: Entry [234500, 105820400]
4. Seek to file position 105820400
5. Scan forward from position
└─ Read records sequentially
└─ Until offset 1234567 found
6. Return record
Time complexity:
└─ O(log N) for binary search in index
└─ O(1) on average for sequential scan (small gap)
5. Time Index Internals
Purpose
PROBLEM:
Find records from timestamp 1609459200000 (Jan 1, 2021):
└─ Without time index: Must scan all records (very slow!)
SOLUTION:
Time index: Maps timestamp → offset
└─ Then use offset index to find record
Time Index File Structure
TIME INDEX FORMAT (.timeindex):
Binary file with fixed-size entries (12 bytes each):
┌──────────────┬──────────────┐
│ Timestamp │ Relative │
│ (8 bytes) │ Offset (4B) │
├──────────────┼──────────────┤
│ 1609459200000│ 0 │
│ 1609459260000│ 100 │
│ 1609459320000│ 200 │
│ ... │ ... │
└──────────────┴──────────────┘
Also sparse:
└─ Sampled at same frequency as offset index
Timestamp Lookup
TO FIND RECORDS FROM TIMESTAMP T:
1. Search all segments' time indexes
└─ Find segment where max timestamp >= T
2. Binary search in time index
└─ Find largest timestamp <= T
└─ Get corresponding offset
3. Use offset index
└─ Seek to approximate position
4. Scan forward
└─ Read records until timestamp >= T
Use case:
└─ Consumer reset to timestamp
└─ Time-based retention (find records older than N days)
6. Segment Lifecycle
State Transitions
SEGMENT STATES:
1. ACTIVE:
├─ Currently being written to
├─ Records appended
└─ Index updated
2. CLOSED:
├─ No longer active (segment rolled)
├─ Immutable
└─ Eligible for cleanup
3. DELETED:
└─ Removed from disk (retention or compaction)
TRANSITION:
Active → Closed:
└─ When rolling occurs
Closed → Deleted:
└─ When retention policy triggers deletion
Segment Deletion
RETENTION-BASED DELETION:
If segment's max timestamp < (now - retention.ms):
└─ Delete segment files:
├─ rm *.log
├─ rm *.index
└─ rm *.timeindex
Size-based deletion:
If total partition size > retention.bytes:
└─ Delete oldest segments until under limit
Deletion is atomic:
└─ All segment files deleted together
└─ Start offset of partition advances
7. Index Recovery and Rebuilding
When Indexes are Lost or Corrupted
REBUILD PROCESS:
1. Scan .log file from beginning
2. For each record batch:
├─ Read offset and timestamp
├─ Determine if index entry should be created
│ └─ Based on log.index.interval.bytes
└─ Write index entry
3. Fsync index files
4. Mark as recovered
When recovery is needed:
├─ Broker crash (indexes may be incomplete)
├─ Index corruption detected (CRC failure)
└─ Index files deleted accidentally
Broker Startup Recovery
ON BROKER START:
For each partition:
1. Load all segment files
2. For active segment only:
├─ Validate records
├─ Truncate incomplete records (if crash)
└─ Rebuild index
3. For closed segments:
└─ Indexes assumed valid (already immutable)
4. Mark partition as recovered
└─ Ready to serve
Fast recovery:
└─ Only active segment needs validation
└─ Closed segments are immutable
8. Storage Layout on Disk
Partition Directory Structure
COMPLETE VIEW:
/var/kafka/data/orders-0/
│
├─ 00000000000000000000.log # Segment 1 (log)
├─ 00000000000000000000.index # Segment 1 (offset index)
├─ 00000000000000000000.timeindex # Segment 1 (time index)
│
├─ 00000000000001000000.log # Segment 2
├─ 00000000000001000000.index
├─ 00000000000001000000.timeindex
│
├─ 00000000000002000000.log # Segment 3 (active)
├─ 00000000000002000000.index
├─ 00000000000002000000.timeindex
│
├─ leader-epoch-checkpoint # Leader epoch tracking
├─ partition.metadata # Partition metadata
│
└─ (optional files):
├─ 00000000000000000000.txnindex # Transaction index
├─ 00000000000000000000.snapshot # Snapshot (if compacted)
└─ cleaner-offset-checkpoint # Compaction progress
File Sizes
TYPICAL SIZES:
.log files:
└─ Up to segment.bytes (default: 1 GB)
└─ Can be larger if single record batch exceeds limit
.index files:
└─ Up to segment.index.bytes (default: 10 MB)
└─ Smaller if segment has fewer records
.timeindex files:
└─ Similar size to .index files
Leader-epoch-checkpoint:
└─ Small (few KB)
└─ One entry per leader epoch
Total disk usage:
└─ Primarily .log files (data)
└─ Indexes are ~1% of data size
9. Read Performance Optimizations
OS Page Cache
KAFKA RELIES ON OS PAGE CACHE:
Write path:
├─ Producer writes → Kafka → OS page cache
└─ OS flushes to disk asynchronously
Read path:
├─ Recent writes still in page cache
├─ Consumer reads → Kafka → OS page cache → return
└─ No disk I/O (served from RAM)
Benefits:
├─ No large JVM heap needed
├─ OS manages memory efficiently
├─ Zero-copy possible (sendfile)
└─ Fast access to recent data
Read-Ahead
OS READ-AHEAD:
Sequential access pattern detected:
└─ OS prefetches next pages into cache
Kafka benefits:
├─ Consumers typically read sequentially
├─ OS loads data ahead of requests
└─ Further reduces disk I/O
Tuning:
└─ Linux: /sys/block/sda/queue/read_ahead_kb
└─ Increase for consumer-heavy workloads
10. Write Performance Optimizations
Batching
KAFKA WRITE PATH:
Producer:
└─ Batches multiple records
└─ Compresses batch
└─ Sends single batch to broker
Broker:
└─ Writes entire batch to log (one operation)
└─ No re-batching (stores as-is)
Benefits:
├─ Fewer disk writes
├─ Better compression ratio
├─ Fewer index entries
└─ Higher throughput
Flush Policy
KAFKA DOES NOT FLUSH IMMEDIATELY:
Default behavior:
├─ Writes to OS page cache
├─ OS flushes to disk asynchronously (lazy)
└─ Relies on replication for durability
Configuration (not recommended):
├─ log.flush.interval.messages
├─ log.flush.interval.ms
└─ Explicit flush (reduces throughput)
Why lazy flush is OK:
└─ Replication factor = 3 means data on 3 brokers
└─ Even if 1 broker crashes, data is safe
└─ No need to wait for disk sync
11. Index Memory Mapping
mmap for Fast Access
INDEXES ARE MEMORY-MAPPED:
mmap():
└─ Maps file directly into process address space
└─ OS handles paging (load pages as needed)
Benefits:
├─ No explicit read() syscalls
├─ Indexes feel like in-memory arrays
├─ OS manages caching automatically
└─ Very fast lookups (binary search in memory)
Kafka implementation:
└─ All index files mmap'ed on broker start
└─ Lookups are simple array accesses
12. Segment Compaction (Preview)
Compacted Segment Structure
IF cleanup.policy=compact:
Segments still follow same structure:
├─ .log, .index, .timeindex files
└─ But semantics change
Compaction process:
├─ Creates new segment with only latest keys
├─ Old segment deleted
└─ Offsets preserved (gaps may appear)
Details in Module 2.3
13. Key Takeaways
You've learned:
✅ Segments split the log into manageable chunks (default: 1 GB)
✅ Each segment has 3 files: .log (data), .index (offset), .timeindex (timestamp)
✅ Offset index is sparse (sampled every 4 KB)
✅ Lookup is O(log N) binary search in index + sequential scan
✅ Segments roll based on size, time, or index size
✅ Active segment is mutable, closed segments are immutable
✅ Deletion is simple: just remove old segment files
✅ Indexes are memory-mapped for fast access
✅ OS page cache is Kafka's primary cache (not JVM heap)
✅ Recovery only validates active segment (fast startup)
14. Storage Layout Checklist
SEGMENT FILES:
☑ *.log: Actual record data
☑ *.index: Offset → file position mapping
☑ *.timeindex: Timestamp → offset mapping
☑ Filename = base offset (20 digits)
SEGMENT PROPERTIES:
☑ Active segment: Currently being written
☑ Closed segments: Immutable
☑ Segments roll based on size or time
☑ Deletion: Remove entire segment (atomic)
INDEX PROPERTIES:
☑ Sparse (not every record indexed)
☑ Fixed-size entries
☑ Memory-mapped for fast access
☑ Binary search for lookups
PERFORMANCE OPTIMIZATIONS:
☑ OS page cache (primary cache)
☑ Zero-copy (sendfile for reads)
☑ Batching (producer and broker)
☑ Sequential I/O (fast on disk)
Summary
Kafka's storage engine is optimized for throughput:
Segment Design =
├─ Split log into fixed-size segments
├─ Simple deletion (remove old segments)
├─ Fast recovery (only active segment)
└─ Manageable index sizes
Index Design =
├─ Sparse (sampled)
├─ Binary searchable
├─ Memory-mapped
└─ Small size (~1% of data)
Performance =
├─ Sequential I/O (disk-friendly)
├─ OS page cache (RAM utilization)
├─ Batching (fewer I/O operations)
└─ Zero-copy (kernel optimizations)
Remember: Kafka's storage engine trades off some complexity (multiple files per partition) for massive performance gains (sequential I/O, simple deletion, fast lookups). Understanding segments and indexes explains why Kafka can sustain millions of messages per second.
Next up: Module 2.3 — Compaction & Retention Mechanics — How Kafka manages log cleanup at the engine level.