12 KiB
Kafka KRaft Protocol Reference
Source material for writing
examples/KafkaKRaft.speck(speckl#22)
Scope
This reference covers the consensus core of KRaft — the state machine for leader election, log replication, and commit advancement. It does NOT cover:
- Full broker functionality (topic creation, partition assignment, consumer group coordination)
- Data plane (produce/fetch request handling)
- Controller-specific logic (metadata quarums, configuration management)
- Network transport (this is a logical model, not a network protocol spec)
The boundary: if it touches currentTerm, votedFor, log, commitIndex, or state transitions, it's in scope.
Sources
- Raft TLA+ Specification — Diego Ongaro, 2014. The canonical formal spec. Available at:
github.com/ongardie/raft.tla - "In Search of an Understandable Consensus Algorithm" — Ongaro & Ousterhout, 2014 (USENIX ATC). Includes safety proof.
- KIP-500: Next Generation Client Protocol — KRaft design document. URL changes; search Apache Kafka JIRA for KIP-500.
- KIP-642: Dynamic Configuration in KRaft — Configuration updates in KRaft mode.
- KIP-830: KRaft Metadata Quorum — The actual metadata quorum implementation.
- Apache Kafka source —
kafka/raft/package in the Apache Kafka repository.
Raft State Machine (from TLA+ Spec)
State Variables
Per server (i ∈ Server):
| Variable | Type | Description |
|---|---|---|
currentTerm |
Nat | Current term number (monotonically increasing) |
state |
{Follower, Candidate, Leader} | Server role |
votedFor |
Server ∪ {Nil} | Candidate voted for in current term, or Nil |
log |
Seq(LogEntry) | Sequence of log entries (index = position) |
commitIndex |
Nat | Index of highest log entry known to be committed |
nextIndex |
Server → Nat | (Leader only) Next log index to send to each follower |
matchIndex |
Server → Nat | (Leader only) Highest log index replicated on each follower |
votesResponded |
Set(Server) | (Candidate only) Servers that responded to RequestVote |
votesGranted |
Set(Server) | (Candidate only) Servers that granted vote |
Global:
| Variable | Type | Description |
|---|---|---|
messages |
Bag(Message) | In-flight messages (requests/responses) |
LogEntry Type
LogEntry == { term: Nat, value: Value }
Quorum
Quorum == {i ∈ SUBSET(Server) : Cardinality(i) * 2 > Cardinality(Server)}
Simple majority. The key property: any two quorums overlap.
Initial State
currentTerm[i] = 1
state[i] = Follower
votedFor[i] = Nil
log[i] = <<>> (empty sequence)
commitIndex[i] = 0
Actions (State Transitions)
1. Timeout → Start Election
Preconditions:
state[i] ∈ {Follower, Candidate}
Effects:
state[i] ← CandidatecurrentTerm[i] ← currentTerm[i] + 1votedFor[i] ← NilvotesResponded[i] ← {}votesGranted[i] ← {}
2. RequestVote Request (Candidate → Peer)
Preconditions:
state[i] = Candidatej ∉ votesResponded[i]
Effects:
- Send
RequestVoteRequestwith{mterm: currentTerm[i], mlastLogTerm: LastTerm(log[i]), mlastLogIndex: Len(log[i]), msource: i, mdest: j}
3. HandleRequestVoteRequest (Peer → Candidate)
Preconditions:
m.mterm ≤ currentTerm[i]
Log check (logOk):
logOk ≡ m.mlastLogTerm > LastTerm(log[i])
∨ (m.mlastLogTerm = LastTerm(log[i]) ∧ m.mlastLogIndex ≥ Len(log[i]))
Grant conditions:
grant ≡ m.mterm = currentTerm[i] ∧ logOk ∧ votedFor[i] ∈ {Nil, j}
Effects (if grant):
votedFor[i] ← j
Effects (always):
- Reply with
RequestVoteResponse{mterm: currentTerm[i], mvoteGranted: grant}
4. HandleRequestVoteResponse (Candidate receives vote)
Preconditions:
m.mterm = currentTerm[i]
Effects:
votesResponded[i] ← votesResponded[i] ∪ {j}- If
m.mvoteGranted:votesGranted[i] ← votesGranted[i] ∪ {j}
5. BecomeLeader (Candidate → Leader)
Preconditions:
state[i] = CandidatevotesGranted[i] ∈ Quorum
Effects:
state[i] ← LeadernextIndex[i] ← [j ∈ Server → Len(log[i]) + 1]for all jmatchIndex[i] ← [j ∈ Server → 0]for all j
6. AppendEntries Request (Leader → Follower)
Preconditions:
i ≠ jstate[i] = Leader
Effects:
- Send
AppendEntriesRequestwith entries fromnextIndex[i][j]tomin(Len(log[i]), nextIndex[i][j])(at most 1 entry per message in the spec) - Include
mprevLogIndex = nextIndex[i][j] - 1,mprevLogTerm,mcommitIndex
7. HandleAppendEntriesRequest (Follower receives entries)
Preconditions:
m.mterm ≤ currentTerm[i]
Log consistency check (logOk):
logOk ≡ m.mprevLogIndex = 0
∨ (m.mprevLogIndex ≤ Len(log[i]) ∧ m.mprevLogTerm = log[i][m.mprevLogIndex].term)
Effects (if logOk AND m.mterm ≥ currentTerm[i]):
- Reset to Follower if Candidate
- Update
currentTerm[i] ← m.mtermif needed - Append/conflict resolution: if
m.mprevLogIndex > 0, entries aftermprevLogIndexthat conflict are truncated commitIndex[i] ← min(m.mcommitIndex, Len(log[i]))- Reply
AppendEntriesResponse{msuccess: TRUE, mmatchIndex: ...}
Effects (if !logOk OR m.mterm < currentTerm[i]):
- Reply
AppendEntriesResponse{msuccess: FALSE, mmatchIndex: 0}
8. AdvanceCommitIndex (Leader)
Preconditions:
state[i] = Leader
Effects:
- Find highest
Nsuch thatN > commitIndex[i]and a quorum ofmatchIndex[i][k] ≥ N - Only commit entries from current term:
log[i][N].term = currentTerm[i] commitIndex[i] ← N
9. ClientRequest (Leader appends entry)
Preconditions:
state[i] = Leader
Effects:
- Append
[term: currentTerm[i], value: v]tolog[i]
10. Restart (Server crash recovery)
Effects:
state[i] ← Follower- Reset volatile state (
votesResponded,votesGranted,nextIndex,matchIndex,commitIndex) - Preserve persistent state (
currentTerm,votedFor,log)
Safety Invariants (from Raft paper)
These are the invariants we need to prove in SpeckDL:
-
Election Safety: At most one leader per term.
∀ i, j ∈ Server: state[i] = Leader ∧ state[j] = Leader ∧ currentTerm[i] = currentTerm[j] → i = j -
Leader Completeness: If a log entry is committed in term T, then all leaders for terms > T have that entry.
This is the core invariant. Proved via the "Leader Completeness Property" in the Raft paper. -
Log Matching: If two entries in different logs have the same index and term, then all preceding entries are identical.
∀ i, j ∈ Server, idx: log[i][idx].term = log[j][idx].term → log[i][1..idx] = log[j][1..idx] -
Leader Append-Only: A leader never overwrites or deletes entries in its log.
∀ i: state[i] = Leader → log[i] only grows (entries appended, never removed) -
Term Monotonicity: Current term never decreases.
∀ i: currentTerm' [i] ≥ currentTerm[i] -
Commit Safety: An entry is committed only if a quorum has replicated it.
commitIndex[i] advances only when a quorum of matchIndex[i][k] ≥ N
KRaft Extensions Over Vanilla Raft
KRaft is Kafka's adaptation of Raft for metadata management. Key differences:
- No ZooKeeper: KRaft replaces the ZooKeeper-based metadata quorum with a Raft-based one.
- Metadata log: The replicated log contains metadata records (topic configs, partition assignments), not user data.
- Controller node: KRaft has an "active controller" concept — the leader of the metadata quorum acts as the controller for the Kafka cluster.
- Snapshotting: KRaft supports snapshot-based log compaction (metadata snapshots).
- Voters and observers: Not all nodes are voters; some are observer nodes that replicate but don't vote.
- Resignation: A leader can voluntarily step down (immediate or graceful).
KRaft Additional State
| Variable | Type | Description |
|---|---|---|
isVoter |
Server → Bool | Whether this node is a voter (vs observer) |
isLeader |
Server → Bool | Cached: state[i] = Leader |
highWatermark |
Nat | Similar to commitIndex; offset of last stable metadata record |
KRaft Additional Actions
- Resign: Leader steps down gracefully
- BecomeObserver: Voter transitions to observer (loses vote)
- BecomeVoter: Observer transitions to voter (gains vote)
Known Verification Gaps / Interesting Targets
-
Election safety under network partitions: Raft's election safety depends on the quorum overlap property. In practice, network partitions and message reordering can create edge cases.
-
Log truncation after leader change: When a new leader overwrites follower logs that conflict, the truncation must be atomic. This is a known source of bugs in Raft implementations.
-
Stale leader problem: After a network partition heals, a stale leader may attempt to commit entries from an old term. AdvanceCommitIndex prevents this by only committing entries from the current term.
-
KRaft observer consistency: Observers don't vote but must maintain log consistency. The spec needs to ensure observers can't diverge from voters.
-
Graceful shutdown / resignation race: If a leader resigns while a client request is in flight, the entry may or may not be committed. This is a liveness property, not safety, but worth verifying.
Producer Acknowledgement Semantics (for M1.3)
acks=0 (Fire and forget)
- Producer sends and doesn't wait for response
- No durability guarantee
- Invariant: None (messages can be lost)
acks=1 (Leader acknowledgment)
- Producer waits for leader to write to local log
- Durability: survives leader failure, but NOT follower failure
- Invariant: If ack received, entry exists in leader's log
acks=all (ISR acknowledgment)
- Producer waits for all in-sync replicas (ISR) to acknowledge
- Durability: survives any failure that leaves a quorum
- Invariant: If ack received, entry is committed (quorum has replicated)
Exactly-once Semantics
- Requires idempotent producer + transaction coordinator
- Idempotent producer: assigns sequence numbers; broker deduplicates
- Transaction coordinator: two-phase commit across partitions
Key Invariants for Producer Acks
acks=all→ no data loss on leader failover (bounded to quorum availability)- Idempotent producer → no duplicate messages for same PID + sequence number
- Transaction atomicity → all partitions in a transaction commit or none do
Consumer Group Rebalancing (for M1.3)
States
- Empty → Joining → Stable → Rebalancing → Stable
Actions
- JoinGroup: consumer requests to join
- SyncGroup: consumer receives assignment
- Heartbeat: consumer indicates liveness
- LeaveGroup: consumer departs
- Rebalance: triggered by membership change or topic partition change
Invariants
- No concurrent assignments to the same partition
- All partitions assigned when group is stable
- Offset monotonicity: committed offsets never decrease
- No two consumers assigned the same partition simultaneously
Scope Boundary
In scope (M1.1): Core Raft consensus — leader election, log replication, commit advancement, and all 6 safety invariants.
Deferred to M1.3: Producer ack semantics, consumer group rebalancing, transaction coordinator.
Out of scope: Full Kafka broker (produce/fetch handling), controller-specific logic, snapshot/recovery, network layer.