ZooKeeper 初始化 Leader 选举深度解析:从零构建集群核心

ZooKeeper 初始化 Leader 选举深度解析:从零构建集群核心

    • 一、初始化选举概述
      • 1.1 什么是初始化选举?
      • 1.2 初始化选举与运行时选举的区别
    • 二、初始化选举的准备阶段
      • 2.1 服务器启动流程
      • 2.2 节点初始状态
    • 三、节点启动顺序对选举的影响
      • 3.1 场景一:依次启动(最常见)
      • 3.2 场景二:同时启动
      • 3.3 场景三:启动两节点后暂停
    • 四、初始化选举的源码实现
      • 4.1 节点启动时的选举触发
      • 4.2 FastLeaderElection 的核心逻辑
    • 五、初始化选举的通信机制
      • 5.1 QuorumCnxManager:管理节点间连接
      • 5.2 初始化选举中的消息类型
    • 六、实战验证:观察初始化选举
      • 6.1 实验环境准备
      • 6.2 观察选举过程
      • 6.3 初始化选举结果分析表
    • 七、初始化选举后的数据同步
    • 八、总结
      • 8.1 初始化选举核心要点
      • 8.2 初始化选举流程图
      • 8.3 一句话总结

🌺The Begin🌺点点关注,收藏不迷路🌺

摘要:当 ZooKeeper 集群首次启动时,所有服务器都是平等的,没有预先指定的 Leader。此时,集群需要通过一套精密的选举机制,从多个候选节点中选出一个 Leader,完成集群的初始化。本文将深入剖析 ZooKeeper 在初始化阶段的 Leader 选举过程,从服务器启动顺序的影响、投票规则的运作,到最终 Leader 的诞生,通过详细的流程图和源码分析,帮助读者全面理解这一关键机制。

一、初始化选举概述

1.1 什么是初始化选举?

初始化选举是指 ZooKeeper 集群首次启动时的 Leader 选举过程。此时集群中没有任何服务器知道谁是 Leader,所有服务器都处于平等的地位,需要通过投票机制选出一个 Leader,完成集群的初始化。

初始化选举 vs 运行时选举

初始化选举

首次启动

所有节点数据一致
(ZXID 相同)

节点启动顺序影响结果

运行时选举

Leader 故障后触发

节点数据可能不一致
(ZXID 不同)

ZXID 大的优先当选

1.2 初始化选举与运行时选举的区别

维度 初始化选举 运行时选举
触发场景 集群首次启动 Leader 故障或失联
数据状态 所有节点 ZXID 相同(通常为 0) 节点间 ZXID 可能不同
选举依据 主要依赖 myid(服务器 ID) 优先比较 ZXID
启动顺序 节点启动顺序影响结果 节点启动顺序不影响
典型结果 先启动的节点更可能成为 Leader 数据最新的节点当选

二、初始化选举的准备阶段

2.1 服务器启动流程

当 ZooKeeper 服务器启动时,会经历以下关键步骤:

首次启动

已有 Leader

启动 ZooKeeper 服务

加载配置文件 zoo.cfg

读取 myid 文件

初始化数据库

恢复本地快照和日志

检查集群状态

进入 LOOKING 状态

直接连接 Leader

开始 Leader 选举

2.2 节点初始状态

初始化时,每个服务器都会进入 LOOKING 状态,并为自己投上一票(投票信息为自己的 myidlastZxid):

// QuorumPeer.java - 初始化投票
public class QuorumPeer {
    protected Election createElectionAlgorithm(int electionAlgorithm) {
        // 创建选举算法实例
        le = new FastLeaderElection(this, qcm);
        // 为自己投上第一票
        le.offerVote(new Vote(myid, getLastLoggedZxid()));
        return le;
    }
}

初始化时的 ZXID:由于所有节点都是首次启动,事务日志为空,因此 lastZxid 通常为 0。

三、节点启动顺序对选举的影响

初始化选举中,节点的启动顺序至关重要。下面以 3 节点集群为例,分析不同启动顺序下的选举结果。

3.1 场景一:依次启动(最常见)

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

1. 第一台启动

2. 第二台启动

比较: ZXID相同(0=0), myid(1<2)

比较: ZXID相同(0=0), myid(2>1)

统计: 节点2获得2票,达到过半(2/3)

3. 第三台启动

进入 LOOKING

投票给自己(1,0)

等待其他节点…

进入 LOOKING

投票给自己(2,0)

交换投票(1,0)

交换投票(2,0)

更新投票为(2,0)

保持投票(2,0)

成为 Leader

成为 Follower

发现已有 Leader

直接成为 Follower

连接 Leader

数据同步

结果分析:当节点1和节点2启动后,由于 ZXID 相同,myid 较大的节点2胜出,成为 Leader。节点3启动时直接作为 Follower 加入。

