13-KRaft 元数据管理与 Raft 协议源码分析

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 元数据日志保留大小

九、源码阅读建议

  1. QuorumState:理解状态机的状态转换规则
  2. KafkaRaftClient :从 initialize() 和 poll() 开始,了解整体流程
  3. LeaderState/FollowerState:理解不同角色的行为差异
  4. BatchAccumulator:了解 Leader 如何缓存和批量写入记录
  5. 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 分区注册信息
相关推荐
Eric_见嘉19 分钟前
在职前端 Skill 和 MCP 分享
前端·后端·agent
孟健2 小时前
出海开发者资金合规:从港卡结汇到完税申报实操
后端·架构
乌暮2 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
架构技术专栏2 小时前
Skill 有效性与稳定性评估:用对照、重复与证据判断价值
后端
拔剑纵狂歌3 小时前
ClickHouse system.query_log 字段详解与排障实践
后端·clickhouse·1024程序员节
再吃一根胡萝卜3 小时前
把本地 RAG 问答做"准":多轮指代消解 + 云端语义向量 + TXT 章节切分
后端
怕浪猫3 小时前
2026年9月面18个后端
后端·面试·github
IT_陈寒4 小时前
Vue 这个响应式陷阱,我的头发都掉没了
前端·人工智能·后端
谁在黄金彼岸4 小时前
trae的gitee mcp莫名错误
后端
前端的日常4 小时前
一个人+AI做情侣食谱小程序,30天纯赚
前端·javascript·后端