手把手带你撸zookeeper源码-zookeeper启动(二)

接上文 手把手带你撸zookeeper源码-从源码角度分析zookeeper启动时都做了什么?

先说点题外话,因为我想着把整个zookeeper源码分析作为系列文章来写,所以每一篇文章只会分析一部分源码,而不是长篇大论去粘贴源码,然后哪个方法进入哪个方法,然后一带而过。而是抓住主线,然后去分析主要方法,用我的理解,用通俗的语言通过书面的形式表达出来,希望对想学习zookeeper源码的你有帮助

先抛出个目标,本篇文章主要写一下zookeeper集群之间是如何建立连接的?

先贴一下上文最后的一段代码,有一部分代码没有讲解清除,需要再深入说一下,也对本篇文章还是比较重要的

 protected Election createElectionAlgorithm(int electionAlgorithm){
        Election le=null;
                
        //TODO: use a factory rather than a switch
        //0 1 2 已经废弃
        //electionAlgorithm = 3
        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:
            //zk节点网络通信的组件
            qcm = createCnxnManager();
            QuorumCnxManager.Listener listener = qcm.listener;
            if(listener != null){
                // 启动一个listener监听,用于监听其他机器发送过来的请求
                listener.start();
                le = new FastLeaderElection(this, qcm);
            } else {
                LOG.error("Null listener when initializing cnx manager");
            }
            break;
        default:
            assert false;
        }
        return le;
    }

这个方法主要是创建一个选举算法实例对象,就是FastLeaderElection, 在最后返回之前,我们需要先关注一下createCnxnManager()这个方法,我们看看它做了什么事,看下代码

public QuorumCnxManager(final long mySid,
                            Map<Long,QuorumPeer.QuorumServer> view,
                            QuorumAuthServer authServer,
                            QuorumAuthLearner authLearner,
                            int socketTimeout,
                            boolean listenOnAllIPs,
                            int quorumCnxnThreadsSize,
                            boolean quorumSaslAuthEnabled,
                            ConcurrentHashMap<Long, SendWorker> senderWorkerMap) {
        this.senderWorkerMap = senderWorkerMap;
        //创建一个recvQueue,返回响应的数据会放到这个队列里面,肯定有其他地方会取出这个队列里面的数据,然后进行处理的
        this.recvQueue = new ArrayBlockingQueue<Message>(RECV_CAPACITY);
        //发送队列的一个map, key: myid, value: 要发送的数据集合
        this.queueSendMap = new ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>>();
        // 最新发送出的数据集合
        this.lastMessageSent = new ConcurrentHashMap<Long, ByteBuffer>();
        String cnxToValue = System.getProperty("zookeeper.cnxTimeout");
        if(cnxToValue != null){
            this.cnxTO = Integer.parseInt(cnxToValue);
        }

        this.mySid = mySid;
        this.socketTimeout = socketTimeout;
        this.view = view;
        this.listenOnAllIPs = listenOnAllIPs;

        initializeAuth(mySid, authServer, authLearner, quorumCnxnThreadsSize,
                quorumSaslAuthEnabled);

        // Starts listener thread that waits for connection requests 
        // 这个比较关键
        listener = new Listener();
    }

在代码里面添加了一部分注释,在这个方法里面主要关注点在于最后的一个Listener,这个Listener是干什么的?首先Listerner是一个线程,那么肯定会在某一个地方会启动这个线程(上上面的代码中大家可以看到有一个listener.start()来启动线程),然后在线程中会创建一个ServerSocket,然后调用accept方法接受其他客户端的连接请求,具体代码我们等会再贴出来再详细看,这里我们可以先大概知道这个方法里面是用来干什么的就行,具体用的时候再进去具体分析,千万不要钻牛角尖,否则进去可能一头雾水出不来了。

然后回到上文中创建选举算法实例的方法里面,如下代码

 QuorumCnxManager.Listener listener = qcm.listener;
            if(listener != null){
                // 启动一个listener监听,用于监听其他机器发送过来的请求
                listener.start();
                le = new FastLeaderElection(this, qcm);
            } else {
                LOG.error("Null listener when initializing cnx manager");
            }

