Zookeeper(106)Zookeeper的ZAB协议是如何保证数据一致性的?

48 阅读3分钟

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 只处理读请求和同步数据。领导选举的过程如下:

  1. 启动阶段:当一个 ZooKeeper 节点启动时,它会进入选举状态。
  2. 投票阶段:每个节点投票给它认为最合适的 Leader,投票依据包括节点的事务 ID(zxid)和节点 ID。
  3. 达成共识:节点不断交换投票信息,直到大多数节点(法定人数)达成共识,选出 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,确保所有节点以相同的顺序处理这些事务。原子广播的过程如下:

  1. 提交请求:客户端将写请求发送给 Leader。
  2. 事务广播:Leader 将请求转换为事务,并以提案(Proposal)的形式广播给所有 Follower。
  3. 接收提案:Follower 接收到提案后,写入本地日志,并发送确认(ACK)给 Leader。
  4. 达成共识:当 Leader 收到法定人数的确认后,提交事务,并将提交消息(Commit)广播给所有 Follower。
  5. 事务应用:Follower 接收到提交消息后,将事务应用到本地数据。

原子广播的代码实现主要在 LeaderFollower 类中:

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 协议的核心实现逻辑,帮助理解其如何在生产环境中保证数据一致性。