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

27 阅读7分钟

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

Kafka Raft(KRaft)是 Kafka 3.x 引入的去 ZooKeeper 化的元数据管理方案,使用自定义的 Raft 一致性协议替代 ZooKeeper 来管理集群元数据。


一、KRaft 架构概览

1.1 传统架构 vs KRaft 架构

┌─────────────────────────────────────────────────────────────┐
│                   传统架构 (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 角色状态机

┌─────────────────────────────────────────────────────────────────────┐
│                        Raft 状态机                                  │
│                                                                     │
│   ┌──────────────┐                                                  │
│   │ Unattached   │─────────────────────────────────────────┐         │
│   │ (未连接)      │                                         │         │
│   └──────┬───────┘                                         │         │
│          │                                                 │         │
│          │ 发现 Leader 或更大 Epoch                          │         │
│          ▼                                                 │         │
│   ┌──────────────┐    选举超时    ┌──────────────┐         │         │
│   │   Follower   │──────────────▶│  Candidate   │         │         │
│   │  (跟随者)     │               │  (候选者)     │         │         │
│   │              │◄───────────────┤              │         │         │
│   │              │  收到更高 Epoch │              │         │         │
│   │              │                │              │         │         │
│   │              │◄───────────────┘              │         │         │
│   │              │   收到大多数投票              │         │         │
│   │              │                             │         │         │
│   └──────────────┘                             └──────┬────┘         │
│          ▲                                          │              │
│          │                                          │              │
│          │         收到 BeginQuorumEpoch           │              │
│          └─────────────────────────────────────────┘              │
│                                                                     │
│   ┌──────────────┐                                                  │
│   │    Leader    │◄─────────────────────────────────────────────┐   │
│   │   (领导者)    │                                              │   │
│   └──────────────┘    Resigned (优雅退出)                      │   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

2.2 核心 API

Kafka Raft 定义了 5 个核心 RPC:

API发送方接收方说明
VoteRequestCandidateVoter选举时请求投票
BeginQuorumEpochLeaderVoterLeader 宣告新 Epoch
EndQuorumEpochLeaderVoterLeader 优雅退出
FetchFollowerLeader拉取日志(复用 Kafka Fetch API)
FetchSnapshotFollowerLeader拉取快照

三、QuorumState - 节点状态管理

3.1 类定义与状态转换

// 文件: 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 状态初始化

// 文件: 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 类结构与组件

// 文件: 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 初始化流程

// 文件: 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 状态实现

// 文件: 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 高水位线更新

// 文件: 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 日志来存储元数据记录,每条记录包含:

┌─────────────────────────────────────────────────────────────────┐
│                      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 机制

┌─────────────────────────────────────────────────────────────────┐
│                       Snapshot 机制                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌──────────────┐                                             │
│   │   Snapshot   │                                             │
│   │  (Offset=0)  │                                             │
│   └──────┬───────┘                                             │
│          │                                                      │
│          ▼                                                      │
│   ┌──────────────────────────────────────────────────────────┐   │
│   │                    Log Segment                           │   │
│   │  ┌──────────┬──────────┬──────────┬──────────┐        │   │
│   │  │ Record 1 │ Record 2 │ Record 3 │ Record 4 │  ...   │   │
│   │  │          │          │          │          │        │   │
│   │  │ Broker   │ Topic    │ Config   │ ...      │        │   │
│   │  │ Register │ Create   │ Change   │          │        │   │
│   │  └──────────┴──────────┴──────────┴──────────┘        │   │
│   └──────────────────────────────────────────────────────────┘   │
│                                                                 │
│   当 Log 超过阈值时,生成 Snapshot,然后压缩旧日志             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 元数据记录类型

// 文件: 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 选举触发

┌─────────────────────────────────────────────────────────────────┐
│                     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 选举关键代码

// 文件: 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 流程

┌─────────────────────────────────────────────────────────────────┐
│                    日志复制流程                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   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

// 文件: 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.ms1000选举超时时间
controller.quorum.fetch.timeout.ms2000Fetch 超时时间
controller.quorum.retry.backoff.ms20重试间隔
metadata.log.segment.bytes1073741824元数据日志段大小
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.javaRaft 核心实现
raft/src/main/java/org/apache/kafka/raft/QuorumState.java节点状态管理
raft/src/main/java/org/apache/kafka/raft/LeaderState.javaLeader 状态
raft/src/main/java/org/apache/kafka/raft/FollowerState.javaFollower 状态
raft/src/main/java/org/apache/kafka/raft/CandidateState.javaCandidate 状态
raft/src/main/java/org/apache/kafka/raft/ReplicatedLog.java复制日志接口
metadata/src/main/java/org/apache/kafka/metadata/BrokerRegistration.javaBroker 注册信息
metadata/src/main/java/org/apache/kafka/metadata/PartitionRegistration.java分区注册信息