zookeeper(五)ZooKeeper源码分析

  • 启动源码分析
  • Leader选举源码分析

(一) 启动源码分析

   Leader选举是保证分布式数据一致性的关键所在。ZooKeeper在初始化启动时和运行期间都可能进行Leader选举。Leader选举主要会在以下情况发生:

  1. 服务器初始化启动时
  2. 服务器运行期间Leader崩溃
  3. 服务器运行期间事务Zxid过大触发Leader选举

(1)启动时选举

   若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。

选举过程如下:

  1. 每个Server发出一个投票

由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

  1. 接受来自各个服务器的投票

集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。

  1. 处理投票

针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
● 先检查Epoch。Epoch大的服务会作为Leader。
● 如果Epoch相同再检查ZXID。ZXID比较大的服务器优先作为Leader。
● 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

  1. 统计投票

每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。

  1. 改变服务器状态

一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。

(2)运行期选举

   在Zookeeper运行期间,Leader与非Leader服务器各司其职,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。假设正在运行的有Server1、Server2、Server3三台服务器,当前Leader是Server2,若某一时刻Leader挂了,此时便开始Leader选举。

选举过程如下:

  1. 变更状态

Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。

  1. 每个Server会发出一个投票

运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为8,Server3的ZXID为9;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 8),(3, 9),然后各自将投票发送给集群中所有机器。

  1. 接收来自各个服务器的投票(与启动时过程相同)
  2. 处理投票(与启动时过程相同)
  3. 统计投票(与启动时过程相同)
  4. 改变服务器的状态(与启动时过程相同)

(3)Leader选举源码分析

ZooKeeper的选举策略接口为:

public interface Election {
    public Vote lookForLeader() throws InterruptedException;
    public void shutdown();
}

ZooKeeper提供了三种Leader算法实现:

  • LeaderElection:Fast Paxos最简单的一种实现(每个Server启动以后都询问其它的Server它要投票给谁)
  • AuthLeaderElection:同FastLeaderElection算法基本一致,只是在消息中加入了认证信息
  • FastLeaderEelection:ZooKeeper默认的选举策略(所有Server提议自己要成为Leader)
// org.apache.zookeeper.server.quorum.QuorumPeer
protected Election createElectionAlgorithm(int electionAlgorithm){
    Election le=null;

    //TODO: use a factory rather than a switch
    switch (electionAlgorithm) {
        case 0:
            // 为0时选择LeaderElection
            le = new LeaderElection(this);
            break;
        case 1:
            // 为1时选择AuthFastLeaderElection,是否验证为false
            le = new AuthFastLeaderElection(this);
            break;
        case 2:
            // 为2时选择AuthFastLeaderElection,是否验证为true
            le = new AuthFastLeaderElection(this, true);
            break;
        case 3:
            // 为3时选择FastLeaderElection
            qcm = createCnxnManager();
            QuorumCnxManager.Listener listener = qcm.listener;
            if(listener != null){
                listener.start();
                le = new FastLeaderElection(this, qcm);
            } else {
                LOG.error("Null listener when initializing cnx manager");
            }
            break;
        default:
            assert false;
    }
    return le;
}

FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader方法和shutdown方法,其是标准的Fast Paxos算法的实现,各服务器之间基于TCP协议进行选举。

(4)主要类介绍

Notification:该类表示当前节点收到的投票信息(其他节点发来的选举投票信息)

// 两种情况下需要发送投票信息:
// 1. 发送节点已加入了Leader选举
// 2. 发送节点从其他节点获得了一个较大的zxid或相同zxid而serverid较大
static public class Notification {
    // 被推荐Leader
    long leader;

    // 所推荐Leader Server的zxid
    long zxid;

    // 选举周期。用来判断多个投票是否在同一轮选举周期中
    long electionEpoch;

    // 发送者当前的状态
    QuorumPeer.ServerState state;

    // 发送这条消息的服务器sid
    long sid;

    // 被推荐Leader的epoch
    long peerEpoch;
}

ToSend:当前节点想要发送给其他节点的投票信息

static public class ToSend {
    // 被推荐的Leader
    long leader;

    // 被推荐Leader的zxid
    long zxid;

    // 选举周期。用来判断多个投票是否在同一轮选举周期中
    long electionEpoch;

    // 节点当前状态
    QuorumPeer.ServerState state;

    // 接收节点的sid
    long sid;

    // 被推荐Leader的epoch
    long peerEpoch;
}

FastLeaderElection

// org.apache.zookeeper.server.quorum.FastLeaderElection
public class FastLeaderElection implements Election {

    // 负责服务器之间Leader选举过程的网络通信
    QuorumCnxManager manager;
    
    // 选票发送队列
    LinkedBlockingQueue<ToSend> sendqueue;
    // 选票接收队列
    LinkedBlockingQueue<Notification> recvqueue;
    
