ZooKeeper Leader 选举:FastLeaderElection 算法
你有没有想过这个问题:
ZooKeeper 集群 5 台机器,突然 Leader 挂了。30 秒后,系统恢复正常。
这 30 秒里发生了什么?新 Leader 是怎么选出来的?
很多人只知道「过半选举」,但面试官追问下去:过半是超过一半,还是不少于一半?数据最新的节点一定被选中吗?Epoch 是什么,为什么重要?
今天,我们来揭开 ZooKeeper Leader 选举的神秘面纱。
Leader 的作用
在说选举之前,先搞清楚 Leader 有什么用。
ZooKeeper 是一个主从架构的分布式系统:
- Leader:处理所有写请求,将事务同步给 Follower
- Follower:处理读请求,参与选主,参与事务同步
- Observer:只处理读请求,不参与选主(扩展读能力)
写请求必须经过 Leader,这是 ZooKeeper 保证顺序性的关键。
// ZooKeeper 写请求流程
// 1. Client 发送写请求给任意节点
// 2. 该节点如果是 Follower,转发给 Leader
// 3. Leader 生成事务 Proposal,发给所有 Follower
// 4. 过半 Follower 回复 ACK
// 5. Leader 提交事务,通知所有 Follower 提交什么时候触发选举?
两种情况会触发 Leader 选举:
- 集群启动时:所有节点都认为自己应该当 Leader,发起投票
- Leader 崩溃时:Follower 检测到 Leader 不可用,发起投票
// 节点状态
public enum ServerState {
LOOKING, // 正在寻找 Leader
LEADING, // 我是 Leader
FOLLOWING, // 我是 Follower
OBSERVING // 我是 Observer
}选举的四个核心要素
选举不是简单比大小,而是四个维度综合比较:
1. Epoch(逻辑时钟)
也叫 zxid 的高 32 位,代表「任期」。
每次新 Leader 当选,Epoch +1。即使 Rawnode 重新加入,Epoch 也会继续增长。
// Epoch 的含义
Epoch 1: 第一个 Leader 时代
Epoch 2: 第二个 Leader 时代
Epoch 3: 第三个 Leader 时代为什么需要 Epoch?因为不同任期的提案不能比较。
2. zxid(事务 ID)
也叫 Proposal ID,是 ZooKeeper 事务的唯一标识。
// zxid 结构
zxid = (Epoch << 32) | counter
// 高 32 位:Epoch
// 低 32 位:事务计数zxid 越大,说明处理的事务越多,数据越新。
3. ServerID(服务器 ID)
也叫 myid,是节点启动时配置的静态 ID。
# zoo.cfg
server.1=192.168.1.1:2888:3888
server.2=192.168.1.2:2888:3888
server.3=192.168.1.3:2888:3888ServerID 只在 Epoch 和 zxid 都相同时作为最终裁决,它不参与正常选举。
4. ServerState(服务器状态)
节点当前的状态,前面提到过:LOOKING / LEADING / FOLLOWING / OBSERVING。
FastLeaderElection 算法:三步达成共识
FastLeaderElection 是 ZooKeeper 默认的选举算法。它的核心思想是:用最短的轮次,让过半节点达成一致。
第一步:自增 Epoch,发起投票
每个 LOOKING 状态的节点都会发起选举,投票给自己。
// 每个节点投票给自己
Vote myVote = new Vote(
serverId, // 自己的 ServerID
zxid, // 自己的最新事务 ID
logicalclock, // 自增的 Epoch
ServerState.LOOKING
);第二步:广播投票,交换信息
节点将自己的投票广播给所有其他节点。
// 收到其他节点的投票
public void receiveVote(Vote vote) {
if (vote.getState() == ServerState.LOOKING) {
// 对方也在 LOOKING,比较优先级
if (vote.getEpoch() > myVote.getEpoch()) {
// Epoch 更大,信任对方的投票
myVote.setEpoch(vote.getEpoch());
myVote.setId(vote.getId());
myVote.setZxid(vote.getZxid());
} else if (vote.getEpoch() == myVote.getEpoch()) {
// Epoch 相同,比较 zxid
if (vote.getZxid() > myVote.getZxid()) {
myVote.setZxid(vote.getZxid());
myVote.setId(vote.getId());
} else if (vote.getZxid() == myVote.getZxid()) {
// zxid 也相同,比较 ServerID
if (vote.getId() > myVote.getId()) {
myVote.setId(vote.getId());
}
}
}
}
}第三步:过半认同,当选 Leader
当一个节点收到过半票数(包括自己),它就当选 Leader。
// 票数统计
if (votesReceived.size() > (totalServerNum / 2)) {
// 达成过半,选举成功
if (myVote.getId() == myServerId) {
// 我是 Leader
setServerState(ServerState.LEADING);
} else {
// 我是 Follower
setServerState(ServerState.FOLLOWING);
}
}为什么能选出「数据最新」的节点?
关键在于比较顺序:Epoch → zxid → ServerID。
// 比较逻辑
if (Epoch 更大) → 信任对方,Epoch 更大意味着经历过更多 Leader
else if (zxid 更大) → zxid 更大意味着处理了更多事务
else if (ServerID 更大) → 作为最终裁决这确保了:新 Leader 一定是数据最新的节点。
但问题是:zxid 大的节点一定能当选吗?
不一定。如果 zxid 大的节点只有 1 台,而其他 4 台都投给 zxid 小但 Epoch 大的节点,那么 zxid 小的节点当选(因为过半)。
这是 Paxos 算法的特性:少数服从多数。
Curator LeaderLatch 使用
实际开发中,我们用 Curator 的 LeaderLatch 做 Leader 选举。
LeaderLatch latch = new LeaderLatch(client, "/leader-election", "participant-1");
latch.addListener(new LeaderLatchListener() {
@Override
public void isLeader() {
System.out.println("我当选为 Leader 了!");
// 开始执行 Leader 职责
}
@Override
public void notLeader() {
System.out.println("我不是 Leader 了");
// 降级为 Follower
}
});
latch.start();
// 等待成为 Leader
latch.await();总结
ZooKeeper 的 Leader 选举,是一个** Paxos 变种**的实现:
- 四要素:Epoch、zxid、ServerID、ServerState
- 比较规则:Epoch 优先 → zxid 其次 → ServerID 最后
- 过半原则:N/2 + 1 票才能当选
- 数据一致性:新 Leader 一定是数据最新的节点
理解选举机制,才能理解 ZooKeeper 如何保证分布式一致性。
面试追问方向:
- ZooKeeper 的过半原则是什么?为什么是 N/2+1 而不是 N/3+1?
- Epoch 一样但 zxid 不同时会发生什么?
- Leader 崩溃后,Follower 的数据如何恢复同步?
- Observer 不参与选举,有什么用?