然后把listener给启动了,会调用Listener中的run方法,接下来就是去实例化FastLeaderElection对象了,然后调用starter方法

private void starter(QuorumPeer self, QuorumCnxManager manager) {
        this.self = self;
        proposedLeader = -1;
        proposedZxid = -1;
        //创建一个发送数据队列
        sendqueue = new LinkedBlockingQueue<ToSend>();
        // 接受响应通知 队列
        recvqueue = new LinkedBlockingQueue<Notification>();
        //实例化一个messager
        this.messenger = new Messenger(manager);
    }

大概就是创建两个队列,然后用来存放发送数据的队列,和接受响应通知的队列

关键点是在new Messager里面

Messenger(QuorumCnxManager manager) {
            //创建一个发送数据线程
            this.ws = new WorkerSender(manager);

            Thread t = new Thread(this.ws,
                    "WorkerSender[myid=" + self.getId() + "]");
            t.setDaemon(true);
            t.start();

            //接受数据线程
            this.wr = new WorkerReceiver(manager);
            t = new Thread(this.wr,
                    "WorkerReceiver[myid=" + self.getId() + "]");
            t.setDaemon(true);
            t.start();
        }

这里创建了两个线程,一个用来发送数据,一个用来接受数据,并同时启动了这两个线程

可以大概看一下

WorkerSender.run()

