05 - KRaft 元数据管理与 Raft 协议源码分析
Kafka Raft(KRaft)是 Kafka 3.x 引入的去 ZooKeeper 化的元数据管理方案,使用自定义的 Raft 一致性协议替代 ZooKeeper 来管理集群元数据。
一、KRaft 架构概览
1.1 传统架构 vs KRaft 架构
scss
┌─────────────────────────────────────────────────────────────┐
│ 传统架构 (Kafka 2.x) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ZooKeeper │◄────────►│ Kafka Broker │ │
│ │ (3-5 节点) │ │ (Controller 选举)│ │
│ │ • 元数据存储 │ │ • 分区状态机 │ │
│ │ • 分布式锁 │ │ • Rebalance │ │
│ │ • 监听通知 │ │ • ISR 管理 │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ KRaft 架构 (Kafka 3.x+) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Kafka Controller Quorum │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Controller│ │ Controller│ │ Controller│ │ │
│ │ │ (Active)│ │ (Follower)│ │ (Follower)│ │ │
│ │ │ • 元数据管理│ │ • 复制日志 │ │ • 复制日志 │ │ │
│ │ │ • 请求处理 │ │ • 选举投票 │ │ • 选举投票 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ │ 基于 Raft 协议实现 leader 选举和日志复制 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Kafka Broker│ │
│ │ (无状态) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
1.2 KRaft 核心优势
- 减少外部依赖:不再需要部署和维护 ZooKeeper 集群
- 简化部署:单节点即可运行 Kafka,便于测试和开发
- 更好的可扩展性:元数据操作不再受限于 ZooKeeper 的性能瓶颈
- 统一日志模型:使用 Kafka 日志来存储元数据,复用成熟的复制机制
二、Raft 协议核心概念
2.1 Raft 角色状态机
scss
┌─────────────────────────────────────────────────────────────────────┐
│ Raft 状态机 │
│ │
│ ┌──────────────┐ │
│ │ Unattached │─────────────────────────────────────────┐ │
│ │ (未连接) │ │ │
│ └──────┬───────┘ │ │
│ │ │ │
│ │ 发现 Leader 或更大 Epoch │ │
│ ▼ │ │
│ ┌──────────────┐ 选举超时 ┌──────────────┐ │ │
│ │ Follower │──────────────▶│ Candidate │ │ │
│ │ (跟随者) │ │ (候选者) │ │ │
│ │ │◄───────────────┤ │ │ │
│ │ │ 收到更高 Epoch │ │ │ │
│ │ │ │ │ │ │
│ │ │◄───────────────┘ │ │ │
│ │ │ 收到大多数投票 │ │ │
│ │ │ │ │ │
│ └──────────────┘ └──────┬────┘ │
│ ▲ │ │
│ │ │ │
│ │ 收到 BeginQuorumEpoch │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Leader │◄─────────────────────────────────────────────┐ │
│ │ (领导者) │ │ │
│ └──────────────┘ Resigned (优雅退出) │ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 核心 API
Kafka Raft 定义了 5 个核心 RPC:
| API | 发送方 | 接收方 | 说明 |
|---|---|---|---|
VoteRequest |
Candidate | Voter | 选举时请求投票 |
BeginQuorumEpoch |
Leader | Voter | Leader 宣告新 Epoch |
EndQuorumEpoch |
Leader | Voter | Leader 优雅退出 |
Fetch |
Follower | Leader | 拉取日志(复用 Kafka Fetch API) |
FetchSnapshot |
Follower | Leader | 拉取快照 |
三、QuorumState - 节点状态管理
3.1 类定义与状态转换
java
// 文件: raft/src/main/java/org/apache/kafka/raft/QuorumState.java (行 35-76)
/**
* 管理节点的当前状态,确保有效的状态转换:
*
* Unattached/Resigned -> Unattached: 发现更高 Epoch 的新选举
* Unattached/Resigned -> Voted: 授予投票给候选者
* Unattached/Resigned -> Candidate: 选举超时
* Unattached/Resigned -> Follower: 发现 Leader
*
* Voted -> Unattached: 发现更高 Epoch
* Voted -> Candidate: 选举超时
*
* Candidate -> Unattached: 发现更高 Epoch
* Candidate -> Candidate: 选举超时
* Candidate -> Leader: 获得大多数投票
*
* Leader -> Unattached: 发现更高 Epoch
* Leader -> Resigned: 优雅关闭
*/
public class QuorumState {
private volatile EpochState state;
private final OptionalInt localId;
private final Set<Integer> voters;
private final int electionTimeoutMs;
private final int fetchTimeoutMs;
...
}
3.2 状态初始化
java
// 文件: raft/src/main/java/org/apache/kafka/raft/QuorumState.java (行 109-200)
public void initialize(OffsetAndEpoch logEndOffsetAndEpoch) throws IllegalStateException {
// 从本地状态文件读取选举状态
ElectionState election = store.readElectionState();
if (election == null) {
election = ElectionState.withUnknownLeader(0, voters);
}
EpochState initialState;
if (localId.isPresent() && election.isLeader(localId.getAsInt())) {
// 如果之前是 Leader,启动时变为 Resigned
// 1. 确保不能在相同 Epoch 投给其他候选者
// 2. 保护 offset + epoch 唯一性不变式
initialState = new ResignedState(...);
} else if (localId.isPresent() && election.isVotedCandidate(localId.getAsInt())) {
// 如果之前是 Candidate,继续作为 Candidate
initialState = new CandidateState(...);
} else if (election.hasVoted()) {
// 如果已投票,进入 Voted 状态
initialState = new VotedState(...);
} else if (election.hasLeader()) {
// 如果有 Leader,进入 Follower 状态
initialState = new FollowerState(...);
} else {
// 否则进入 Unattached 状态
initialState = new UnattachedState(...);
}
this.state = initialState;
}
四、KafkaRaftClient - Raft 核心实现
4.1 类结构与组件
java
// 文件: raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java (行 143-258)
public class KafkaRaftClient<T> implements RaftClient<T> {
// 核心组件
private final NetworkChannel channel; // 网络通信通道
private final ReplicatedLog log; // 复制日志
private final QuorumState quorum; // 节点状态机
private final RequestManager requestManager; // 请求管理
private final RecordSerde<T> serde; // 序列化器
// 超时管理
private final FuturePurgatory<Long> appendPurgatory;
private final FuturePurgatory<Long> fetchPurgatory;
// 监听器(上层业务,如 Controller)
private final Map<Listener<T>, ListenerContext> listenerContexts = new IdentityHashMap<>();
public KafkaRaftClient(
RecordSerde<T> serde,
NetworkChannel channel,
ReplicatedLog log,
QuorumStateStore quorumStateStore,
Time time,
Metrics metrics,
ExpirationService expirationService,
LogContext logContext,
String clusterId,
OptionalInt nodeId,
RaftConfig raftConfig
) {
// 初始化各个组件
this.channel = channel;
this.log = log;
this.serde = serde;
this.quorum = new QuorumState(
nodeId, quorumVoterIds,
raftConfig.electionTimeoutMs(),
raftConfig.fetchTimeoutMs(),
quorumStateStore, time, logContext, random);
...
}
}
4.2 初始化流程
java
// 文件: raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java (行 362-381)
public void initialize() {
// 1. 根据本地日志初始化 QuorumState
quorum.initialize(new OffsetAndEpoch(log.endOffset().offset, log.lastFetchedEpoch()));
long currentTimeMs = time.milliseconds();
if (quorum.isLeader()) {
throw new IllegalStateException("Voter cannot initialize as a Leader");
} else if (quorum.isCandidate()) {
onBecomeCandidate(currentTimeMs);
} else if (quorum.isFollower()) {
onBecomeFollower(currentTimeMs);
}
// 2. 单节点投票者(用于测试和开发),直接成为 Candidate
if (quorum.isVoter()
&& quorum.remoteVoters().isEmpty()
&& !quorum.isCandidate()) {
transitionToCandidate(currentTimeMs);
}
}
4.3 Leader 状态实现
java
// 文件: raft/src/main/java/org/apache/kafka/raft/LeaderState.java (行 44-200)
public class LeaderState<T> implements EpochState {
private final int localId;
private final int epoch;
private final long epochStartOffset;
// 高水位线
private Optional<LogOffsetMetadata> highWatermark;
// 投票者状态(包括 Leader 自己和 Followers)
private final Map<Integer, ReplicaState> voterStates = new HashMap<>();
// 观察者状态
private final Map<Integer, ReplicaState> observerStates = new HashMap<>();
// Leader 变更记录
private final Set<Integer> grantingVoters = new HashSet<>();
// 日志累加器
private final BatchAccumulator<T> accumulator;
public LeaderState(int localId, int epoch, long epochStartOffset,
Set<Integer> voters, Set<Integer> grantingVoters,
BatchAccumulator<T> accumulator, LogContext logContext) {
this.localId = localId;
this.epoch = epoch;
this.epochStartOffset = epochStartOffset;
this.highWatermark = Optional.empty();
// 初始化所有投票者状态
for (int voterId : voters) {
boolean hasAcknowledgedLeader = voterId == localId;
this.voterStates.put(voterId, new ReplicaState(voterId, hasAcknowledgedLeader));
}
this.grantingVoters.addAll(grantingVoters);
this.accumulator = accumulator;
}
}
4.4 高水位线更新
java
// 文件: raft/src/main/java/org/apache/kafka/raft/LeaderState.java (行 148-199)
private boolean updateHighWatermark() {
// 找出被大多数副本复制的最大偏移量
List<ReplicaState> followersByDescendingFetchOffset = followersByDescendingFetchOffset();
// 大多数 = voter 数量 / 2
int indexOfHw = voterStates.size() / 2;
Optional<LogOffsetMetadata> highWatermarkUpdateOpt =
followersByDescendingFetchOffset.get(indexOfHw).endOffset;
if (highWatermarkUpdateOpt.isPresent()) {
LogOffsetMetadata highWatermarkUpdateMetadata = highWatermarkUpdateOpt.get();
long highWatermarkUpdateOffset = highWatermarkUpdateMetadata.offset;
// KRaft 协议要求:Leader 必须先提交自己 Epoch 的一条记录
// 才能暴露之前 Epoch 的记录,保证安全性
if (highWatermarkUpdateOffset > epochStartOffset) {
highWatermark = highWatermarkUpdateOpt;
return true;
}
}
return false;
}
五、元数据存储
5.1 Metadata Log
KRaft 使用 Kafka 日志来存储元数据记录,每条记录包含:
sql
┌─────────────────────────────────────────────────────────────────┐
│ Metadata Log 记录格式 │
├─────────────────────────────────────────────────────────────────┤
│ Offset │ Epoch │ Record Type │ Data │
├─────────────────────────────────────────────────────────────────┤
│ 0 │ 1 │ LeaderChange │ voter1, voter2, voter3 │
│ 1 │ 1 │ RegisterBroker │ broker-0 info │
│ 2 │ 1 │ RegisterBroker │ broker-1 info │
│ 3 │ 1 │ CreateTopic │ topic=test partitions=3 │
│ 4 │ 1 │ AssignPartition│ partition assignment │
│ 5 │ 1 │ ConfigChange │ topic config update │
│ ... │ ... │ ... │ ... │
└─────────────────────────────────────────────────────────────────┘
5.2 Snapshot 机制
ini
┌─────────────────────────────────────────────────────────────────┐
│ Snapshot 机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Snapshot │ │
│ │ (Offset=0) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Log Segment │ │
│ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │
│ │ │ Record 1 │ Record 2 │ Record 3 │ Record 4 │ ... │ │
│ │ │ │ │ │ │ │ │
│ │ │ Broker │ Topic │ Config │ ... │ │ │
│ │ │ Register │ Create │ Change │ │ │ │
│ │ └──────────┴──────────┴──────────┴──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 当 Log 超过阈值时,生成 Snapshot,然后压缩旧日志 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 元数据记录类型
java
// 文件: metadata/src/main/java/org/apache/kafka/metadata/BrokerRegistration.java
public class BrokerRegistration {
private final int id; // Broker ID
private final Uuid incarnationId; // 实例 ID(重启后变化)
private final List<EndPoint> listeners; // 监听器列表
private final Map<String, VersionRange> supportedFeatures; // 支持特性
private final boolean fenced; // 是否被隔离
private final boolean inControlledShutdown; // 是否在关闭中
...
}
// 文件: metadata/src/main/java/org/apache/kafka/metadata/PartitionRegistration.java
public class PartitionRegistration {
private final int[] replicas; // 副本列表
private final int[] isr; // 同步副本
private final int[] removingReplicas; // 正在移除的副本
private final int[] addingReplicas; // 正在添加的副本
private final int leader; // Leader ID
private final int leaderEpoch; // Leader Epoch
private final int partitionEpoch; // 分区 Epoch
...
}
六、Leader 选举流程
6.1 选举触发
ini
┌─────────────────────────────────────────────────────────────────┐
│ Leader 选举流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Follower (Node B) │
│ │ │
│ │ 选举超时 (electionTimeout) │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ transitionToCandidate │ │
│ │ • 增加 Epoch │ │
│ │ • 投票给自己 │ │
│ └───────────┬──────────┘ │
│ │ │
│ │ VoteRequest (epoch=2, lastOffset=100) │
│ │─────────────────────────────────────────────► │
│ │ │ │
│ │ │ Node A (Voter) │
│ │ │ • 检查 Epoch │
│ │ │ • 比较日志长度 │
│ │ │ • 如果合法,返回 Vote=Yes │
│ │ │ │
│ │ VoteResponse (Vote=Yes) │
│ │◄────────────────────────────────────────────── │
│ │ │
│ │ VoteRequest (epoch=2, lastOffset=100) │
│ │─────────────────────────────────────────────► │
│ │ │ │
│ │ │ Node C (Voter) │
│ │ │ • 检查 Epoch │
│ │ │ • 比较日志长度 │
│ │ │ • 如果合法,返回 Vote=Yes │
│ │ │ │
│ │ VoteResponse (Vote=Yes) │
│ │◄────────────────────────────────────────────── │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 获得大多数投票 → Leader │
│ │ • 发送 BeginQuorumEpoch │
│ │ • 开始接收客户端请求 │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 选举关键代码
java
// 文件: raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java
private boolean handleVoteResponse(
VoteResponseData response,
int remoteNodeId,
long currentTimeMs
) {
final int remoteEpoch = response.voteEpoch();
// 1. 检查 Epoch
if (remoteEpoch < quorum.epoch()) {
return false;
}
// 2. 检查是否还是 Candidate
if (!quorum.isCandidate()) {
return false;
}
CandidateState state = quorum.candidateStateOrNull();
// 3. 检查是否已投票给该候选者
if (state.isGranted(remoteNodeId)) {
return false;
}
// 4. 记录投票结果
if (response.voteGranted()) {
state.recordGrantedVote(remoteNodeId);
// 5. 检查是否获得大多数
if (state.isVoteGranted()) {
transitionToLeader(currentTimeMs);
return true;
}
}
return false;
}
七、日志复制流程
7.1 客户端 Append 流程
scss
┌─────────────────────────────────────────────────────────────────┐
│ 日志复制流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client (Controller) │
│ │ │
│ │ append(record) │
│ ▼ │
│ ┌─────────────┐ │
│ │ Leader Raft │ │
│ │ (Node A) │ │
│ │ │ │
│ │ ┌────────┐ │ │
│ │ │ Accumulator │ │
│ │ │ (Buffer) │ │
│ │ └────────┘ │ │
│ │ │ │ │
│ │ ▼ │ drain() │
│ │ ┌────────┐ │ │
│ │ │ Log │ │ append() │
│ │ │ (本地) │ │ │
│ │ └────────┘ │ │
│ └──────┬──────┘ │
│ │ │
│ │ FetchRequest (offset=100) │
│ │──────────────────────────────────────────────────► │
│ │ │ │
│ │ │ Follower (Node B) │
│ │ │ • 写入本地 Log │
│ │ │ • 返回 FetchResponse │
│ │ FetchResponse │ │
│ │◄────────────────────────────────────────────────── │
│ │ │
│ │ 大多数 Follower 确认后 │
│ ▼ │
│ ┌─────────────┐ │
│ │ High Watermark 更新 │
│ │ 记录已提交,通知 Listener │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 Leader 端处理 Fetch
java
// 文件: raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java
private FetchResponseData handleFetchRequest(
RaftRequest.Inbound requestMetadata,
FetchRequestData fetchRequest
) {
// 1. 检查 Leader 身份
if (!quorum.isLeader()) {
return buildNotLeaderResponse(fetchRequest.replicaId());
}
LeaderState<T> leader = quorum.leaderStateOrNull();
long currentTimeMs = time.milliseconds();
// 2. 更新 Follower 状态
updateLeaderEndOffsetAndTimestamp(leader, currentTimeMs);
// 3. 读取日志
LogFetchInfo logFetchInfo = log.read(
fetchRequest.fetchOffset(),
Isolation.COMMITTED // 只读取已提交记录
);
// 4. 构建响应
return buildFetchResponse(logFetchInfo, ...);
}
八、关键配置参数
| 参数名 | 默认值 | 说明 |
|---|---|---|
process.roles |
- | 节点角色:broker、controller 或两者 |
node.id |
- | 节点唯一 ID |
controller.quorum.voters |
- | Controller 投票者列表 |
controller.quorum.election.timeout.ms |
1000 | 选举超时时间 |
controller.quorum.fetch.timeout.ms |
2000 | Fetch 超时时间 |
controller.quorum.retry.backoff.ms |
20 | 重试间隔 |
metadata.log.segment.bytes |
1073741824 | 元数据日志段大小 |
metadata.log.retention.bytes |
-1 | 元数据日志保留大小 |
九、源码阅读建议
- QuorumState:理解状态机的状态转换规则
- KafkaRaftClient :从
initialize()和poll()开始,了解整体流程 - LeaderState/FollowerState:理解不同角色的行为差异
- BatchAccumulator:了解 Leader 如何缓存和批量写入记录
- Snapshot:了解快照生成和恢复的机制
十、相关源码文件索引
| 文件 | 说明 |
|---|---|
raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java |
Raft 核心实现 |
raft/src/main/java/org/apache/kafka/raft/QuorumState.java |
节点状态管理 |
raft/src/main/java/org/apache/kafka/raft/LeaderState.java |
Leader 状态 |
raft/src/main/java/org/apache/kafka/raft/FollowerState.java |
Follower 状态 |
raft/src/main/java/org/apache/kafka/raft/CandidateState.java |
Candidate 状态 |
raft/src/main/java/org/apache/kafka/raft/ReplicatedLog.java |
复制日志接口 |
metadata/src/main/java/org/apache/kafka/metadata/BrokerRegistration.java |
Broker 注册信息 |
metadata/src/main/java/org/apache/kafka/metadata/PartitionRegistration.java |
分区注册信息 |