zookeeper源码分析—— leader选举(一)

这篇文章有点长,如果你想知道zk怎么进行leader选举的话,请耐心看完,
最好是安装完zk源码之后再来看,对着源码来看这篇文章不容易晕车,好了,大家坐好,准备发车。。滴滴

一、 zookeeper节点角色。

leader节点
客户端的写请求一定会到这个节点,然后这个节点会同步到follower节点

follower节点
客户端的读请求会经过一定的算法分部到不同的flower节点,如果leader节点挂掉,follower会参与投票选举出新的leader

observer节点
一个特殊的节点,可以帮助解决zk的扩展性(如果大量客户端访问我们的集群,需要增加zk集群性能,导致zk性能下降,因为zk的数据变更需要半数以上的节点投票通过,找出网络消耗,增加投票成本。)

1.observer不参与投票,只接收投票结果
2.不属于zk的关键节点,但是一些高可用架构里最好有这个节点,用来提高性能

二、源码分析leader选举投票

1、zk启动入口分析
怎么找zk启动类呢? 我来看一下zk中bin目录下的启动脚本zkServer.sh
在这里插入图片描述
在这里插入图片描述
可能看不懂,没关系,找能看懂的。
在开头定义了一个ZOOMAIN的东西,看着像是java的包路径, 假设这个就是启动类,然后再往后看,不懂、不懂、不懂、哎哎哎,突然看到一个start,我靠有点兴奋了,start就是启动的意思啊、看看start里都干什么了,我看到了nohup这个命令,并且这个命令后面跟着ZOOMAIN。nohup这个命令是干什么用的呢?这个不就是运行我们的程序的命令嘛。好吧,我的假设成立了,zk的启动类就是ZOOMAIN后面跟的java类 QuorumPeerMain。
入口找到了,下面就一步一步分析一下zk选举的过程

2、QuorumPeerMain类分析
刚刚我们说了,QuorumPeerMain类是我们zk的入口,那么这个类做了哪些事情呢?
这个类里加了一些注释,有的我就不进行文字描述了
打开这个类,会发现这里有main方法,在main方法里有这么一段话:

QuorumPeerMain main = new QuorumPeerMain();
main.initializeAndRun(args);

main.initializeAndRun(args)就是初始化并运行我们的zk,接着去看initializeAndRun方法做了哪些事情,下面贴出源码

protected void initializeAndRun(String[] args)
        throws ConfigException, IOException, AdminServerException
    {
    
    
        //这个里面存的是我们在安装zk的时候在zoo.cfg中配置的东西,当然还有一些zk默认的配置
        QuorumPeerConfig config = new QuorumPeerConfig();
        if (args.length == 1) {
    
    
            config.parse(args[0]);
        }

        // Start and schedule the the purge task
        //启动并计划清除任务,这里面做了个定时器
        //留个疑问?清除什么东西,定时任务做了什么,后面慢慢解决,目前看不懂
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();

        //判断是集群模式还是单机模式、如果单机模式不会涉及选举算法
        if (args.length == 1 && config.isDistributed()) {
    
    //集群
            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);//单机
        }
    }

在上面源码中,我加了一些,这章我们主要是分析选举,那么其他的东西可以先抛开,
在分析选举前,我们要知道zk在什么时候进行选举
1、zk启动的时候会去选举
2、leader挂的时候会去选举
我们知道什么时候去选举了就好分析源码了,目前leader挂掉选举的源码还不知道在哪,所以先分析zk启动的时候选举的源码,说不定看着看着就发现第二种情况的选举源码了。

在源码中 if (args.length == 1 && config.isDistributed()) 这段代码是判断我们zk是集群呢还是单机呢,单机是没有选举的,所以我们看集群的方法 runFromConfig(config);

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

   LOG.info("Starting quorum peer");
   try {
    
    
       ServerCnxnFactory cnxnFactory = null;
       ServerCnxnFactory secureCnxnFactory = null;
       //为客户端提供读写2181这个端口的访问功能
       if (config.getClientPortAddress() != null) {
    
    
           cnxnFactory = ServerCnxnFactory.createFactory();
           cnxnFactory.configure(config.getClientPortAddress(),
                   config.getMaxClientCnxns(),
                   false);
       }

       if (config.getSecureClientPortAddress() != null) {
    
    
           secureCnxnFactory = ServerCnxnFactory.createFactory();
           secureCnxnFactory.configure(config.getSecureClientPortAddress(),
                   config.getMaxClientCnxns(),
                   true);
       }
       //这里是主逻辑,负责选举,投票
       //这下面会从配置文件里加载配置信息
       //quorumPeer继承了ZooKeeperThread,是个线程
       quorumPeer = getQuorumPeer();
       quorumPeer.setTxnFactory(new FileTxnSnapLog(
                   config.getDataLogDir(),
                   config.getDataDir()));
       quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());
       quorumPeer.enableLocalSessionsUpgrading(
           config.isLocalSessionsUpgradingEnabled());
       //quorumPeer.setQuorumPeers(config.getAllMembers());
       quorumPeer.setElectionType(config.getElectionAlg());
       quorumPeer.setMyid(config.getServerId());
       quorumPeer.setTickTime(config.getTickTime());
       quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
       quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
       quorumPeer.setInitLimit(config.getInitLimit());
       quorumPeer.setSyncLimit(config.getSyncLimit());
       quorumPeer.setConfigFileName(config.getConfigFilename());
       quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
       quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);
       if (config.getLastSeenQuorumVerifier()!=null) {
    
    
           quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false);
       }
       quorumPeer.initConfigInZKDatabase();
       quorumPeer.setCnxnFactory(cnxnFactory);
       quorumPeer.setSecureCnxnFactory(secureCnxnFactory);
       quorumPeer.setLearnerType(config.getPeerType());
       quorumPeer.setSyncEnabled(config.getSyncEnabled());
       quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());

       // 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);
   }
 }

