Zookeeper源码解析之选举算法(二)

1. 找到zk的入口类

image-20200811214326701

image-20200811214823125

在zkServer.sh里面我们看到这里是启动类,那么我们就好好的看看这个类

2. Zookeeper的入口类QuorumPeerMain的main方法

/**
 * To start the replicated server specify the configuration file name on the command line.
 * @param args path to the configfile
 */
public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();

    /**
     * 代码结构中:try中的是最重要的代码
     * 如果看  exception,都是容错和异常处理的代码
     * 如果 finnly 里面有代码,一定要看,一定很重要。大部分代码都是 stop()  close()
     */
    try {

        /**
         * TODO_MA 启动入口
         */
        main.initializeAndRun(args);

    } catch (IllegalArgumentException e) {
        LOG.error("Invalid arguments, exiting abnormally", e);
        LOG.info(USAGE);
        System.err.println(USAGE);
        System.exit(2);
    } catch (ConfigException e) {
        LOG.error("Invalid config, exiting abnormally", e);
        System.err.println("Invalid config, exiting abnormally");
        System.exit(2);
    } catch (Exception e) {
        LOG.error("Unexpected exception, exiting abnormally", e);
        System.exit(1);
    }
    LOG.info("Exiting normally");
    System.exit(0);
}

2.1 main.initializeAndRun(args)

    /**
     * TODO QuorumPeer 管理和代表了一台服务器,那么 QuorumPeerConfig 就一定是用来管理 配置的
     *   启动的之后, zkserver 会去加载 zoo.cfg , 这里面的配置,都会读取进来保存在 QuorumPeerConfig
     * @param args
     * @throws ConfigException
     * @throws IOException
     */
    protected void initializeAndRun(String[] args) throws ConfigException, IOException {
        // 解析配置,如果传入的是配置文件(参数只有一个),解析配置文件并初始化QuorumPeerConfig
        // 集群模式,会解析配置参数
        QuorumPeerConfig config = new QuorumPeerConfig();

        if (args.length == 1) {   // "zoo.cfg的路径"

            // TODO 解析参数,通过 Properties 方式来进行。
            config.parse(args[0]);
        }

        /**
         * DatadirCleanupManager 线程,由于 ZooKeeper 的任何一个变更操作都产生事务,事务日志需要持久化到硬盘,
         * 同时当写操作达到一定量或者一定时间间隔后,会对内存中的数据进行一次快照并写入到硬盘上的 snapshop 中,
         * 快照为了缩短启动时加载数据的时间从而加快整个系统启动。
         * 而随着运行时间的增长生成的 transaction log 和 snapshot 将越来越多,所以要定期清理,
         * DatadirCleanupManager 就是启动一个 TimeTask 定时任务用于清理 DataDir 中的 snapshot 及对应的 transaction log。
         *
         * DatadirCleanupManager主要有两个参数:
         * 	snapRetainCount:清理后保留的snapshot的个数,对应配置:autopurge.snapRetainCount,大于等于3,默认3
         * 	purgeInterval:清理任务TimeTask执行周期,即几个小时清理一次,对应配置:autopurge.purgeInterval,单位:小时
         */
        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config.getDataDir(), config.getDataLogDir(),
                config.getSnapRetainCount(), config.getPurgeInterval());
        // 启动清理文件的线程
        purgeMgr.start();
        // zk 的数据会不断的从该内存快照到磁盘。快照文件越来越多!
        // DatadirCleanupManager 就是用来定期清理多余的快照文件。  每次清理,最少会依然保存3个最近的快照

        /**
         * 根据配置中的 servers 数量判断是集群环境还是单机环境,如果单机环境以 standalone 模式运行
         * 直接调用 ZooKeeperServerMain.main()方法,否则进入集群模式中。
         * 集群命令格式:zkServer.sh start
         */
        // 集群模式  zkServer.sh start     servers.size = 4
        if (args.length == 1 && config.servers.size() > 0) {
            /**
             * 集群模式,生产环境,毫无疑问,都是集群模式,所以重点关注集群模式
             */
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running in standalone mode");
            // there is only server in the quorum -- run as standalone
            /**
             * 单机模式
             */
            ZooKeeperServerMain.main(args);
        }
    }

2.2 runFromConfig

/**
 * TODO_MA 集群模式启动
 * @param config
 * @throws IOException
 */
public void runFromConfig(QuorumPeerConfig config) throws IOException {

    // 加载 日志 组件的
    try {
        ManagedUtil.registerLog4jMBeans();
    } catch (JMException e) {
        LOG.warn("Unable to register log4j JMX control", e);
    }

    /**
     * QuorumPeer : 代表 一台服务器
     */
    LOG.info("Starting quorum peer");
    try {

        /**
         * 创建 ServerCnxnFactory 实例, ServerCnxnFactory 从名字就可以看出其是一个工厂类,负责管理 ServerCnxn,
         * ServerCnxn 这个类代表了一个客户端与一个 server 的连接,每个客户端连接过来都会被封装成一个 ServerCnxn 实例用
         * 来维护了服务器与客户端之间的 Socket 通道。
         *
         * 首先要有监听端口,客户端连接才能过来,ServerCnxnFactory.configure()方法的核心就是启动监听端口供客户端连接进来,
         * 端口号由配置文件中clientPort属性进行配置,默认是2181
         *
         * ServerCnxnFactory 有 NIOServerCnxnFactory 和 NettyServerCnxnFactory 两种。
         *
         * NIOServerCnxnFactory = cnxnFactory
         *
         * NIOServerCnxnFactory 的作用:? 监听 2181 端口  如果有客户端发送请求过来,则由这个组件进行处理
         * 如果有一个 客户端发送链接请求过来,NIOServerCnxnFactory 处理了之后,就会生成一个 ServerCnxn 来负责管理
         */
        ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
        cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns());
        // 到这为止,只是初始化,并没有启动

        /**
         * 获取 QuorumPeer, zk的逻辑主线程,负责选举、投票
         * QuorumPeer 是一个线程,注意它的 start 和 run 方法。
         * 里头有一个内部类:QuorumServer
         *
         * QuorumPeer quorumPeer = getQuorumPeer();
         * quorumPeer.setXXXX(XXXX);
         */
        quorumPeer = getQuorumPeer();

        quorumPeer.setQuorumPeers(config.getServers());

        //FileTxnSnapLog主要用于snap和transaction log的IO工具类
        quorumPeer.setTxnFactory(new FileTxnSnapLog(new File(config.getDataLogDir()), new File(config.getDataDir())));
        //选举类型,用于确定选举算法
        quorumPeer.setElectionType(config.getElectionAlg());
        //设置myid
        quorumPeer.setMyid(config.getServerId());
        //设置心跳时间
        quorumPeer.setTickTime(config.getTickTime());
        //设置初始化同步时间
        quorumPeer.setInitLimit(config.getInitLimit());
        //设置节点间状态同步时间
        quorumPeer.setSyncLimit(config.getSyncLimit());

        quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());

        //ServerCnxnFactory客户端请求管理工厂类  NIOServerCnxnFactory
        quorumPeer.setCnxnFactory(cnxnFactory);

        quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
        quorumPeer.setClientPortAddress(config.getClientPortAddress());
        quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
        quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
        //ZKDatabase维护ZK在内存中的数据结构, ZKDatabase 就是用来管理 DataTree 的
        quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
        //服务节点的角色类型:两种类型()
        // zk 服务器的类型:PARTICIPANT, OBSERVER; 只是在 zoo.cfg 中用来进行配置的。
        // PARTICIPANT: LEADER, FOLLOWER
        // 只有服务器被配置成 PARTICIPANT 类型,才有资格 有选举权  和 被选举权
        // learner:  OBSERVER + FOLLOWER
        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 的 run() 方法
         * 启动主线程,QuorumPeer重写了Thread.start方法
         */
        quorumPeer.start();    // 既有可能是线程。
        quorumPeer.join();

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