    // 消息处理类
    protected class Messenger {
        // 从QuorumCnxManager接收消息并处理该消息
        class WorkerReceiver extends ZooKeeperThread { ...... }

        // 该类使要发送的消息出队并将其放入QuorumCnxManager的队列中
        class WorkerSender extends ZooKeeperThread { ...... }
        
        // 选票发送线程。作用是将FastLeaderElection的ToSend转化为QuorumCnxManager的Message
        // 不断地从sendqueue中获取待发送的选票,并将其传递给QuorumCnxManager
        WorkerSender ws;

        // 选票接收线程。将QuorumCnxManager的Message转化为FastLeaderElection的Notification
        // 不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票并保存到recvqueue中
        // 在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票
        WorkerReceiver wr;
    }
    
    // 负责管理Quorum协议,并根据表决结果进行角色转换
    QuorumPeer self;
    Messenger messenger;
    // 逻辑时钟。用来表示ZooKeeper服务器Leader选举的轮次
    AtomicLong logicalclock = new AtomicLong();
    // 推荐的Leader
    long proposedLeader;
    // 推荐Leader的Zxid
    long proposedZxid;
    // 推荐Leader的选举epoch
    long proposedEpoch;
}

QuorumCnxManager

   每个服务器启动时,都会启动一个QuorumCnxManager,它主要负责各个服务器之间Leader选举过程中的网络通信。
QuorumCnxManager内部维护了一系列的队列,用于保存接收的、待发送的消息及消息发送器。所有队列都会按照SID分组构成队列集合。假如,集群中除自身外还有2台机器,那么当前服务器就会为另外2台各创建一个发送队列。

// org.apache.zookeeper.server.quorum.QuorumCnxManager
public class QuorumCnxManager {
    
    // 消息发送器
    // 按照sid进行分组,每个SenderWorker都单独对应一台服务器
    final ConcurrentHashMap<Long, SendWorker> senderWorkerMap;

    // 消息发送队列。用于保存待发送消息
    // 按照sid进行分组,为每台服务器分配一个单独队列
    final ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap;

    // 为每个sid保留最近发送的一条消息
    final ConcurrentHashMap<Long, ByteBuffer> lastMessageSent;
    
    // 消息接受队列
    public final ArrayBlockingQueue<Message> recvQueue;
}

算法详解:

public Vote lookForLeader() throws InterruptedException {
    if (self.start_fle == 0) {
        self.start_fle = Time.currentElapsedTime();
    }
    try {
        // 存放接受来自它节点的选票
        HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();

        // 将该选举结果加入到集合中,再根据集合来判断是否可以结束选举
        HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();

        // Leader选举一旦结束,需要等待的时间
        int notTimeout = finalizeWait;

        synchronized(this){
            // 逻辑时钟+1
            logicalclock.incrementAndGet();
            // 使用本地的sid、zxid和epoch更新投票信息
            updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
        }

        // 广播新的选票信息。往发送队列(sendqueue)里插入一条投票信息
        sendNotifications();

        // 循环交换通知直到找到Leader
        while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
            // 从recvqueue取出一条收到的消息
            Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);

            // 如果未收到消息,则发送更多通知。否则处理新的消息。
            if(n == null){
                if(manager.haveDelivered()){
                    // 如果消息都投递,发送更多的消息
                    sendNotifications();
                } else {
                    // 否则表示网络可能出现问题,重新连接其他节点
                    manager.connectAll();
                }

                // 退避重试
                int tmpTimeOut = notTimeout*2;
                notTimeout = (tmpTimeOut < maxNotificationInterval?
                              tmpTimeOut : maxNotificationInterval);
                LOG.info("Notification time out: " + notTimeout);
            }
            // 有收到消息
            else if(validVoter(n.sid) && validVoter(n.leader)) {
                // 仅当投票来自投票视图时才继续进行(投票视图即:是本集群的服务器节点)
                switch (n.state) {
                    case LOOKING:
                        // 对选举周期进行判断,如果选票中的周期大于本地周期
                        if (n.electionEpoch > logicalclock.get()) {
                            // 设置本地的逻辑时钟为选票的逻辑时钟
                            logicalclock.set(n.electionEpoch);
                            // 清空接收选票的缓存
                            recvset.clear();
                            // 对选票进行PK
                            if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                                   getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
                                // 消息的选票信息PK获胜,则更新本地选票信息
                                updateProposal(n.leader, n.zxid, n.peerEpoch);
                            } else {
                                // 否则,仍然使用本地选票信息
                                updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
                            }
                            // 广播新的选票信息
                            sendNotifications();
                        // 如果选票中的逻辑时钟小于本地逻辑时间,则什么记录日志
                        } else if (n.electionEpoch < logicalclock.get()) {
                            if(LOG.isDebugEnabled()){
                            }
                            break;
                        // 如果选票的逻辑时钟和本地的逻辑时钟相等,说明是一个选举周期,选票进行PK
                        } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                                       proposedLeader, proposedZxid, proposedEpoch)) {
                            // 消息的选票信息PK获胜,则更新本地选票信息
                            updateProposal(n.leader, n.zxid, n.peerEpoch);
                            // 广播新的选票信息
                            sendNotifications();
                        }

                        // 添加到本机投票集合,用来做选举终结判断
                        recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

                        // 验证投票是否胜出,默认Quorum算法(过半胜出)
                        if (termPredicate(recvset,
                                          new Vote(proposedLeader, proposedZxid,
                                                   logicalclock.get(), proposedEpoch))) {

                            // 校验是否有新的选票接受(可能导致Leader发生变化)
                            // 这个时候并不会立即更新服务器状态,而是等待一段时间(默认200ms)来确认是否有新的选票
                            while((n = recvqueue.poll(finalizeWait,
                                                      TimeUnit.MILLISECONDS)) != null){
                                // 如果新来的选票胜出
                                if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                                       proposedLeader, proposedZxid, proposedEpoch)){
                                    // 选票接收队列
                                    recvqueue.put(n);
                                    // 跳出循环
                                    break;
                                }
                            }

                            // 一旦没有从接收队列中读取到任何新的消息,则结束本轮选举
                            if (n == null) {
                                // 根据sid判断自己是否为Leader
                                self.setPeerState((proposedLeader == self.getId()) ?
                                                  ServerState.LEADING: learningState());
                                // 构造选票类
                                Vote endVote = new Vote(proposedLeader,
                                                        proposedZxid,
                                                        logicalclock.get(),
                                                        proposedEpoch);
                                leaveInstance(endVote);
                                // 返回最终结果选票
                                return endVote;
                            }
                        }
                        break;
                    case OBSERVING:
                        LOG.debug("Notification from observer: " + n.sid);
                        break;
                    case FOLLOWING:
                    case LEADING:
                        // 外部投票推荐的服务器为Leader
                        if(n.electionEpoch == logicalclock.get()){
                            // 添加到本机投票集合
                            recvset.put(n.sid, new Vote(n.leader,
                                                        n.zxid,
                                                        n.electionEpoch,
                                                        n.peerEpoch));

                            // 判断是否已选出Leader
                            if(ooePredicate(recvset, outofelection, n)) {
                                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;
                            }
                        }

                        // 已经完成的选票放入outofelection
                        outofelection.put(n.sid, new Vote(n.version,
                                                          n.leader,
                                                          n.zxid,
                                                          n.electionEpoch,
                                                          n.peerEpoch,
                                                          n.state));

                        // 在变更节点状态前,验证多数节点跟随的是同一个Leader
                        if(ooePredicate(outofelection, outofelection, n)) {
                            synchronized(this){
                                // 设置节点状态
                                logicalclock.set(n.electionEpoch);
                                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;
                        }
                        break;
                    default:
                        LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)",
                                 n.state, n.sid);
                        break;
                }
            } else {
                if (!validVoter(n.leader)) {
                    .......
                }
                if (!validVoter(n.sid)) {
                    .......
                }
            }
        }
        return null;
    } finally {

    }
}

// 判断一个被选出的节点是否真的可以做Leader
// 如果其他所有人都认为我是Leader,那么我必须是Leader
protected boolean checkLeader(
    HashMap<Long, Vote> votes,
    long leader,
    long electionEpoch){

    boolean predicate = true;
    
    // 另外两项检查仅用于我不是Leader的情况。如果我不是领导者,并且没有收到来自Leader的消息则返回false。
    if(leader != self.getId()){
        if(votes.get(leader) == null) predicate = false;
        else if(votes.get(leader).getState() != ServerState.LEADING) predicate = false;
    } else if(logicalclock.get() != electionEpoch) {
        predicate = false;
    } 

    return predicate;
}

源码流程:
在这里插入图片描述

(二) Leader选举源码分析

(1)ZooKeeper启动

org.apache.zookeeper.server.quorum.QuorumPeerMain