3.2 场景二:同时启动

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

三台几乎同时启动

par

[并发投票]

收到(2,0)和(3,0)

收到(1,0)和(3,0)

收到(1,0)和(2,0)

par

[重新广播]

统计: 获得3票,达到过半

投票给自己(1,0)

投票给自己(2,0)

投票给自己(3,0)

投票(1,0)

投票(1,0)

投票(2,0)

投票(2,0)

投票(3,0)

投票(3,0)

比较: ZXID相同,myid最大是3

更新投票为(3,0)

比较: ZXID相同,myid最大是3

更新投票为(3,0)

比较: 自己myid最大,保持(3,0)

投票(3,0)

投票(3,0)

投票(3,0)

投票(3,0)

投票(3,0)

投票(3,0)

成为 Leader

成为 Follower

成为 Follower

结果分析:当三台几乎同时启动时,myid 最大的节点3会成为 Leader。这是因为在 ZXID 相同的情况下,比较规则会倾向于 myid 较大的节点。

3.3 场景三:启动两节点后暂停

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

节点3 (myid=3)

节点2 (myid=2)

节点1 (myid=1)

获得2票,成为 Leader

集群已有 Leader

启动

LOOKING, 等待

启动

交换投票

交换投票

通知选举结果

成为 Follower

启动

连接 Leader

数据同步

成为 Follower

关键结论:在初始化选举中,只要任意两个节点完成通信并达成一致,就能选出 Leader,后续启动的节点自动加入。

四、初始化选举的源码实现

4.1 节点启动时的选举触发

// QuorumPeerMain.java - 主入口
public class QuorumPeerMain {
    public static void main(String[] args) {
        // 1. 解析配置文件
        QuorumPeerConfig config = new QuorumPeerConfig();
        config.parse(args);
        // 2. 启动 QuorumPeer
        QuorumPeer quorumPeer = new QuorumPeer();
        quorumPeer.setConfig(config);
        // 3. 启动选举
        quorumPeer.start();
    }
}
// QuorumPeer.java - 启动选举
public class QuorumPeer extends ZooKeeperThread {
    public synchronized void start() {
        // 加载数据库
        loadDataBase();
        // 创建选举算法
        createElectionAlgorithm(electionType);
        // 启动选举线程
        super.start();
    }
    public void run() {
        while (running) {
            switch (getPeerState()) {
                case LOOKING:
                    // 执行选举
                    setCurrentVote(makeLEStrategy().lookForLeader());
                    break;
                // ...
            }
        }
    }
}

4.2 FastLeaderElection 的核心逻辑

// FastLeaderElection.java - 初始化选举核心
public class FastLeaderElection implements Election {
    public Vote lookForLeader() throws InterruptedException {
        // 1. 自增逻辑时钟(初始化时开始计数)
        logicalclock.incrementAndGet();
        // 2. 初始投票给自己
        updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
        // 3. 广播投票
        sendNotifications();
        // 4. 循环接收和处理投票
        while (running) {
            Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
            if (n == null) {
                // 超时重发
                sendNotifications();
                continue;
            }
            // 5. 根据不同 epoch 处理
            if (n.electionEpoch > logicalclock.get()) {
                logicalclock.set(n.electionEpoch);
                updateProposal(n);
            } else if (n.electionEpoch < logicalclock.get()) {
                continue;
            }
            // 6. 比较投票(核心规则)
            if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                   proposedLeader, proposedZxid, proposedEpoch)) {
                updateProposal(n);
                sendNotifications();
            }
            // 7. 统计得票
            HashMap<Vote, Integer> voteCount = new HashMap<>();
            // ... 统计逻辑
            // 8. 判断是否达到过半
            if (voteCount.get(proposedVote) > (peers.size() / 2)) {
                return proposedVote;
            }
        }
        return null;
    }
    /**
     * 投票比较规则(初始化场景:zxid 相同)
     */
    protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch,
                                          long curId, long curZxid, long curEpoch) {
        if (newEpoch > curEpoch) {
            return true;
        }
        if (newEpoch < curEpoch) {
            return false;
        }
        if (newZxid > curZxid) {
            return true;
        }
        if (newZxid < curZxid) {
            return false;
        }
        // ZXID 相同,比较 myid
        return newId > curId;
    }
}

五、初始化选举的通信机制

5.1 QuorumCnxManager:管理节点间连接

ZooKeeper节点通信规则

规则说明

节点SID配置

主动连接

主动连接

主动连接

✓ 只允许SID大的节点
主动连接SID小的节点

节点2
SID: 2

节点1
SID: 1 (最小)

节点3
SID: 3 (最大)

✗ 不允许SID小的节点
主动连接SID大的节点

目的:避免连接风暴
确保连接方向一致性

连接方向规则