3. quorumPeer.start()

@Override
public synchronized void start() {

    /**
     * 刚启动的时候,需要加载数据库, 从磁盘中的 数据快照文件 和 comitted logs 中恢复
     * 涉及到的核心类是 ZKDatabase,并借助于 FileTxnSnapLog 工具类将 snap 和 transaction log
     * 反序列化到内存中,最终构建出内存数据结构 DataTree。
     *
     * 总结:从事务日志目录dataLogDir和数据快照目录dataDir中恢复出DataTree数据
     * 涉及到的核心类是ZKDatabase,并借助于FileTxnSnapLog工具类将snap和transaction log反序列化到内存中,最终构建出内存数据结构DataTree
     */
    loadDataBase();

    /**
     * 服务连接:开启对客户端的连接端口,启动ServerCnxnFactory主线程
     *
     * ServerCnxnFactory的作用:构建reactor模型的EventLoop,Selector每隔1秒执行一次select方法来处理IO请求,
     * 并分发到对应的代表该客户端的ServerCnxn中并利用doIO进行处理
     *
     * NIOServerCnxnFactory 负责 创建 ServerCnxn 负责和客户端进行数据读写通信的
     *
     * 调用这句代码的时候,跳转到 new ZooKeeperThread(this).start()
     * this = cnxnFactory
     * 跳转到自己的 run 方法
     */
    cnxnFactory.start();    // 卡住了

    /**
     * 开始选举, 这儿并不是真正的选举,而是只是初始化选举需要的各种组件
     */
    startLeaderElection();

    /**
     * TODO_MA 到此为止,一定要记得:
     *  1、FastLeaderElection 的 WorkerReceiver 和 WorkerSender 都启动好了。
     *  2、QuorumCnxManager 的 Listen监听 也启动好了,等待有其他 Server 的链接的话,就会创建成对的 RecvWorker 和 SendWorker
     */

    /**
     * 线程启动 跳转到 run() 方法。
     * 记得,上面的代码执行完了之后,会跳转到 run() 方法。 因为  QuorumPeer 是一个线程!
     * 启动QuorumPeer线程,在该线程中进行服务器状态的检查
     */
    super.start();
}

4. loadDataBase();

   File updating = new File(getTxnFactory().getSnapDir(), UPDATING_EPOCH_FILENAME);
    try {

        /**
         * TODO_MA 加载数据   ZKDataBase zkDb
         *  最重要的两件事:
         *  1、真的加载了磁盘中的数据(快照文件数据 + CommitedLog)到内存(DataTree)
         *  2、返回了当前这个 server节点中的 最大的 zxid
         */
        zkDb.loadDataBase();

        /**
         * load the epochs  这是已得到的 lastProcessedZxid
         * 从最新的 zxid 恢复 epoch 变量、zxid 64位,前32位是 epoch 的值,后 32 位是zxid
         * 这段代码的目的是寻找到  acceptEpoch
         */
        long lastProcessedZxid = zkDb.getDataTree().lastProcessedZxid;
        long epochOfZxid = ZxidUtils.getEpochFromZxid(lastProcessedZxid);

        try {

            //从文件中读取当前的epoch
            currentEpoch = readLongFromFile(CURRENT_EPOCH_FILENAME);

            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;
            LOG.info(CURRENT_EPOCH_FILENAME + " not found! Creating with a reasonable default of {}. This should only happen when you are " +
                    "upgrading your installation", currentEpoch);
            writeLongToFile(CURRENT_EPOCH_FILENAME, currentEpoch);
        }
        if (epochOfZxid > currentEpoch) {
            throw new IOException("The current epoch, " + ZxidUtils
                    .zxidToString(currentEpoch) + ", is older than the last zxid, " + lastProcessedZxid);
        }
        try {
            /**
             * TODO 从配置文件读 acceptedEpoch
             */
            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;
            LOG.info(ACCEPTED_EPOCH_FILENAME + " not found! Creating with a reasonable default of {}. This should only happen when you are " +
                    "upgrading your installation", acceptedEpoch);
            writeLongToFile(ACCEPTED_EPOCH_FILENAME, acceptedEpoch);
        }
        if (acceptedEpoch < currentEpoch) {
            throw new IOException("The accepted epoch, " + ZxidUtils.zxidToString(acceptedEpoch) + " is less than the current epoch, " + ZxidUtils
                    .zxidToString(currentEpoch));
        }
    } catch (IOException ie) {
        LOG.error("Unable to load database on disk", ie);
        throw new RuntimeException("Unable to run quorum server ", ie);
    }
}