这个方法看着多,不着急,慢慢看,我们发现这个方法里操作最多的就是给QuorumPeer类set的一些值,那我们打开这个类看下这个类的描述,这里我按照自己理解翻译了一下

/**
 * 这个类管理者我们的quorum协议,
 * 这个服务器处于三种状态
 * 1、leader选举   每个服务器将选举一个leader,最初提出自己是leader
 * 2、Follower    服务器(follower)将与leader同步并复制任何事务
 * 3、Leader      服务器器(Leader)将处理请求并转发给follower
 * 这个类设置一个数据报,这个数据报总是响应当前leader的看法
 * 这个响应包涵了xid   myid  leader_id   leader_zxid
 * 对当前leader的请求将只包含一个xid: int xid
 */
public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider {
    
    

QuorumPeer 这个类主要是干嘛的,我们大致清楚了,主要是封装我们的请求和响应,然后发送给其他服务器,是不是我们的选举算法会在这个类里呢?带着个疑问接着回到我们启动类中的runFromConfig方法中
刚刚我们在看QuorumPeer 类的时候我们看到了这个类是继承了ZooKeeperThread 类,是个线程,启动线程是start方法,所以在runFromConfig方法中肯定会有一个quorumPeer.start()方法。在runFromConfig方法中我们也找到了quorumPeer.start()方法,这个方法处于启动类QuorumPeerMain的207行,执行了start方法,我们需要去找到quorumPeer中的run()方法,这个才是线程启动之后要做的事情。
好了,我们又回到QuorumPeer 类中,去搜索run()方法,这里需要注意了,你会搜索到ResponderThread内部类下的run()方法,这个内部类在3.4.0之后被弃用了。所以不要去看这个内部类的run方法了。真的的run方法是在1112

下面我们看下这个run方法里到底做了哪些事情,这里截取部分代码

try {
    
    
            /*
             * Main loop
             */
            while (running) {
    
    
                switch (getPeerState()) {
    
      //判断当前节点状态
                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, 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() {
    
    

在1147行,我们发现有个注释叫 Main loop ,主循环嘛,在这个循环里有个判断switch (getPeerState())
这个判断是判断我们当前服务器的Peer状态的,默认为LOOKING状态,所以我们先去看case LOOKING下面的内容,这里补充一下我们Peer状态分为四种, LOOKING, FOLLOWING, LEADING, OBSERVING;
LOOKING状态下zk做了什么什么事情?不要急,快到高潮了。。
在1191行附近,为什么说在1191行附近呢?这个是楼主的锅,因为楼主自己加了些注释。。。。。。接着看代码吧。有这这么一段代码

//通过策略模式来决定使用哪个选举算法来进行选举
//并且将投票结果赋值给当前的投票中
setCurrentVote(makeLEStrategy().lookForLeader());

vote投票的意思,兴奋起来了,好像找到投票的入口了,到底是不是呢?高潮来没来呢?继续。。
在setCurrentVote方法里传入了makeLEStrategy().lookForLeader()东西,这个东西是什么?占时还不知道,我们直接去setCurrentVote方法里面看看这个方法做了什么事情

 public synchronized void setCurrentVote(Vote v){
    
    
        currentVote = v;
    }
/**
* This is who I think the leader currently is.
* 这就是我认为目前的leader。
*/
 volatile private Vote currentVote;

这时候发现,setCurrentVote方法里面就一句话,currentVote = v 那么currentVote 这个是什么?在我们当前类(QuorumPeer)中定义了一个Vote 类型的变量,这个变量就是currentVote ,好吧,我们知道了,setCurrentVote方法就是个赋值的方法,把我们的投票赋值给currentVote 。
这时候我们就明白setCurrentVote方法里传入的makeLEStrategy().lookForLeader()就是个传投票信息进来
而我们的makeLEStrategy().lookForLeader()就是为了构造投票信息的
这个时候我们的方向就又明确了,我们去看看makeLEStrategy().lookForLeader()怎么来构造投票信息的。

这里用了策略模式来决定使用哪个选举算法,zk中有4中选举算法,那我们到底使用的是哪一种呢?
其实不用纠结用哪一种。。。因为3.4之后只有一种选举算法没有被废弃。。那就是FastLeaderElection其他几个都被废弃不用了。。。
那我们现在就去看看FastLeaderElection这个选举算法是怎么选举出leader的

ps:在这里切断一下,文章太长了会导致消化不了,下面一篇文章会详细讲解FastLeaderElection这个算法策略

这里分享一下看我看源码的一些方法
1、确定我们看源码要看哪些东西,有方向有目标
2、找到源码入口,如果自己找不到可以网上冲个浪
3、如果是参考别人的文章一定要自己下载了源码对照了看
4、初次看源码切记不要太深,点到为止
5、多看几遍
6、最重要的一点,不要怕源码,都是人写的

猜你喜欢

转载自blog.csdn.net/u010994966/article/details/93653277