public void runFromConfig(QuorumPeerConfig config) throws IOException {
    try {
        ManagedUtil.registerLog4jMBeans();
    } catch (JMException e) {
        LOG.warn("Unable to register log4j JMX control", e);
    }

    LOG.info("Starting quorum peer");
    try {
        ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
        cnxnFactory.configure(config.getClientPortAddress(),
                              config.getMaxClientCnxns());

        quorumPeer = getQuorumPeer();

        quorumPeer.setQuorumPeers(config.getServers());
        quorumPeer.setTxnFactory(new FileTxnSnapLog(
            new File(config.getDataLogDir()),
            new File(config.getDataDir())));
        quorumPeer.setElectionType(config.getElectionAlg());
        quorumPeer.setMyid(config.getServerId());
        quorumPeer.setTickTime(config.getTickTime());
        quorumPeer.setInitLimit(config.getInitLimit());
        quorumPeer.setSyncLimit(config.getSyncLimit());
        quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
        quorumPeer.setCnxnFactory(cnxnFactory);
        quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
        quorumPeer.setClientPortAddress(config.getClientPortAddress());
        quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
        quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
        quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
        quorumPeer.setLearnerType(config.getPeerType());
        quorumPeer.setSyncEnabled(config.getSyncEnabled());

        // sets quorum sasl authentication configurations
        quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
        if(quorumPeer.isQuorumSaslAuthEnabled()){
            quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);
            quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);
            quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);
            quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);
            quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);
        }

        quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
        quorumPeer.initialize();

        quorumPeer.start();
        quorumPeer.join();
    } catch (InterruptedException e) {
        // warn, but generally this is ok
        LOG.warn("Quorum Peer interrupted", e);
    }
}

org.apache.zookeeper.server.quorum.QuorumPeer

public QuorumPeer(Map<Long, QuorumServer> quorumPeers, File dataDir,
                  File dataLogDir, int electionType,
                  long myid, int tickTime, int initLimit, int syncLimit,
                  boolean quorumListenOnAllIPs,
                  ServerCnxnFactory cnxnFactory, 
                  QuorumVerifier quorumConfig) throws IOException {
    this();
    this.cnxnFactory = cnxnFactory;
    this.quorumPeers = quorumPeers;
    this.electionType = electionType;
    this.myid = myid;
    this.tickTime = tickTime;
    this.initLimit = initLimit;
    this.syncLimit = syncLimit;        
    this.quorumListenOnAllIPs = quorumListenOnAllIPs;
    // 快照和日志数据
    this.logFactory = new FileTxnSnapLog(dataLogDir, dataDir);
    // 快照和日志数据构成内存数据
    this.zkDb = new ZKDatabase(this.logFactory);
    if(quorumConfig == null)
        this.quorumConfig = new QuorumMaj(countParticipants(quorumPeers));
    else this.quorumConfig = quorumConfig;
}

(2)加载数据

org.apache.zookeeper.server.ZKDatabase

public ZKDatabase(FileTxnSnapLog snapLog) {
    dataTree = new DataTree();
    sessionsWithTimeouts = new ConcurrentHashMap<Long, Integer>();
    this.snapLog = snapLog;
}

public long loadDataBase() throws IOException {
    long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
    initialized = true;
    return zxid;
}

(3)初始化内存数据

org.apache.zookeeper.server.quorum.QuorumPeer

private void loadDataBase() {
    File updating = new File(getTxnFactory().getSnapDir(),
                             UPDATING_EPOCH_FILENAME);
    try {
        zkDb.loadDataBase();

        // 从恢复到内存中的数据加载最后一个处理的Zxid
        long lastProcessedZxid = zkDb.getDataTree().lastProcessedZxid;
        // 从Zxid中解析出epoch
        long epochOfZxid = ZxidUtils.getEpochFromZxid(lastProcessedZxid);
        try {
            // 从currentEpoch文件中加载currentEpoch
            currentEpoch = readLongFromFile(CURRENT_EPOCH_FILENAME);
            // 如果zxid中的epoch大于currentEpoch并且存在更新,则说明update文件较旧
            if (epochOfZxid > currentEpoch && updating.exists()) {
                LOG.info("{} found. The server was terminated after " +
                         "taking a snapshot but before updating current " +
                         "epoch. Setting current epoch to {}.",
                         UPDATING_EPOCH_FILENAME, epochOfZxid);
                setCurrentEpoch(epochOfZxid);
                if (!updating.delete()) {
                    throw new IOException("Failed to delete " +
                                          updating.toString());
                }
            }
        } catch(FileNotFoundException e) {
            // pick a reasonable epoch number
            // this should only happen once when moving to a
            // new code version
            currentEpoch = epochOfZxid;
            // 记录currentEpoch
            writeLongToFile(CURRENT_EPOCH_FILENAME, currentEpoch);
        }
        if (epochOfZxid > currentEpoch) {
            // 抛出异常
        }
        try {
            acceptedEpoch = readLongFromFile(ACCEPTED_EPOCH_FILENAME);
        } catch(FileNotFoundException e) {
            // pick a reasonable epoch number
            // this should only happen once when moving to a
            // new code version
            acceptedEpoch = epochOfZxid;
            // 记录currentEpoch
            writeLongToFile(ACCEPTED_EPOCH_FILENAME, acceptedEpoch);
        }
        if (acceptedEpoch < currentEpoch) {
            // 抛出异常
        }
    } catch(IOException ie) {
        LOG.error("Unable to load database on disk", ie);
        throw new RuntimeException("Unable to run quorum server ", ie);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值