ZXID是什么?

我们应该知道我们古代有一些朝代,或者我们想美国总统,例如美国总统有好几任,是吧

其实可以就是这么想,例如朝代就是康熙11年,就是康熙当皇帝的第11年,或者道光3年,类似这样的一种计数方式

1、epoch:就相当于康熙 道光 民国 ,就是某个server上位之后就会生成一个新的epoch

2、txid:每个server不一样那么txid就会从头开始即 例如 a01 a02 a03

zk集群里面每一次事务操作,都会生成一个全局唯一的zxid

1、选举
2、广播

4.1 loadDataBase

/**
 * load the database from the disk onto memory and   从磁盘加载数据到内存
 * also add the transactions to the committedlog in memory.     在内存中完成已提交日志的事务操作,得到下一个 zxid
 * @return the last valid zxid on disk
 * @throws IOException
 */
public long loadDataBase() throws IOException {
    // 从磁盘加载数据, 从快照恢复数据
    // FileSnapTxnLog snapLog
    long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);

    // 已经初始化好
    initialized = true;

    // 返回 zxid
    return zxid;
}

4.2 restore

public long restore(DataTree dt, Map<Long, Integer> sessions, PlayBackListener listener) throws IOException {
    /**
     * 第一个操作:先把 已经持久化到磁盘的 快照数据反序列化到内存中。
     */
    snapLog.deserialize(dt, sessions);

    /**
     * 第二个操作:从 ComittedLogs 去加载数据。
     * 在最后一次快照之后,也有一些新的 事务被提交了,这些事务的日志记录在日志文件。
     * 冷启动,也需要加载这个数据
     * 也会返回这些日志中的, maxZXID
     */
    return fastForwardFromEdits(dt, sessions, listener);
}

这一行代码的意思就是从磁盘恢复日志到内存里面的DataTree里面,然后把最大的zxid返回就是最大事务id也就是最后执行的事务id,其实也就是WAL机制的冷启动问题

5. cnxnFactory.start()

@Override
public void start() {
    // ensure thread is started once and only once
    if (thread.getState() == Thread.State.NEW) {

        /**
         * TODO_MA 去找 ZooKeeperThread 的 run 方法!
         *  事实上,去找 当前这个类的 run() 方法。因为 ZooKeeperThread 这个类的第一个参数是 this
         */
        thread.start();

        // 事实上,跳转到 当前这个 工厂类的 run 方法
    }
}

5.1 NIOServerCnxnFactory

/**
 * TODO_MA 等待用户的请求过来,进行处理。
 */
public void run() {

    // ServerSocket
    while (!ss.socket().isClosed()) {
        try {

            /**
             * TODO 等待用户连接
             * NIO 的 核心API:
             *   Buffer
             *   Channel(InputStream + OutputStream  Buffer)
             *   Selector(从所有的channle找找 IO就绪的 chanel )
             *   Reactor
             */
            selector.select(1000);   // 阻塞方法
            Set<SelectionKey> selected;
            synchronized (this) {

                // 拿到的结果
                selected = selector.selectedKeys();
            }
            ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(selected);
            Collections.shuffle(selectedList);
            for (SelectionKey k : selectedList) {

                /**
                 * TODO 建立连接部分  OP_ACCEPT
                 */
                if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
                    SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();
                    InetAddress ia = sc.socket().getInetAddress();
                    int cnxncount = getClientCnxnCount(ia);
                    if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns) {
                        LOG.warn("Too many connections from " + ia + " - max is " + maxClientCnxns);
                        sc.close();
                    } else {
                        LOG.info("Accepted socket connection from " + sc.socket().getRemoteSocketAddress());
                        sc.configureBlocking(false);
                        SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);

                        /**
                         * TODO_MA 处理连接请求
                         *  每个客户端请求过来,都会相应的创建一个 NIOServerCnxn 来进行处理
                         */
                        NIOServerCnxn cnxn = createConnection(sc, sk);
                        sk.attach(cnxn);
                        addCnxn(cnxn);
                    }

                /**
                 * TODO 读数据进行处理部分   OP_READ   OP_WRITE
                 */
                } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
                    NIOServerCnxn c = (NIOServerCnxn) k.attachment();

                    /**
                     * TODO_MA 读数据进行处理!
                     */
                    c.doIO(k);

                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Unexpected ops in select " + k.readyOps());
                    }
                }
            }
            selected.clear();
        } catch (RuntimeException e) {
            LOG.warn("Ignoring unexpected runtime exception", e);
        } catch (Exception e) {
            LOG.warn("Ignoring exception", e);
        }
    }
    closeAll();
    LOG.info("NIOServerCnxn factory exited run method");
}

这个时候并没有请求进来所以,这里是没有任何处理的,只是在不停的轮训

6. startLeaderElection

/**
 * Leader 选举涉及到节点间的网络 IO,QuorumCnxManager 就是负责集群中各节点的网络 IO,
 * QuorumCnxManager 包含一个内部类 Listener,Listener 是一个线程,这里启动 Listener 线程,
 * 主要启动选举监听端口并处理连接进来的 Socket;
 * FastLeaderElection 就是封装了具体选举算法的实现。
 */