连接建立规则:为了避免两台机器之间重复创建 TCP 连接,ZooKeeper 只允许 SID 大的服务器主动和其他机器建立连接。

// QuorumCnxManager.java - 连接管理
public class QuorumCnxManager {
    private void initiateConnection(SocketAddress addr, Long sid) {
        // 只允许 SID 大的节点主动连接
        if (sid < self.getId()) {
            // 当前节点 SID 更大,应由对方主动连接
            return;
        }
        // 建立 TCP 连接
        Socket sock = new Socket();
        sock.connect(addr);
        // 发送本地 SID 进行握手
        sendSid(sock, self.getId());
    }
}

5.2 初始化选举中的消息类型

消息类型 发送方 接收方 内容
Notification 任意节点 任意节点 (epoch, state, sid, zxid)
Proposal Leader 候选 其他节点 选举结果提案

六、实战验证:观察初始化选举

6.1 实验环境准备

# 在三台机器上安装 ZooKeeper(版本 3.8.0)
# 配置 zoo.cfg
server.1=192.168.1.10:2888:3888
server.2=192.168.1.11:2888:3888
server.3=192.168.1.12:2888:3888
# 创建 myid 文件
# 节点1
echo "1" > /var/lib/zookeeper/myid
# 节点2
echo "2" > /var/lib/zookeeper/myid
# 节点3
echo "3" > /var/lib/zookeeper/myid

6.2 观察选举过程

第一步:启动节点1

./bin/zkServer.sh start
./bin/zkServer.sh status
# 输出:Mode: standalone  (单节点无法选举,进入单机模式)

第二步:启动节点2

# 节点2启动后,查看节点1状态
./bin/zkServer.sh status
# 输出:Mode: follower  或 Mode: leader
# 查看日志
tail -f logs/zookeeper.out
# 预期日志
2024-01-01 10:00:01 - Peer state changed: looking
2024-01-01 10:00:02 - Notification received from 2
2024-01-01 10:00:02 - Updating vote to 2
2024-01-01 10:00:03 - Received majority votes, election done
2024-01-01 10:00:03 - Peer state changed: following

第三步:启动节点3

./bin/zkServer.sh start
# 节点3会自动加入集群,成为 Follower

6.3 初始化选举结果分析表

启动顺序 节点1 (myid=1) 节点2 (myid=2) 节点3 (myid=3) Leader
1 → 2 → 3 Follower Leader Follower 节点2
1 → 3 → 2 Follower Follower Leader 节点3
2 → 1 → 3 Follower Leader Follower 节点2
2 → 3 → 1 Follower Follower Leader 节点3
3 → 1 → 2 Follower Follower Leader 节点3
3 → 2 → 1 Follower Follower Leader 节点3
同时启动 Follower Follower Leader 节点3

结论:初始化选举的结果由最早启动的两个节点中的较大 myid 决定。如果所有节点几乎同时启动,myid 最大的节点成为 Leader。

七、初始化选举后的数据同步

Leader 选举完成后,Follower 需要与 Leader 进行数据同步。但在初始化场景下,所有节点的数据都是空的,因此同步过程非常简单:

新 Leader

Follower

新 Leader

Follower

所有节点 lastZxid 都是 0

4. 状态变更为 FOLLOWING

5. 状态为 LEADING

集群初始化完成

1. 发送 FOLLOWERINFO(lastZxid=0)

2. 比较 lastZxid

3. 发送 UPTODATE

八、总结

8.1 初始化选举核心要点

要点 说明
触发条件 集群首次启动,所有节点无数据
投票依据 ZXID 相同(均为 0),主要依赖 myid
过半机制 需要获得超过半数节点的投票
启动顺序影响 最早启动的两个节点中的较大 myid 成为 Leader
同时启动情况 myid 最大的节点成为 Leader
数据同步 所有节点数据为空,同步过程极快

8.2 初始化选举流程图

对方 ZXID 更大

自己 ZXID 更大

ZXID 相同

对方 myid 更大

自己 myid 更大

节点启动

进入 LOOKING 状态

投票给自己

广播投票

接收其他节点投票

比较投票规则

更新投票

保持投票

比较 myid

重新广播

统计得票

过半?

选举完成

自己是否当选?

成为 Leader

成为 Follower

等待 Follower 连接

数据同步

集群就绪

8.3 一句话总结

ZooKeeper 初始化选举是一个基于 ZXID 和 myid 的投票比较过程,由于所有节点首次启动时 ZXID 均为 0,因此选举结果实际上由 myid 和节点启动顺序共同决定:最先启动的两个节点中,myid 较大的节点当选 Leader,后续节点自动作为 Follower 加入。这一设计确保了集群在无任何预配置的情况下能够自动、快速地完成初始化。

在这里插入图片描述

🌺The End🌺点点关注,收藏不迷路🌺
© 版权声明

相关文章