public void run() {
                while (!stop) {
                    try {
                        ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
                        if(m == null) continue;

                        process(m);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
                LOG.info("WorkerSender is down");
            }

从sendQueue里面获取数据,然后调用process去处理

WorkerReceiver.run()

这个方法代码稍微有点长,我不再粘贴出来了,大家可以自己看一下,其实就是从recvQueue里面获取数据,然后进行一系列的处理,我们接下来主要先关注一下,这两个队列里面在什么时候会放入数据,然后再回来看他们是怎么处理的

我们先继续往下看,看看还会有什么操作

发现调用完startLeaderElection方法之后,直接调用的就是super.start(), QuorumPeer也是一个线程,直接启动线程,然后找这个类中的run方法即可

刚才我们在startLeaderElection方法中看到先启动了Listener线程,我们先来分析一下Listener中的run方法

然后再分析QuorumPeer中的run方法

QuorumCnxnManager.Listener

        @Override
        public void run() {
            int numRetries = 0;
            InetSocketAddress addr;
            while((!shutdown) && (numRetries < 3)){
                    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;
                    }
                    LOG.info("My election bind port: " + addr.toString());
                    setName(view.get(QuorumCnxManager.this.mySid)
                            .electionAddr.toString());
                    ss.bind(addr);
                    while (!shutdown) {
                        Socket client = ss.accept();//bio
                        setSockOpts(client);
                        
                        if (quorumSaslAuthEnabled) {
                            receiveConnectionAsync(client);
                        } else {
                            receiveConnection(client);
                        }

                        numRetries = 0;
                    }
            }
        }

把注释和异常给删除掉了,其他代码都在shutdown默认为false, 另外shutdown变量是一个被volatile修饰的变量,对线程是可见的,我们可以手动去设置shutdown为true,那么此时就不会再监听其他客户端的连接了, numRetries<3,说明最终只会重试三次,超过三次之后直接异常不再重连。对volatile不熟悉的小伙伴可以自行百度

对java socket熟悉的小伙伴一眼就能看出while中创建了一个ServerSocket,然后绑定一个端口,通过accept阻塞住,等待客户端连接。(上篇末尾说会写两篇有关java socket和java nio的文章,对不熟悉的这方面的小伙伴做个知识点的扩充,这块在这周末去写吧。大家对这块不熟悉的也可以去百度一下,然后自己手动敲个demo先看一下,对这块有个大概的了解)

我们假设现在有一个客户端连接进来,接下来代码将接入到 receiveConnection中去接受连接请求的处理。再说补充一点,就是很多开源框架,大家可以看一下他们的类命名、方法命名、变量名,其实都是很有讲究的,很多时候我们光看名字都可以猜想出来它干了什么事的,大家要多锻炼"连蒙带猜"的能力

private void handleConnection(Socket sock, DataInputStream din)
            throws IOException {
        Long sid = null;
        
            sid = din.readLong();
            if (sid < 0) { // this is not a server id but a protocol version (see ZOOKEEPER-1633)
                sid = din.readLong();

                int num_remaining_bytes = din.readInt();
                if (num_remaining_bytes < 0 || num_remaining_bytes > maxBuffer) {
                    LOG.error("Unreasonable buffer length: {}", num_remaining_bytes);
                    closeSocket(sock);
                    return;
                }
                byte[] b = new byte[num_remaining_bytes];

                // remove the remainder of the message from din
                int num_read = din.read(b);
            }
            if (sid == QuorumPeer.OBSERVER_ID) {
                
                sid = observerCounter.getAndDecrement();
                LOG.info("Setting arbitrary identifier to observer: " + sid);
            }
        

        authServer.authenticate(sock, din);

        //If wins the challenge, then close the new connection.
        if (sid < this.mySid) {
            SendWorker sw = senderWorkerMap.get(sid);
            if (sw != null) {
                sw.finish();
            }

            LOG.debug("Create new connection to server: " + sid);
            closeSocket(sock);
            connectOne(sid);

        } else {
            SendWorker sw = new SendWorker(sock, sid);
            RecvWorker rw = new RecvWorker(sock, din, sid, sw);
            sw.setRecv(rw);

            SendWorker vsw = senderWorkerMap.get(sid);
            
            if(vsw != null)
                vsw.finish();
            
            senderWorkerMap.put(sid, sw);
            queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
            
            sw.start();
            rw.start();
            
            return;
        }
    }

我把不需要的代码删了一部分,分析一下这个方法,主要都干了什么事?

1、在接收到连接之后会读取创建连接的服务器的sid给读出来

2、接下来有个分支就是判断读取出来的sid和我自己的sid做比较,现在我们先考虑进入else的部分,先看看都做了什么事

 创建了两个线程,SenderWorker和RecvWorker,并把二者给关联起来,并启动,它们两个应该是负责发送和接受选举数据的

3、我们看一下sid < mySid的部分(这部分今天先说一下,后面还会提到)

我们先来简单看一下这张图,打个比方,zookeeper01启动,然后阻塞等待有人连接进来,然后zookeeper02也启动,然后也会等待其他人连接进来。之后呢,zookeeper01会遍历zoo.cfg里面所有的server.x(里面的服务器,然后循环建立连接)即,zookeeper01和zookeeper02建立连接,同样的zookeeper02也会和zookeeper01建立连接。

是不是感觉很奇怪,zookeeper01和zookeeper02其实只需要建立一个连接,然后进行Leader选举进行通信就可以了,而现在却相互建立连接,造成资源浪费

而sid < mySid就是为了处理这个问题的,zookeeper在集群中相互创建连接时,会判断当前哪个sid和我建立连接,如果是比我小的sid和我建立连接然后直接把连接进行关闭了,如果是比我大的sid,则允许建立连接,就是zookeeper只允许sid大的服务器向sid小的服务器发起连接,而小的sid不能向sid大的发起连接,发起之后会直接关闭掉


        if (sid < this.mySid) {
            
            SendWorker sw = senderWorkerMap.get(sid);
            if (sw != null) {
                sw.finish();
            }
            //关掉socket
            closeSocket(sock);
            connectOne(sid);
        } 

换个角度思考一下这个问题,其实我们完全在发起连接的时候去先判断一下sid的大小(通过server.x肯定是可以知道我要准备向哪台zookeeper发起连接的),然后再去创建连接,而不是说我先去建立连接,然后再判断,再关闭连接

 

另外至于zookeeper之间发送连接的代码,我们接下来再去深入分析,每篇文章不会太长,但是会尽可能的分析全面,不会追求长篇大论,只是用最通俗的话来讲解,一次一个点即可

 

觉得对你有帮助的小伙伴,请点个赞

 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值