synchronized public void startLeaderElection() {
    try {

        /**
         * TODO 投票是投给自己的哟: 一个 vote 包含 myid, zxid, epoch 三个重要的信息
         * 选票!
         * 1、myid   config.getMyID(zoo.cfg)
         * 2、zxid   epoch   ====> loadDataBase()
         */
        currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());

    } catch (IOException e) {
        RuntimeException re = new RuntimeException(e.getMessage());
        re.setStackTrace(e.getStackTrace());
        throw re;
    }
    for (QuorumServer p : getView().values()) {
        if (p.id == myid) {
            myQuorumAddr = p.addr;
            break;
        }
    }
    if (myQuorumAddr == null) {
        throw new RuntimeException("My id " + myid + " not in the peer list");
    }
    if (electionType == 0) {
        try {
            /**
             * TODO_MA 通过 UDP 协议往所有 LOOKING 状态的服务器广播选票
             */
            udpSocket = new DatagramSocket(myQuorumAddr.getPort());
            responder = new ResponderThread();
            responder.start();

        } catch (SocketException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 这里是初始化选举算法,从 zoo.cfg 中可以得知,默认的选举算法是: FastPaxos(FastLeaderElection) 算法
     * 初始化网络通信组件:QuorumCnxManager,当中会初始化一个 Listener,监听客户端的链接
     * 初始化选举算法实例:FastLeaderElection, 负责选举流程的正常进行
     *
     * 负责具体选举的 那个 算法实例, 现在才开始创建!
     *
     * electionAlg = FastLeaderElection
     */
    this.electionAlg = createElectionAlgorithm(electionType);
}

选举的机制

选举神奇吗,其实一点都不神奇,我们可以看到,每一张选票都带有epoch、zxid、myid。

1、首先选举的就是epoch大的,只要是epoch小的都不要

2、当epoch一样大的时候,选择zxid大的。

3、如果zxid也一样,选serverid大的,也就是myid

总体的原则:

选票: 发送给其他人用来赞成或者反对的提案

投票: 对选票投赞成或者反对票

1、每个server一启动都是把自己生成的epoch、zxid、myid构建成自己的选票,广播给所有其他节点,通俗来讲,就是每个server启动之后就去找leader找不到,就会构建自己的选票,然后广播给所有的server

2、每次接收到一个选票,就会进行一次epoch、zxid、myid的比较,如果自己的不满足要求,就把自己的选票更新成别人的,然后再次广播。

3、如果每次接受到的投票中,有一个投票是赞成自己的,那么执行判断方法,来判断,迄今为止接收到的所有投票,是否有超过半数是同意自己的

4、如果有,那么就更改自己的状态(LOOKING变成LEADING),因为有可能会出现,其他的server还在投票,而且还有大于自己的epoch的,把自己的选票再广播一次

5、再获取一个投票,看看是否有获取到的投票的epoch zxid是否比自己的大, 如果不比自己大,则称为leader

6.1 createElectionAlgorithm

/**
 * TODO_MA Leader 选举涉及到两个核心类:QuorumCnxManager 和 FastLeaderElection。
 * @param electionAlgorithm
 * @return
 */
protected Election createElectionAlgorithm(int electionAlgorithm) {
    Election le = null;

    //TODO: use a factory rather than a switch
    switch (electionAlgorithm) {
        case 0:
            le = new LeaderElection(this);
            break;
        case 1:
            le = new AuthFastLeaderElection(this);
            break;
        case 2:
            le = new AuthFastLeaderElection(this, true);
            break;
        case 3:

            /**
             * TODO_MA 选举过程中的 IO 负责类  qcm = QuorumCnxManager
             * 选举过程中的,一切网络通信,都由这个组件负责
             * QuorumCnxManager : 负责选举过程中的网络通信  3888
             * ServerCnxnFactory:   负责客户端和 server段进行通信的组件  2181
             *
             * 在这个 createCnxnManager(); 代码中,创建了 Listener
            */
            qcm = createCnxnManager();

            /**
             * QuorumCnxManager 有一个内部类 Listener,初始化一个 ServerSocket,然后在一个 while 循环中调用 accept 接收客户端
             * (注意:这里的客户端指的是集群中其它服务器)连接。
             * 当有客户端连接进来后,会将该客户端 Socket 封装成 RecvWorker 和 SendWorker,它们都是线程,分别负责和该 Socket 所代表的客户端进行读写。
             * 其中,RecvWorker 和 SendWorker 是成对出现的,每对负责维护和集群中的一台服务器进行网络 IO 通信。
             */
            QuorumCnxManager.Listener listener = qcm.listener;
            if (listener != null) {
                /**
                 * 启动监听! Listener 监听选举端口进来的 client 请求。
                 * 把进行选举锁需要的各种网络通信组件,都启动好了。
                 *
                 * 启动了 QuorumCnxnManager 中的俩个线程   SendWorker(广播选票)   RecvWorker(从外界接受投票结果)(三个map联合管理接收结果的)
                 * QuorumCnxnManager 这个对象中, 也有一个队列:  recvQueue
                 */
                listener.start();

                /**
                 * TODO 选举算法具体实现类!
                 * 启动了 选举算法实例: 启动了两个线程: WorkerReceiver(构建选票,发送选票,唱票)  WorkerSender(负责发送)
                 * 这个选举算法中,还有两个队列:  sendqueue<ToSend>,  recvqueue<Notification>
                 */
                le = new FastLeaderElection(this, qcm);

            } else {
                LOG.error("Null listener when initializing cnx manager");
            }
            break;
        default:
            assert false;
    }
    return le;
}

6.2 Listener的run方法

/**
 * Thread to listen on some port
 * 负责各个server之间的通信,维护了和各个server之间的连接,下面的线程负责与其他server建立连接
 */
public class Listener extends ZooKeeperThread {

    volatile ServerSocket ss = null;

    public Listener() {
        // During startup of thread, thread name will be overridden to
        // specific election address
        super("ListenerThread");
    }

    /**
     * Sleeps on accept().
     */
    @Override
    public void run() {
        int numRetries = 0;
        InetSocketAddress addr;
        while ((!shutdown) && (numRetries < 3)) {
            try {
                /**
                 * TODO 初始化服务器
                 */
                ss = new ServerSocket();
                ss.setReuseAddress(true);
                if (listenOnAllIPs) {
                    int port = view.get(QuorumCnxManager.this.mySid).electionAddr.getPort();
                    addr = new InetSocketAddress(port);
                } else {
                    addr = view.get(QuorumCnxManager.this.mySid).electionAddr;
                }

                /**
                 * 绑定选举端口  3888
                 */
                LOG.info("My election bind port: " + addr.toString());
                setName(view.get(QuorumCnxManager.this.mySid).electionAddr.toString());
                ss.bind(addr);

                /**
                 * 等待客户端(其他服务器)的链接!
                 */
                while (!shutdown) {

                    /**
                     * 客户端 Socket 链接管理对象。 卡在这儿。
                     */
                    Socket client = ss.accept();   // 阻塞的方法
                    setSockOpts(client);
                    LOG.info("Received connection request " + client.getRemoteSocketAddress());

                    // Receive and handle the connection request asynchronously if the quorum sasl authentication is enabled.
                    // This is required  because sasl server authentication process may take few seconds to finish,
                    // this may delay next peer connection requests.
                    if (quorumSaslAuthEnabled) {
                        receiveConnectionAsync(client);
                    } else {

                        /**
                         * TODO_MA 接受链接
                         */
                        receiveConnection(client);
                    }
                    numRetries = 0;
                }
            } catch (IOException e) {
                LOG.error("Exception while listening", e);
                numRetries++;
                try {
                    ss.close();
                    Thread.sleep(1000);
                } catch (IOException ie) {
                    LOG.error("Error closing server socket", ie);
                } catch (InterruptedException ie) {
                    LOG.error("Interrupted while sleeping. " + "Ignoring exception", ie);
                }
            }
        }
        LOG.info("Leaving listener");
        if (!shutdown) {
            LOG.error("As I'm leaving the listener thread, " + "I won't be able to participate in leader " + "election any longer: " + view
                    .get(QuorumCnxManager.this.mySid).electionAddr);
        }
    }

6.3 FastLeaderElection类的注释

/**
 * TODO_MA FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader方法和shutdown方法,
 *  其是标准的Fast Paxos算法的实现,各服务器之间基于TCP协议进行选举。
 *
 * TODO_MA 使用TCP实现 leader 选举。它使用 QuorumCnxManager 类的对象来管理连接。
 *  否则,与其他UDP实现一样,该算法基于推送。
 * Implementation of leader election using TCP. It uses an object of the class QuorumCnxManager to manage connections.
 * Otherwise, the algorithm is push-based as with the other UDP implementations.
 *
 * TODO_MA 这里有一些参数可以调整以更改其行为。首先,finalizeWait 确定等待直到决定领导者的时间。
 *  这是领导者选举算法的一部分。
 * finalizeWait = 200ms
 * There are a few parameters that can be tuned to change its behavior. First,
 * finalizeWait determines the amount of time to wait until deciding upon a leader.
 * This is part of the leader election algorithm.
 *
 * TODO_MA Messenger中维护了一个 WorkerSender和 WorkerReceiver,分别表示选票发送器和选票接收器。
 *
 *
 * ZooKeeper 的底层分布式一致性算法: paxos (basic paxos,  fast paxos,  multi paxos)
 * ZooKeeper 的底层选举算法实现:fast paxo
 * 具体的实现类就是:FastLeaderElection
 */

6.4 FastLeaderElection的starter

/**
 * This method is invoked by the constructor. Because it is a
 * part of the starting procedure of the object that must be on
 * any constructor of this class, it is probably best to keep as
 * a separate method. As we have a single constructor currently,
 * it is not strictly necessary to have it separate.
 *
 * @param self    QuorumPeer that created this object
 * @param manager Connection manager
 *
 *
 * TODO_MA 其完成在构造函数中未完成的部分,如会初始化FastLeaderElection的sendqueue和recvqueue,并且启动接收器和发送器线程。
 */
private void starter(QuorumPeer self, QuorumCnxManager manager) {

    // 赋值,对Leader和投票者的ID进行初始化操作
    this.self = self;
    proposedLeader = -1;
    proposedZxid = -1;

    // 初始化发送队列( 发起选举者 发送的选票信息,就被封装成 ToSend 放在一个同步队列里面)
    sendqueue = new LinkedBlockingQueue<ToSend>();
    // 操作 sendqueue 的就是  workerSender

    // 初始化接收队列(发起选举者 接受的投票结果信息,也被放在一个队列里面等待处理 )
    recvqueue = new LinkedBlockingQueue<Notification>();
    // 操作 recvqueue 就是 workerReceiver

    // 创建Messenger,会启动接收器和发送器线程
    this.messenger = new Messenger(manager);
}

其实我们看到这里应该就已经看到了一些眉目了,其实看zk的源码就是看他的选举机制因为paxos算法,其他的也很重要但是可以慢慢看 并不是很着急

6.5 总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e76liuMD-1597328279286)(https://gitee.com/wuqingzhi128/blogImg/raw/master//未命名文件 (2)].png)

7. QuorumPeer的run方法

public void run() {
    setName("QuorumPeer" + "[myid=" + getId() + "]" + cnxnFactory.getLocalAddress());

    LOG.debug("Starting quorum peer");
    try {
        jmxQuorumBean = new QuorumBean(this);
        MBeanRegistry.getInstance().register(jmxQuorumBean, null);
        for (QuorumServer s : getView().values()) {
            ZKMBeanInfo p;
            if (getId() == s.id) {
                p = jmxLocalPeerBean = new LocalPeerBean(this);
                try {
                    MBeanRegistry.getInstance().register(p, jmxQuorumBean);
                } catch (Exception e) {
                    LOG.warn("Failed to register with JMX", e);
                    jmxLocalPeerBean = null;
                }
            } else {
                p = new RemotePeerBean(s);
                try {
                    MBeanRegistry.getInstance().register(p, jmxQuorumBean);
                } catch (Exception e) {
                    LOG.warn("Failed to register with JMX", e);
                }
            }
        }
    } catch (Exception e) {
        LOG.warn("Failed to register with JMX", e);
        jmxQuorumBean = null;
    }

    /**
     * QuorumPeer 线程进入到一个无限循环模式,不停的通过 getPeerState 方法获取当前节点状态,然后执行相应的分支逻辑
     */
    try {
        /**
         * TODO_MA 主要逻辑
         * Main loop
         * 只要 当前这台服务器一直为 looking 状态, 停止选举的开关也一直开着。
         * 一直进行选举,直到选出来leader为止!
         *
         * 重点
         * 1、lookForLeader() 选举的具体实现(只是通过唱票发现自己能成为leader,并不代表自己一定是leader)
         * 2、follower.followLeader(); 数据同步的具体实现
         * 3、leader.lead();  先校验!我再广播一次我的选票给所有其他服务器,然后再次验证是否超过半数同意自己
         *
         * 特例:
         * 1、我是刚启动的server, 这个集群的leader早就存在了。
         *    我一上线,也会调用lookForLeader找leader。(构建自己的选票,广播给所有用户)
         *
         * 2、因为集群已经启动,leader, follower角色已经确定。所有的角色都有保存现在的合法选票!
         *  这个选票其实保存的信息就是:leader的:ephoch  serverid
         *
         * 3、所有角色都会在收到 刚上线的server的选票的时候,都会把现在的合法选票 发回给这个server
         *  刚启动的这个server应该要进行唱票,最终发现大家返回的都是leader的选票。 唱票之后,发现这个leader获得的投票已经
         *  超过半数,也就意味着,能确认这个集群中已经有leader了, 自己立即成为 learner(根据配置来决定到底是 follower 还是 observer)
         */
        while (running) {
            switch(getPeerState()){
                case LOOKING:
                    // 先找到选举算法实例,然后调用他的 lookForLeader() 进行选举
                    // 在 lookForLeader() 里面有唱票和改变状态的操作,下一次可能就不是looking状态
                    // lookForLeader  找leader, 找不到就举行选举
                    setCurrentVote(makeLEStrategy().lookForLeader());
                    break;
                case FOLLOWING:
                    setFollower(makeFollower(logFactory));
                    follower.followLeader();   // 数据同步!
                    break;
                case OBSERVING:
                    setObserver(makeObserver(logFactory));
                    observer.observeLeader();
                    break;
                case LEADING:
                    setLeader(makeLeader(logFactory));
                    leader.lead();  // 在校验过程中,还需要确认有超过半数节点存活
                    break;
            }

            /**
             * 根据当前节点自己的状态,去做响应的处理。
             */
            switch (getPeerState()) {
                /**
                 * 首先系统刚启动时 serverState 默认是 LOOKING,表示需要进行 Leader 选举,这时进入 Leader 选举状态中,
                 * 会调用 FastLeaderElection.lookForLeader 方法,lookForLeader 方法内部也包含了一个循环逻辑,
                 * 直到选举出 Leader 才会跳出 lookForLeader 方法,如果选举出的 Leader 就是本节点,
                 * 则将 serverState=LEADING 赋值,否则设置成 FOLLOWING 或 OBSERVING。
                 */
                case LOOKING:               //如果是looking,则进入选举流程
                    LOG.info("LOOKING");

                    if (Boolean.getBoolean("readonlymode.enabled")) {
                        LOG.info("Attempting to start ReadOnlyZooKeeperServer");

                        // Create read-only server but don't start it immediately
                        final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(logFactory, this,
                                new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb);

                        // Instead of starting roZk immediately, wait some grace period before we decide we're partitioned.
                        //
                        // Thread is used here because otherwise it would require changes in each of election strategy classes which is
                        // unnecessary code coupling.
                        Thread roZkMgr = new Thread() {
                            public void run() {
                                try {
                                    // lower-bound grace period to 2 secs
                                    sleep(Math.max(2000, tickTime));
                                    if (ServerState.LOOKING.equals(getPeerState())) {

                                        /**
                                         * TODO_MA 启动服务器
                                         */
                                        roZk.startup();
                                    }
                                } catch (InterruptedException e) {
                                    LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
                                } catch (Exception e) {
                                    LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
                                }
                            }
                        };
                        try {

                            /**
                             * TODO_MA 启动服务器
                             */
                            roZkMgr.start();
                            setBCVote(null);

                            /**
                             * TODO_MA 此处通过策略模式来决定当前哪个选举算法来进行领导选举
                             */
                            setCurrentVote(makeLEStrategy().lookForLeader());

                        } catch (Exception e) {
                            LOG.warn("Unexpected exception", e);
                            setPeerState(ServerState.LOOKING);
                        } finally {
                            // If the thread is in the the grace period, interrupt
                            // to come out of waiting.
                            roZkMgr.interrupt();
                            roZk.shutdown();
                        }
                    } else {
                        try {

                            /**
                             * TODO_MA  选举
                             */
                            setBCVote(null);

                            // TODO 重要: 调用 FastLeaderElection.lookForLeader();
                            // makeLEStrategy().lookForLeader()  返回的就是 leader 的选票
                            setCurrentVote(makeLEStrategy().lookForLeader());

                        } catch (Exception e) {

                            /**
                             * 如果选举报错,则继续设置自己的状态为 LOOKING 然后继续选举
                             */
                            LOG.warn("Unexpected exception", e);
                            setPeerState(ServerState.LOOKING);
                        }
                    }
                    break;

                /**
                 * 对于 Follower 和 Observer 而言,主要的初始化工作是要建立与 Leader 的连接并同步 epoch 信息,最后完成与 Leader 的数据同步
                 */
                case OBSERVING:
                    try {
                        LOG.info("OBSERVING");
                        setObserver(makeObserver(logFactory));

                        /**
                         * TODO_MA observer 和 Leader 做数据同步
                         */
                        observer.observeLeader();

                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        observer.shutdown();
                        setObserver(null);
                        setPeerState(ServerState.LOOKING);
                    }
                    break;

                /**
                 * 对于 Follower 和 Observer 而言,主要的初始化工作是要建立与 Leader 的连接并同步 epoch 信息,最后完成与 Leader 的数据同步
                 */
                case FOLLOWING:
                    try {
                        LOG.info("FOLLOWING");
                        setFollower(makeFollower(logFactory));

                        /**
                         * TODO_MA
                         */
                        follower.followLeader();

                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        follower.shutdown();
                        setFollower(null);
                        setPeerState(ServerState.LOOKING);
                    }
                    break;

                /**
                 * 然后 QuorumPeer.run 进行下一轮次循环,通过 getPeerState 获取当前 serverState 状态,
                 * 如果是 LEADING,则表示当前节点当选为 LEADER,则进入 Leader 角色分支流程,执行作为一个 Leader 该干的任务;
                 * 如果是 FOLLOWING 或 OBSERVING,则进入 Follower 或 Observer 角色,并执行其相应的任务。
                 * 注意:进入分支路程会一直阻塞在其分支中,直到角色转变才会重新进行下一轮次循环,
                 * 比如 Follower 监控到无法与 Leader 保持通信了,会将 serverState 赋值为 LOOKING,跳出分支并进行下一轮次循环,
                 * 这时就会进入 LOOKING 分支中重新进行 Leader 选举。
                 *
                 * Leader 会启动 LearnerCnxAcceptor 线程,该线程会接受来自 Follower 和 Observer(统称为 Learner)的连接请求
                 * 并为每个连接创建一个 LearnerHandler 线程,该线程会负责包括数据同步在内的与 learner 的一切通信。
                 */
                case LEADING:
                    LOG.info("LEADING");
                    try {

                        // Leader leader
                        // 改角色
                        setLeader(makeLeader(logFactory));

                        /**
                         * TODO_MA ZooKeeper 就会进入集群同步阶段,集群同步主要完成集群中各节点状态信息和数据信息的一致。
                         *  选出新的 Leader 后的流程大致分为:计算 epoch、统一 epoch、同步数据、广播模式等四个阶段。
                         *  其中其前三个阶段:计算 epoch、统一 epoch、同步数据就是这一节主要介绍的集群同步阶段的主要内容,
                         *  这三个阶段主要完成新 Leader 与集群中的节点完成同步工作,
                         *  处于这个阶段的 zk 集群还没有真正做好对外提供服务的能力,可以看着是新 leader
                         *  上任后进行的内部沟通、前期准备工作等,只有等这三个阶段全部完成,新 leader 才会真正的成为 leader,
                         *  这时 zookeeper 集群会恢复正常可运行状态并对外提供服务。
                         */
                        leader.lead();

                        setLeader(null);
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        if (leader != null) {
                            leader.shutdown("Forcing shutdown");
                            setLeader(null);
                        }
                        setPeerState(ServerState.LOOKING);
                    }
                    break;
            }
        }
    } finally {
        LOG.warn("QuorumPeer main thread exited");
        try {
            MBeanRegistry.getInstance().unregisterAll();
        } catch (Exception e) {
            LOG.warn("Failed to unregister with JMX", e);
        }
        jmxQuorumBean = null;
        jmxLocalPeerBean = null;
    }
}

8. lookForLeader

/**
 * TODO_MA 该函数用于开始新一轮的Leader选举,其首先会将逻辑时钟自增,(epoch)
 *  然后更新本服务器的选票信息(初始化选票),之后将选票信息放入 sendqueue 等待发送给其他服务器
 *
 * Starts a new round of leader election. Whenever our QuorumPeer changes its state to LOOKING, this method is invoked, and it
 * sends notifications to all other peers.
 */
public Vote lookForLeader() throws InterruptedException {

    // 一些准备操作
    try {
        self.jmxLeaderElectionBean = new LeaderElectionBean();
        MBeanRegistry.getInstance().register(self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
    } catch (Exception e) {
        LOG.warn("Failed to register with JMX", e);
        self.jmxLeaderElectionBean = null;
    }
    if (self.start_fle == 0) {
        self.start_fle = Time.currentElapsedTime();
    }

    try {
        // recvset 存储合法投票
        HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
        // 存储选举之外的投票结果
        HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();

        int notTimeout = finalizeWait;

        synchronized (this) {
            // 更新逻辑时钟,每进行一轮选举,都需要更新逻辑时钟
            // logicalclock  = epoch
            logicalclock.incrementAndGet();  //  +===> epoch + 1

            // 更新选票(serverid, zxid, epoch)
            updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
        }

        // 向其他服务器发送自己的选票
        LOG.info("New election. My id =  " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
        sendNotifications();

        /**
         * Loop in which we exchange notifications until we find a leader
         * 之后每台服务器会不断地从 recvqueue 队列中获取外部选票。如果服务器发现无法获取到任何外部投票,
         * 就立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,
         * 如果已经建立了连接,则再次发送自己当前的内部投票
         */
        // 本服务器状态为LOOKING并且还未选出leader
        while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
            /**
             * Remove next notification from queue, times out after 2 times the termination time
             *
             * ToSend: 选票
             * Notification: 投票
             */
            // 从recvqueue接收队列中取出投票
            Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);

            /**
             * Sends more notifications if haven't received enough. Otherwise processes new notification.
             */
            // 如果没有收到足够多的选票,则发送选票
            if (n == null) {
                // manager已经发送了所有选票消息
                if (manager.haveDelivered()) {
                    // 向所有其他服务器发送消息
                    sendNotifications();
                    // 还未发送所有消息
                } else {
                    // 连接其他每个服务器
                    manager.connectAll();
                }

                /**
                 * Exponential backoff
                 * 如果第一进行投票的时候,没有收到选票的话,会重新发起投票,然后等待更长的超时时间
                 * 这个超时更新机制就是 指数增长,2的倍数。
                 */
                int tmpTimeOut = notTimeout * 2;
                notTimeout = (tmpTimeOut < maxNotificationInterval ? tmpTimeOut : maxNotificationInterval);
                LOG.info("Notification time out: " + notTimeout);


                // 接收到一个投票结果
            // 投票者集合中包含接收到消息中的服务器id
                // n.leader 推举成leader 的SID
                // n.sid 哪个服务器发过来的
            } else if (validVoter(n.sid) && validVoter(n.leader)) {
                /**
                 * Only proceed if the vote comes from a replica in the voting view for a replica in the voting view.
                 * 确定接收消息中的服务器状态
                 * 对面发送投票回来的那个server是  follwer还是 leader 还是?
                 */
                switch (n.state) {

                    /**
                     * 如果对端发过来的 electionEpoch 大于自己,则表明重置自己的 electionEpoch,
                     * 然后清空之前获取到的所有投票 recvset,因为之前获取的投票轮次落后于当前则代表之前的投票已经无效了,
                     * 然后调用 totalOrderPredicate()将当前期望的投票和对端投票进行 PK,用胜出者更新当前期望投票,
                     * 然后调用 sendNotifications()将自己期望头破广播出去。
                     */
                    case LOOKING:
                        // If notification > current, replace and send messages out
                        // TODO_MA 其选举周期大于逻辑时钟,外部投票的选举轮次大于内部投票。
                        //  若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),
                        //  并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。
                        if (n.electionEpoch > logicalclock.get()) {

                            // 重新赋值逻辑时钟
                            logicalclock.set(n.electionEpoch);
                            // 清空选票
                            recvset.clear();

                            // 进行PK,选出较优的服务器( epoch   zxid  serverid )
                            // 前面三个参数是对方的,后面三个参数是自己的。按顺序进行比较
                            // updateProposal 更新选票
                            // totalOrderPredicate 怎么更新选票的逻辑
                            if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
                                updateProposal(n.leader, n.zxid, n.peerEpoch);
                                // 无法选出较优的服务器
                            } else {
                                // 更新选票
                                updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
                            }

                            // 广播消息
                            // 注意:这里不管哪一方胜出,都需要广播出去
                            // 这是因为由于 electionEpoch 落后导致之前发出的所有投票都是无效的,所以这里需要重新发送
                            sendNotifications();

                            // TODO_MA 选举周期小于逻辑时钟,不做处理,外部投票的选举轮次小于内部投票。
                            //  若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理。
                        } else if (n.electionEpoch < logicalclock.get()) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long
                                        .toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
                            }
                            break;

                            // TODO_MA 逻辑时钟相等,并且能选出较优的服务器.外部投票的选举轮次等于内部投票。
                            //  此时可以开始进行选票PK,如果消息中的选票更优,则需要更新本服务器内部选票,再发送给其他服务器。
                        } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
                            // 更新选票
                            updateProposal(n.leader, n.zxid, n.peerEpoch);
                            // 发送消息
                            sendNotifications();
                        }

                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Adding vote: from=" + n.sid + ", proposed leader=" + n.leader + ", proposed zxid=0x" + Long
                                    .toHexString(n.zxid) + ", proposed election epoch=0x" + Long.toHexString(n.electionEpoch));
                        }

                        // recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票
                        recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

                        // 判断是否能选出 leader
                        // 唱票
                        // recvset所有的投票结果集,new Vote() 当前自己持有的合法选票
                        if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) {

                            // 遍历已经接收的投票集合
                            // Verify if there is any change in the proposed leader
                            while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {

                                // 能够选出较优的服务器。选票有变更,比之前提议的Leader有更好的选票加入
                                if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
                                    // 将更优的选票放在recvset中
                                    recvqueue.put(n);
                                    break;
                                }
                            }

                            /**
                             * This predicate is true once we don't read any new relevant message from the reception queue
                             */
                            // 表示之前提议的Leader已经是最优的
                            if (n == null) {

                                // 设置服务器状态
                                self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING : learningState());

                                // 最终的选票
                                // endVote 就是 最终的选票 就是leader的选票
                                Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);

                                // 清空recvqueue队列的选票
                                leaveInstance(endVote);

                                // 返回选票
                                return endVote;
                            }
                        }
                        break;

                    case OBSERVING:
                        LOG.debug("Notification from observer: " + n.sid);
                        break;

                    case FOLLOWING:

                    // 处于LEADING状态
                    /**
                     * 若选票中的服务器状态为FOLLOWING或者LEADING时,其大致步骤会判断选举周期是否等于逻辑时钟,归档选票,
                     * 是否已经完成了Leader选举,设置服务器状态,修改逻辑时钟等于选举周期,返回最终选票
                     */
                    case LEADING:
                        /**
                         * Consider all notifications from the same epoch together.
                         */
                        // 与逻辑时钟相等
                        if (n.electionEpoch == logicalclock.get()) {

                            // 将该服务器和选票信息放入recvset中
                            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);

                                // 清空recvqueue队列的选票
                                leaveInstance(endVote);

                                // 返回最终选票
                                return endVote;
                            }
                        }

                        /**
                         * Before joining an established ensemble, verify a majority is following the same leader.
                         */
                        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);

                            // 清空recvqueue队列的选票
                            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)) {
                    LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
                }
                if (!validVoter(n.sid)) {
                    LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
                }
            }
        }
        return null;
    } finally {
        try {
            if (self.jmxLeaderElectionBean != null) {
                MBeanRegistry.getInstance().unregister(self.jmxLeaderElectionBean);
            }
        } catch (Exception e) {
            LOG.warn("Failed to unregister with JMX", e);
        }
        self.jmxLeaderElectionBean = null;
        LOG.debug("Number of connection processing threads: {}", manager.getConnectionThreadCount());
    }
}

