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 - 节点角色:brokercontroller 或两者
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 分区注册信息
相关推荐
只爱喝胡辣汤1 小时前
异步线程深度剖析(CompletableFuture/异步模式/虚拟线程)
后端
苏三说技术1 小时前
推荐一个比ES快5倍的搜索引擎
后端
二月龙1 小时前
Java 线程池核心参数详解:从原理到生产避坑
后端
小强19881 小时前
线上 Java 项目 CPU 飙升、OOM 排查思路:完整实战流程
后端
学编程就要猛1 小时前
流式编程及Spring中SSE实现
java·后端·spring·流式编程
wechatbot8881 小时前
SpringBoot Vue 企业微信多账号托管|扫码登录 代理 IP 消息回调
大数据·后端·微信·企业微信·ai编程
不才不才不不才2 小时前
Spring 源码系列(27): @Transactional 七大失效场景与源码归因
java·后端·spring
妙码生花2 小时前
PHP 各框架下和 Go 的性能比较
前端·后端·go
名字还没想好☜2 小时前
Python 字符编码实战:encode/decode、UnicodeDecodeError 与 open 的 encoding 坑
开发语言·后端·python·编程语言