Kafka Partitioned Log — Architecture, Replication, and Exactly-Once Semantics
Kafka Fundamentals — The Partitioned Log
Core Abstraction
Kafka is a **distributed, partitioned, replicated commit log**. Topics are split into **partitions** (ordered, immutable sequences of records). Each partition is a **segmented log file** on disk.
Record Structure
Record {
key: bytes (nullable) // Partitioning key
value: bytes // Payload
timestamp: int64 // CreateTime / LogAppendTime
headers: Header[] // Key-value metadata
offset: int64 // Position in partition (monotonic)
}
Topic → Partition → Segment
- Topic — Logical category (e.g.,
orders). - Partition — Physical shard (e.g.,
orders-0,orders-1). Ordered, append-only. - Segment — File on disk (
00000000000000000000.log,.index,.timeindex,.snapshot). Default 1GB.
Offsets — Two Numbering Spaces
| Space | Description | Use |
|---|---|---|
| Log Offset (Base Offset) | Absolute position in partition (0, 1, 2…) | Consumer position, replication |
| Epoch + Relative Offset | Leader epoch + offset within epoch | Leader election, truncation |
Each leader election increments **Leader Epoch**. Offsets reset per epoch. Prevents stale leader data.
Broker Architecture — Storage, Network, Request Pipeline
Storage Tiering (Kafka 3.6+ / Tiered Storage)
- Local tier — Broker disks (NVMe/SSD). Hot data. Fast reads/writes.
- Remote tier — S3, GCS, Azure Blob, HDFS. Cold segments. Cost-efficient.
- Segment migration — Based on
remote.log.storage.enable,local.retention.bytes,local.retention.ms.
Request Pipeline
- Acceptor — Accepts TCP connections (default port 9092).
- Processor — Thread pool (
num.network.threads, default 3). Reads requests, parses. - Request Handler — Thread pool (
num.io.threads, default 8). Executes logic (produce, fetch, metadata). - Purgatory — Holds delayed requests (acks=all waiting for ISR, throttling).
- Response Queue — Serializes responses back to processor.
Page Cache Reliance
Kafka **relies heavily on OS page cache**. Writes go to page cache (via mmap or FileChannel). Flush controlled by flush.messages / flush.ms (default: OS decides). **Zero-copy transfer** (sendfile) for consumer reads.
Replication — In-Sync Replicas (ISR)
Replication Factor & ISR
replication.factor (RF) = total replicas per partition. One **Leader**, rest **Followers**. **ISR** = replicas caught up to leader (within replica.lag.time.max.ms, default 30s).
ISR Protocol
- Leader writes to local log.
- Followers fetch via
FetchRequest(high watermark = leader’s LEO). - Follower acks up to its LEO.
- Leader updates ISR: if follower lag > threshold → **shrink ISR** (remove). If caught up → **expand ISR** (add).
- Controller (KRaft) or ZooKeeper persists ISR changes to
/brokers/topics//partitions//state.
acks Configuration — Durability vs Latency
| Setting | Behavior | Durability | Latency |
|---|---|---|---|
acks=0 |
Fire-and-forget. No ack. | None (data loss on leader fail) | Lowest |
acks=1 |
Leader writes local, acks immediately. | Leader only (loss if leader fails before replication) | Low |
acks=all / -1 |
Leader waits for **full ISR** to ack. | Strong (survives any single failure if RF≥3) | Higher |
Default: acks=all (since 3.0). Min ISR: min.insync.replicas (default 1). If ISR shrinks below min → producer gets NOT_ENOUGH_REPLICAS.
Leader Election — Controller / KRaft
**Controller** (single active) monitors broker heartbeats. On leader failure:
- Controller detects (ZooKeeper watch / KRaft metadata log).
- Selects new leader from **current ISR** (preferred:
preferred.replica.election.enable). - Sends
LeaderAndIsrRequestto new leader and followers. - New leader truncates log to high watermark, starts accepting writes.
**Unclean Leader Election** (unclean.leader.election.enable=false default): If no ISR, don’t elect out-of-sync replica (data loss). Enable only for availability-over-durability.
Consumer Groups — Cooperative Rebalancing
Group Protocol
Consumers join group via JoinGroup → SyncGroup. **Group Coordinator** (broker) assigns partitions.
Partition Assignment Strategies
| Strategy | Algorithm | Characteristics |
|---|---|---|
| Range (legacy) | Per-topic, contiguous ranges | Simple, but uneven if partitions not multiple of consumers |
| RoundRobin (legacy) | Distribute partitions evenly across all topics | Better balance, but full rebalance on any change |
| Sticky (default 2.4+) | Preserve existing assignment, minimize movement | Incremental rebalance, near-optimal balance |
| CooperativeSticky (3.0+) | Sticky + **cooperative** (no stop-the-world) | Best — only revoked partitions pause |
Cooperative Rebalancing (Incremental)
- Coordinator sends
REVOKEonly for partitions moving. - Consumer releases only those partitions, commits offsets.
- Coordinator sends
ASSIGNfor new partitions. - Consumer starts fetching new partitions. **No full pause**.
Offset Management
- Auto-commit (
enable.auto.commit=true, default): Periodic (5s) async commit. Risk: duplicate processing on crash. - Manual commit (
commitSync()/commitAsync()): Exact control. Pair with idempotent processing. - Transaction commit (see below): Offsets committed atomically with output.
Consumer Lag Monitoring
# kafka-consumer-groups.sh --bootstrap-server broker:9092 --group my-group --describe
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID
my-group orders 0 1000 1050 50 consumer-1
my-group orders 1 995 1040 45 consumer-2
Exactly-Once Semantics — Transactions
Problem: Duplicate Writes
Producer retries (network timeout) → broker may have written but ack lost → duplicate. Consumer reprocesses on rebalance → duplicate.
Solution: Idempotent Producer + Transactions
Idempotent Producer (enable.idempotence=true)
- Producer gets **PID** (Producer ID) and **sequence number** per partition.
- Broker tracks
(PID, partition, seq)— deduplicates on retry. - Requires
acks=all,max.in.flight.requests.per.connection=5(default). - **Per-partition deduplication only** — not cross-partition.
Transactions — Atomic Multi-Partition Write
producer.initTransactions()
producer.beginTransaction()
producer.send(new ProducerRecord("orders", key, value))
producer.send(new ProducerRecord("inventory", key, value))
producer.sendOffsetsToTransaction(
Map.of(new TopicPartition("orders", 0), new OffsetAndMetadata(1001)),
"consumer-group-id"
)
producer.commitTransaction() // or abortTransaction()
Transaction Log — __transaction_state
- Internal topic (50 partitions default). Stores transaction state:
Ongoing,PrepareCommit,Commit,Abort. - Coordinator (broker) manages transaction lifecycle.
- **Two-phase commit**: Producer sends
PrepareCommit→ Coordinator writes to transaction log → Coordinator writesCommit→ Producer receives ack. - On failure: Coordinator aborts (writes
Abort). Consumers seeAbortmarker → skip.
Transactional Consumer — Read Committed
props.put("isolation.level", "read_committed") // Default: read_uncommitted
// Only returns records from committed transactions.
// Aborted transaction records filtered out (consumer sees control records).
Exactly-Once End-to-End
- Producer:
enable.idempotence=true+ Transactions. - Consumer:
isolation.level=read_committed+ manual offset commit inside transaction (sendOffsetsToTransaction). - Downstream: Idempotent writes (deduplication key) or transactional sink.
KRaft Mode — Kafka Without ZooKeeper
Architecture
Metadata (topics, partitions, ISR, configs, ACLs) stored in **Kafka’s own Raft-based metadata log** (__cluster_metadata topic). **Controller** = Raft leader. **Brokers** = Raft followers (voters) or observers.
Migration Path
- Enable KRaft in ZooKeeper cluster:
metadata.log.dir,controller.quorum.voters. - Start controller quorum (3+ nodes).
- Migrate brokers:
process.roles=broker(orbroker,controllerfor combined). - Remove ZooKeeper.
Benefits
- Single system — no ZooKeeper ops, version skew, split-brain.
- Faster controller failover (Raft election ~ms vs ZK ~seconds).
- Scalable metadata (millions of partitions).
- Simpler security (single auth system).
Production Operations & Tuning
Key Broker Configs
# Network / Threads
num.network.threads=8
num.io.threads=16
socket.request.max.bytes=104857600 # 100MB
# Log / Retention
log.dirs=/mnt/kafka/data1,/mnt/kafka/data2 # Multiple dirs = parallel I/O
log.segment.bytes=1073741824 # 1GB segments
log.retention.hours=168 # 7 days
log.retention.bytes=-1 # No size limit
log.roll.hours=168
# Replication
replica.fetch.max.bytes=1048576
replica.fetch.wait.max.ms=500
replica.lag.time.max.ms=30000
# Flush (rely on page cache mostly)
flush.messages=10000
flush.ms=1000
# Compression (producer side default)
compression.type=snappy # or zstd for better ratio
# Quotas (prevent noisy neighbors)
quota.window.size.seconds=1
quota.window.num=10
Monitoring — Critical Metrics
| Metric | Alert | Meaning |
|---|---|---|
kafka.server.ReplicaManager.UnderReplicatedPartitions |
> 0 | Partitions not fully replicated — broker down, disk slow |
kafka.server.ReplicaManager.OfflinePartitionsCount |
> 0 | No leader — data unavailable |
kafka.network.RequestMetrics.RequestsPerSec |
Spike | Traffic anomaly |
kafka.log.LogFlushStats.FlushLatencyMs |
p99 > 100ms | Disk flush bottleneck |
kafka.consumer.FetchManager.FetchLatencyMs |
p99 > 500ms | Consumer falling behind |
kafka.coordinator.group.GroupCoordinator.Members |
Flapping | Consumer rebalance storms |
kafka.controller.KafkaController.ActiveControllerCount |
!= 1 | No controller / split brain |
Operational Procedures
Rolling Restart
- Set
controlled.shutdown.enable=true(default). - Stop broker → controller moves partitions (preferred replica election).
- Start broker → rejoins ISR, catches up.
- One at a time. Monitor
UnderReplicatedPartitions= 0 between restarts.
Partition Reassignment (Rebalance)
# Generate reassignment plan
kafka-reassign-partitions.sh --bootstrap-server broker:9092
--topics-to-move-json-file topics.json
--broker-list "1,2,3" --generate
# Execute
kafka-reassign-partitions.sh --bootstrap-server broker:9092
--reassignment-json-file reassignment.json --execute
# Verify
kafka-reassign-partitions.sh --bootstrap-server broker:9092
--reassignment-json-file reassignment.json --verify
Log Compaction (Key-Based Retention)
# Topic config
kafka-configs.sh --bootstrap-server broker:9092
--entity-type topics --entity-name user-profiles
--alter --add-config cleanup.policy=compact,min.cleanable.dirty.ratio=0.5
Keeps latest value per key. Used for changelog, materialized views, KTables.
TL;DR — Key Takeaways
- Kafka = **Partitioned, replicated commit log**. Partitions = unit of parallelism & ordering.
- ISR = in-sync replicas.
acks=allwaits for full ISR.min.insync.replicasenforces durability. - CooperativeSticky assignor = incremental rebalances (no stop-the-world).
- Transactions = idempotent producer + transaction log + read_committed consumer = exactly-once.
- KRaft = native Raft metadata, no ZooKeeper. Standard since 3.0.
- Tiered Storage = local NVMe + remote S3. Infinite retention at low cost.
- Monitor:
UnderReplicatedPartitions,OfflinePartitions,ConsumerLag,ControllerCount.
Next in series: Linux RCU Synchronization — Read-Copy-Update internals and usage patterns.