到此选举算法就结束了,那么我们来回顾一下这个流程

image-20200813214415427

9. 选举算法的总结

image-20200813213854195

假定三台机器来进行选举,第一台机器先启动,然后启动其他的每个myid依次为1,2,3,并且选举轮次也是

  1. 还没有选举出来leader之前,所有的节点都是LOOKING的状态,也就是找leader的状态
  2. 首先会构造自己的选票,然后把自己选票发送给所有的参与选举的机器,然后就会一直去接收外部队列的里面获取外部选票,因为是场景驱动,所以此时没有机器获取到选票,只能获取到自己的
  3. 然后依次进行判断,当前选票和外部接收到的选票的epoch是否一样,此时肯定是一样,所以会进行判断zxid,发现zxid还是一样的,这个时候就判断myid,发现还是一样的,这个时候发现就是我自己的,然后就把自己的选票加入到了合法队列中
  4. 然后就判断合法队列中的这个选票是否大于半数,如果不大于继续循环,但是现在就一台机器,并且还发现,别的机器一直连不上,就处于了等待

现在启动第二台机器

  1. 第二台机器启动的时候还是LOOKING的状态,也就是找leader的状态
  2. 还是会构造自己的选票,发送给所有参与选举的机器
  3. 这个时候第一台机器发现第二台机器启动起来了,然后又发送了自己认为正确的选票给其他机器,在进行判断
  4. 第二台机器也会发送自己的选票,第二台机器先收到的是自己的选票,然后把自己的选票更新为最优的,然后发现唱票,发现不足半数
  5. 这个时候,第二台机器收到了第一台机器发来的选票依次判断epoch、zxid、myid发现myid不如我啊,就什么都不做
  6. 第一台机器发现,有新选票了,我要看看,然后进行对比,发现,我靠,我没它厉害,然后我认为他就是leader,然后把自己认为对的选票更新为他的选票
  7. 然后发送给所有机器,这个时候,第二台机器收到了这个选票,然后对于epoch、zxid、myid,然后把这条消息,添加到选票集合中
  8. 发现唱票成功,这个时候又会将这个选票进行发送一次,看看是否还是认为他是leader
  9. 如果是那么就开始更新状态,为leader,然后第一台启动的机器也发现,第二台机器是leader这个时候就开始更新自己的状态follower开始同步数据
  10. leader也会开始开放端口对外进行服务

现在启动第三台机器

  1. 先开始找leader发现找到了leader然后就更新自己的状态为follower
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值