Current state: Under Discussion
Discussion thread: here
JIRA: here
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
The log.segment.bytes broker config (and its topic-level synonym segment.bytes) is currently defined as ConfigDef.Type.INT, capping the maximum segment size at Integer.MAX_VALUE (2,147,483,647 bytes, approximately 2 GB). Additionally, the .index file format stores physical file positions as 4-byte signed integers, which also cannot address beyond approximately 2 GB.
With modern storage hardware (multi-TB NVMe drives) and high-throughput workloads, the 2 GB cap is increasingly a problem:
.log, .index, .timeindex, .txnindex). A 10 TB partition with 2 GB segments means approximately 20,000 open files.Allowing segments of 4 GB, 8 GB, or larger would significantly reduce these overheads for high-throughput, large-retention workloads.
| Config | Current type | New type | Current range | New range |
|---|---|---|---|---|
log.segment.bytes (broker) | INT | LONG | [1 MB, 2,147,483,647] | [1 MB, Long.MAX_VALUE] (after MetadataVersion finalization) |
segment.bytes (topic) | INT | LONG | [1 MB, 2,147,483,647] | [1 MB, Long.MAX_VALUE] (after MetadataVersion finalization) |
The expanded range (values greater than Integer.MAX_VALUE) is gated by MetadataVersion. Before finalization, the effective range remains [1 MB, Integer.MAX_VALUE]. After IBP_4_4_IV1 is finalized, the range becomes [1 MB, Long.MAX_VALUE].
Index size guidance: With 12-byte entries, log.index.size.max.bytes (default 10 MB) holds approximately 873K entries. For an 8 GB segment with the default log.index.interval.bytes of 4096 bytes, approximately 2M index entries would be needed, requiring approximately 24 MB of index space. Operators using segments larger than 2 GB should increase log.index.size.max.bytes proportionally.
Offset index (.index) file format -- new 12-byte entry format (gated by MetadataVersion):
| Field | Legacy format (8 bytes per entry) | Large format (12 bytes per entry) |
|---|---|---|
| Relative offset | 4-byte signed int | 4-byte signed int |
| Physical position | 4-byte signed int (max approximately 2 GB) | 8-byte signed long (effectively unlimited) |
The large format is only written after MetadataVersion IBP_4_4_IV1 is finalized. Before finalization, all index files use the legacy 8-byte format.
Format detection for existing files: When opening an existing index file, the OffsetIndex auto-detects the format by analyzing the file size and validating entry content:
useLargeFormat hint from MetadataVersion is used as the tiebreaker and a warning is logged for operator visibility.sanityCheck() and recover().This auto-detection enables:
LogSegment.recover()), they are written in the new 12-byte format.Time index (.timeindex) -- no format change. Entry size is already 12 bytes (8-byte timestamp + 4-byte relative offset). No physical positions are stored.
Transaction index (.txnindex) -- no format change. Uses FileChannel directly with long positions.
| Class | Member | Before | After |
|---|---|---|---|
LogConfig | DEFAULT_SEGMENT_BYTES | int | long |
LogConfig | segmentSize() | returns int | returns long |
LogConfig | initFileSize() | returns int | returns long |
LogConfig | useLargeIndexFormat | N/A (new) | boolean, default false |
AbstractKafkaConfig | logSegmentBytes() | returns Integer via getInt() | returns Long via getLong() |
RollParams | maxSegmentBytes | int | long |
OffsetIndex | append(long offset, ...) | int position | long position |
OffsetIndex | detectEntrySize(File, int) | N/A (new) | static method, auto-detects format for existing files |
OffsetPosition | position field | int | long |
FileRecords | internal size field | AtomicInteger | AtomicLong |
LazyIndex | forOffset(...) | no format param | new overload with useLargeFormat parameter |
LogSegment | new sizeInBytesLong() | N/A | returns long |
LogSegment | recover() | returns int | returns long |
LogSegment | truncateTo() | returns int | returns long |
LogOffsetMetadata | relativePositionInSegment | int | long |
SegmentPosition (raft) | relativePosition | int | long |
RemoteStorageManager | fetchLogSegment(metadata, int) | only overload | @Deprecated; new default method with long added |
RemoteStorageManager | fetchLogSegment(metadata, int, int) | only overload | @Deprecated; new default method with long, long added |
SegmentPosition change rationale: SegmentPosition.relativePosition (raft module) is widened from int to long because it is constructed from LogOffsetMetadata.relativePositionInSegment, which is now long. Keeping types consistent avoids lossy truncation. In practice, KRaft metadata segments are small (approximately 1 GB), so positions would not exceed Integer.MAX_VALUE, but type consistency prevents subtle bugs.
BaseRecords.sizeInBytes() remains int (449 callers across 89 files -- cascading this change is too large for this KIP). FileRecords adds a new sizeInBytesLong() method that returns the true long size. The existing sizeInBytes() clamps at Integer.MAX_VALUE. All internal storage layer callers have been migrated to sizeInBytesLong(). The 449 client/network layer callers deal with fetch responses and producer batches bounded by max.message.bytes (int), so clamping does not affect them.RecordBatch.sizeInBytes() remains int (bounded by max.message.bytes).MemoryRecords.sizeInBytes() remains int (bounded by ByteBuffer capacity).transaction.state.log.segment.bytes, offsets.topic.segment.bytes, share.coordinator.state.topic.segment.bytes) remain INT.RemoteLogSegmentMetadata.segmentSizeInBytes remains int (schema uses int32). For segments larger than 2 GB, the size in metadata is clamped to Integer.MAX_VALUE and a warning is logged. This will be addressed in a follow-up schema evolution.No new metrics are added. Existing segment size metrics will report accurate values for segments larger than 2 GB because LogSegments.sizeInBytes() uses long arithmetic internally.
kafka-log-dirs.sh and DumpLogSegments correctly handle segments larger than 2 GB. DumpLogSegments uses auto-detection for index format since it does not have access to MetadataVersion.
Change log.segment.bytes and segment.bytes from ConfigDef.Type.INT to ConfigDef.Type.LONG. Apply atLeast(1024 * 1024) as the validator. The expanded range (values greater than Integer.MAX_VALUE) is gated by MetadataVersion IBP_4_4_IV1.
Widen internal storage layer types from int to long for segment sizes and physical file positions:
FileRecords: Internal AtomicInteger changed to AtomicLong for size tracking. New sizeInBytesLong(), sliceLong(), truncateToLong() methods added for callers that need long precision. The existing BaseRecords.sizeInBytes() interface remains int to avoid cascading changes across 449 call sites.
LogSegment: New sizeInBytesLong() alongside existing size(). Methods recover(), append(), shouldRoll(), read(), and truncateTo() widened to use long for positions and sizes. All internal callers migrated from size() to sizeInBytesLong():
| Call site | Before | After |
|---|---|---|
LogSegment.append() physicalPosition | int log.sizeInBytes() | long log.sizeInBytesLong() |
LogSegment.shouldRoll() | int size = size() | long size = sizeInBytesLong() |
LogSegment.read() startPosition | int | long |
LogSegment.read() fetchSize | (int)(maxPosition - startPosition) | (int) Math.min(maxPosition - startPosition, (long) adjustedMaxSize) |
LogSegment.recover() validBytes | int | long |
LogSegment.truncateTo() return | int | long |
LogSegment.toString() | size() | sizeInBytesLong() |
UnifiedLog retention | segment.size() | segment.sizeInBytesLong() |
LocalLog.updateLogEndOffset() | segment.size() | segment.sizeInBytesLong() |
LocalLog.read() maxPosition | segment.size() | segment.sizeInBytesLong() |
LocalLog.splitOverflowedSegment() | int totalSize | long totalSize |
Cleaner.groupSegmentsBySize() | segment.size() | segment.sizeInBytesLong() |
LogSegments.sizeInBytes() | LogSegment::size | LogSegment::sizeInBytesLong |
LogLoader initial metadata | activeSegment.size() | activeSegment.sizeInBytesLong() |
The OffsetIndex supports two entry formats, controlled by a useLargeFormat constructor parameter:
| Format | Entry size | Layout | When used |
|---|---|---|---|
| Legacy (default) | 8 bytes | [4-byte relative offset] [4-byte physical position] | Before IBP_4_4_IV1 finalization |
| Large | 12 bytes | [4-byte relative offset] [8-byte physical position] | After IBP_4_4_IV1 finalization |
Format selection mechanism:
The format of each OffsetIndex instance is determined through a layered selection process with three distinct code paths:
Path 1 -- Production (local segments): The primary format selection comes from MetadataVersion via LogConfig.useLargeIndexFormat. When a broker starts or creates a new segment, LogSegment.open() reads LogConfig.useLargeIndexFormat and passes it to LazyIndex.forOffset(useLargeFormat). When the lazy index is first accessed, LazyIndex.loadIndex() constructs an OffsetIndex with the useLargeFormat flag. Before IBP_4_4_IV1 is finalized, this flag is always false and all indexes use the legacy 8-byte entry format. After finalization, the flag becomes true and new indexes are written in the 12-byte format.
Path 2 -- Remote Index Cache (tiered storage): RemoteIndexCache fetches offset index files from remote storage and creates OffsetIndex instances using the default constructor with useLargeFormat=false. Since remote index files were uploaded at whatever format was active when the segment was copied to remote storage, the actual format is determined by auto-detection from the file content (described below). The useLargeFormat=false hint serves as a safe default for the ambiguous case.
Path 3 -- CLI Tools (DumpLogSegments): Offline tools like DumpLogSegments do not have access to MetadataVersion. They create OffsetIndex with useLargeFormat=false and rely entirely on auto-detection to determine the correct format from the file content.
Auto-detection for existing files: All three paths converge at the OffsetIndex constructor, which calls detectEntrySize(file, requestedEntrySize). For new files (file does not exist yet), the requested entry size is used directly. For existing files with data, the format is auto-detected:
requestedEntrySize (from MetadataVersion) is used as the tiebreaker and a warning is logged so operators have visibility into the ambiguous detection.sanityCheck() will detect the corruption and trigger a rebuild via recover().Constructor safety: The AbstractIndex base class is enhanced with an effectiveEntrySize() method that safely resolves the entry size at construction time. This avoids calling the overridable entrySize() method from the constructor, which would fail because subclass fields are not yet initialized when the parent constructor runs.
Bounds check in legacy mode: When writing entries in legacy mode, OffsetIndex.append() checks that the physical position does not exceed Integer.MAX_VALUE and throws IllegalArgumentException if it does. This provides a clear error message ("Finalize MetadataVersion to IBP_4_4_IV1 to enable large index format") instead of silently truncating the position via an (int) cast.
A new MetadataVersion entry gates the format change:
IBP_4_4_IV1(32, "4.4", "IV1", true) // didMetadataChange=true
isLargeIndexFormatSupported() helper method returns true when the cluster MetadataVersion >= IBP_4_4_IV1.didMetadataChange=true ensures downgrade is blocked after finalization, consistent with existing KRaft downgrade rules.New default methods added to the RemoteStorageManager interface with long position parameters:
default InputStream fetchLogSegment(RemoteLogSegmentMetadata metadata, long startPosition)
default InputStream fetchLogSegment(RemoteLogSegmentMetadata metadata, long startPosition, long endPosition)
These delegate to the existing int methods with bounds checking. Existing RemoteStorageManager implementations continue to work unchanged. The old int methods are marked @Deprecated.
RemoteIndexCache compatibility: RemoteIndexCache creates OffsetIndex instances from fetched remote index files. Since remote indexes may be in either format (depending on whether the segment was uploaded before or after MetadataVersion finalization), RemoteIndexCache uses the default constructor (useLargeFormat=false) and relies on auto-detection to determine the correct format from the file content.
kafka-features.sh upgrade --release-version 4.4. LogConfig.useLargeIndexFormat becomes true. New index files are written in 12-byte format. Existing 8-byte index files continue to be read correctly via auto-detection. When an index is rebuilt (for example during LogSegment.recover()), it is written in the new format. The segment.bytes config upper bound is lifted.IBP_4_4_IV1 has didMetadataChange=true, consistent with existing KRaft downgrade rules.INT values stored as strings (for example "1073741824") parse correctly as LONG. No user action required.recover() is triggered.int methods. The new long default methods delegate to the old int methods with bounds checking. No changes required for existing RSM plugins.segment.bytes back to 2 GB or less and wait for segment rolls before downgrading.RemoteStorageManager.fetchLogSegment(RemoteLogSegmentMetadata, int) is deprecated in favor of fetchLogSegment(RemoteLogSegmentMetadata, long).RemoteStorageManager.fetchLogSegment(RemoteLogSegmentMetadata, int, int) is deprecated in favor of fetchLogSegment(RemoteLogSegmentMetadata, long, long).segment.bytes above the current default (1 GB) are completely unaffected.MetadataVersion to IBP_4_4_IV1.RemoteStorageManager implementations should migrate to the long overloads at their convenience. The deprecated int methods continue to work.LONG type works correctly for log.segment.bytes and segment.bytes.useLargeFormat=true), and that indexes written by new code in legacy mode are readable by old code.sizeInBytesLong() returns accurate values exceeding Integer.MAX_VALUE, and truncateToLong() handles truncation amounts exceeding Integer.MAX_VALUE.shouldRoll() works with maxSegmentBytes greater than 2 GB, sizeInBytesLong() consistency, and recovery preserves all records.long-param fetchLogSegment() methods work correctly.updateTopicConfig, produces more records verifying the new segment size takes effect, bounces the broker, and consumes all records to verify data integrity across mixed segment sizes spanning both local and remote tiers.Rejected. The 2 GB limit is an artificial constraint from a type choice made when storage hardware was smaller. Modern deployments routinely manage multi-TB partitions where 2 GB segments create excessive overhead in file handles, segment rolls, compaction cycles, and tiered storage operations.
Rejected. Maintaining two configs for the same purpose adds confusion for operators. A single config with a type change is cleaner and follows the precedent set by KIP-1161, which reclassified several configs from STRING to LIST type.
Rejected. The relative offset (4 bytes) is sufficient because it represents the delta from the segment base offset, not an absolute offset. Only the physical position needs widening to 8 bytes. Using 16 bytes per entry would waste 50% more space for no practical benefit.
Rejected. Java does not natively support unsigned integers, making the code error-prone (values above Integer.MAX_VALUE appear negative, breaking comparison operators and binary search). The additional 2 GB headroom is not worth the complexity. Widening to long is the clean solution and future-proofs the format.
Rejected as the complete approach. While Phase 1 (config type change with INT range cap) is useful as a stepping stone, it does not deliver the actual user-facing value of larger segments. A single KIP covering the full scope ensures the community reviews the complete design, even though implementation can be phased across multiple PRs.
Rejected. An earlier prototype used a .index_version marker file in each partition directory to track whether indexes had been rebuilt in the new format. On first startup after upgrade, if the marker was absent, all indexes were rebuilt. This approach was rejected because:
Rejected as the primary mechanism. Adding a version byte at the start of each index file would make format detection unambiguous, but:
The chosen approach (MetadataVersion gating as primary, content-based auto-detection as fallback) achieves the same reliability without modifying the file layout. Auto-detection is only needed for edge cases (tools without MetadataVersion access, remote indexes in unknown format).
Rejected. An earlier iteration relied solely on file-content analysis to determine the index format. This was found to be unreliable because:
useLargeFormat hint, making auto-detection non-deterministic.Auto-detection is retained as a fallback safety net for tools and remote indexes, but the primary format selection comes from MetadataVersion via LogConfig.useLargeIndexFormat.