ZooKeeper 使用 ZooKeeper Atomic Broadcast (ZAB) 协议来保证数据一致性和高可用性。ZAB 协议是一种专门为 ZooKeeper 设计的分布式一致性协议,类似于 Paxos 和 Raft,但更适合于 ZooKeeper 的需求。ZAB 协议主要包括两个阶段:领导选举和原子广播。下面详细解释 ZAB 协议如何保证数据一致性,并结合相关代码示例进行深入探讨。
1. ZAB 协议概述
ZAB 协议由两个主要部分组成:
- 领导选举(Leader Election):选出一个 Leader,确保只有一个 Leader 负责处理客户端的写请求。
- 原子广播(Atomic Broadcast):确保所有的写请求都能被集群中所有节点以相同的顺序处理。
2. 领导选举
在 ZooKeeper 集群中,只有 Leader 可以处理写请求,Follower 只处理读请求和同步数据。领导选举的过程如下:
- 启动阶段:当一个 ZooKeeper 节点启动时,它会进入选举状态。
- 投票阶段:每个节点投票给它认为最合适的 Leader,投票依据包括节点的事务 ID(zxid)和节点 ID。
- 达成共识:节点不断交换投票信息,直到大多数节点(法定人数)达成共识,选出 Leader。
领导选举的代码实现主要在 FastLeaderElection 类中:
public class FastLeaderElection implements Election {
// 核心选举方法
public Vote lookForLeader() throws InterruptedException {
// 初始化投票
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
synchronized(this){
logicalclock++;
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 投票过程
sendNotifications();
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
Notification n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS);
if(n == null){
if(manager.haveDelivered()){
sendNotifications();
}
} else if (validVoter(n.sid) && validVoter(n.leader)) {
// 处理收到的投票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
// 判断是否有足够的票数
if (termPredicate(recvset, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state))) {
self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING : learningState());
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
}
}
}
3. 原子广播
当选出 Leader 后,客户端的写请求会被发送到 Leader,Leader 将请求作为事务广播给所有 Follower,确保所有节点以相同的顺序处理这些事务。原子广播的过程如下:
- 提交请求:客户端将写请求发送给 Leader。
- 事务广播:Leader 将请求转换为事务,并以提案(Proposal)的形式广播给所有 Follower。
- 接收提案:Follower 接收到提案后,写入本地日志,并发送确认(ACK)给 Leader。
- 达成共识:当 Leader 收到法定人数的确认后,提交事务,并将提交消息(Commit)广播给所有 Follower。
- 事务应用:Follower 接收到提交消息后,将事务应用到本地数据。
原子广播的代码实现主要在 Leader 和 Follower 类中:
Leader 处理请求
public class Leader {
// 处理客户端请求
public void processPacket(QuorumPacket qp) {
switch (qp.getType()) {
case OpCode.create:
case OpCode.delete:
case OpCode.setData:
Proposal proposal = new Proposal();
proposal.packet = qp;
proposal.request = request;
synchronized (this) {
lastProposed = proposal;
outstandingProposals.put(lastProposedPacket, proposal);
}
sendPacket(qp);
break;
case OpCode.sync:
syncHandler.add(qp);
break;
}
}
// 发送提案
public void sendPacket(QuorumPacket qp) {
for (LearnerHandler f : learners) {
f.queuePacket(qp);
}
}
// 接收确认
public void processAck(long sid, long zxid) {
Proposal p = outstandingProposals.get(zxid);
if (p == null) {
return;
}
p.ackSet.add(sid);
if (p.ackSet.size() > self.getQuorumSize()) {
commit(zxid);
}
}
// 提交事务
public void commit(long zxid) {
QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null);
sendPacket(qp);
outstandingProposals.remove(zxid);
}
}
Follower 处理提案
public class Follower {
// 接收提案
public void processPacket(QuorumPacket qp) {
switch (qp.getType()) {
case Leader.PROPOSAL:
Proposal proposal = new Proposal();
proposal.packet = qp;
proposal.request = request;
synchronized (this) {
outstandingProposals.put(qp.getZxid(), proposal);
}
writeToLog(qp);
sendAck(qp.getZxid());
break;
case Leader.COMMIT:
synchronized (this) {
outstandingProposals.remove(qp.getZxid());
}
applyTransaction(qp);
break;
}
}
// 写入日志
public void writeToLog(QuorumPacket qp) {
// 将提案写入本地日志
}
// 发送确认
public void sendAck(long zxid) {
QuorumPacket ack = new QuorumPacket(Leader.ACK, zxid, null, null);
queuePacket(ack);
}
// 应用事务
public void applyTransaction(QuorumPacket qp) {
// 将事务应用到本地数据
}
}
总结
通过领导选举和原子广播,ZAB 协议确保了 ZooKeeper 集群中的数据一致性。领导选举保证了只有一个 Leader 处理写请求,避免了脑裂问题;原子广播确保所有节点以相同的顺序处理事务,保证了数据一致性。上述代码示例展示了 ZAB 协议的核心实现逻辑,帮助理解其如何在生产环境中保证数据一致性。