Zookeeper选举算法和流程

Zookeeper

1.zookeeper是什么?

zookeeper是一个为分布式应用提供服务的一个中间件。分布式中CAP的问题,他可以帮助我们实现CP,也就是一致性和分区容错性。

简单理解:
它类似文件系统,数据结构是节点类型的(一切皆节点),它的节点有点像操作系统的目录,但是它的节点可以直接存储数据。
应用场景:分布式锁,分布式配置中心,注册中心。

Zab协议: 所有来自Client的写请求,都要转发给Leader,有Leader根据请求发起一起Proposal(提议),然后其他的Follower发起ack(投票),当投票的数量过半时,
Leader会向所有的Server发送一个通知消息。然后其他Server收到通知,就会认为这次的请求通过,更新内存中的数据。其中Client所连接的那台还需要做出响应。

2.选举算法和流程是怎样的?

选择优秀的服务器当leader,优秀的定义:
1.谁的数据最新谁最优秀,也就是zxid大的优秀
2.如果zxid相同,就比较myid,myid大的优秀

0.自增选举轮次
Zookeeper规定了所有有效的投票的都必须在同一轮次中,所以在开启投票之前,每台服务器都会对logiclclock进行自增操作。

1.发起投票
每个服务器启动的时候都是LOOKING状态,这个时候会先给自己投票,比且把投票结果发送给其他服务器

2.接收投票
electionEpoch,logicalclock 前者是别人传递过来的,后者是自己。都表示选举轮次。
1.electionEpoch > logicalclock ,说明自己落后,则跟上 logicalclock=electionEpoch,然后放弃自己的投票,再使用初始化投票来PK,重新发送投票结果。
2.electionEpoch < logicalclock,说明别人轮次落后了,则不需要理会这个投票。
3.electionEpoch = logicalclock, 说明轮次一样,就进入PK

3.PK
就是根据上面的规则,选择优秀的服务器作为leader,如果别人优秀,就把自己这一票从投自己改为投别人。

4.统计
每次投票后,都会去判断一下是否有过半的机器投票给同一台机器,如果有,则认为那台机器是准leader

5.检查余票
选出准leader之后,还有一步判断,继续获取其他服务器的投票,再判断谁更优秀,如果新投出来的更优秀,就会重新投票。

6.改变状态
在检查余票期间是循环一直获取新的投票,直到没有再收到其他人投票,那么之前准leader就会把自己设置为leader。
其他服务器根据配置文件,设置自己为follower或者是observer
补充:运行的过程中,如果leader挂掉之后,follower由于同步数据到leader的时候会出错,此时follower就会把自己状态改成looking,之后就会重新选举。
下面附上领导者选举的部分源码
FastLeaderElection#lookForLeader()

case LOOKING:
    // If notification > current, replace and send messages out
    if (n.electionEpoch > logicalclock.get()) {
        // 如果别人的选举周期比自己大,则把自己的时钟设置为跟别人一样
        logicalclock.set(n.electionEpoch);
        // 放弃自己的之前的选票
        recvset.clear();
        // 比较谁数据比较新,就投给谁
        if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
            updateProposal(n.leader, n.zxid, n.peerEpoch);
        } else {
            updateProposal(getInitId(),
                    getInitLastLoggedZxid(),
                    getPeerEpoch());
        }
        // 重新发送
        sendNotifications();
    } 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;
    } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
            proposedLeader, proposedZxid, proposedEpoch)) {
        // 这里就是投票周期相等,并且别人服务器数据比较新,通过epoch和zxid和myid比较
        // 更新自己的投票信息
        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.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
    // 接收别人的选票以及自己的投票,判断能不能成为准leader(过半机制)
    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){
            // 这里循环去取再来看下还有没有新的投票过来,如果有,并且新的投出来这个服务器更加优秀就会把他放到recvqueue队列中,然后跳出循环,重新选举

            if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                    proposedLeader, proposedZxid, proposedEpoch)){
                recvqueue.put(n);
                break;
            }
        }

        /*
         * This predicate is true once we don't read any new
         * relevant message from the reception queue
         */
        if (n == null) {
            // 如果没有新的投票了,那就判断当前的投出来的sid是否等于自己,是就把自己设置成leader,否则就根据配置文件设置为follower或者observer
            self.setPeerState((proposedLeader == self.getId()) ?
                    ServerState.LEADING: learningState());

            Vote endVote = new Vote(proposedLeader,
                                    proposedZxid,
                                    logicalclock.get(),
                                    proposedEpoch);
            // 将最终的投票结果返回
            leaveInstance(endVote);
            return endVote;
        }
    }
    break;

服务器之间通信
QuorumCnxManager
每台服务器在启动的时候都会启动QuorumCnxManager,它的作用就是各个服务器之间选举的时候通信管理。

1.消息队列
public class QuorumCnxManager
Map<Long, SendWorker> senderWorkerMap; // 每台服务器对应的SendWordker sid:SendWorker

Map<Long, ArrayBlockingQueue> queueSendMap; // 需要发送给各个服务器的消息队列 sid:queue

Map<Long, ByteBuffer> lastMessageSent; // 发送给每台服务器最近的消息 sid:lastMessageSent

ArrayBlockingQueue recvQueue; // 本台服务器接收到的消息

2.建立连接

QuorumCnxManager启动的时候会创建一个ServerSocket连接,为了避免两台服务器重复创建TCP连接,Zookeeper做了一个判断,如果连过来的服务器的sid比自己小,
则断开连接,自己再主动向该服务器发起连接。一旦连接建立,就会根据其他服务器的sid来创建上面几个Map以及消息接收器recvQueue

3.消息发送
Zookeeper为每台远程服务器都分配一个单独的SendWorker,SendWorker是单独的一个线程。
SendWorker的中,是不断的从queueSendMap中针对当前服务器的消息发送队列,判断是否为空,为空则从lastMessageSent中取出最近一次发送过的消息来再次发送,
这样是为了保证,最后一次发送出去的消息确保对方能收到,同时接收方会处理好幂等性,避免重复消息发生的影响。

SendWork部分源码加注释

@Override
public void run() {
    threadCnt.incrementAndGet();
    try {
        /**
         * If there is nothing in the queue to send, then we
         * send the lastMessage to ensure that the last message
         * was received by the peer. The message could be dropped
         * in case self or the peer shutdown their connection
         * (and exit the thread) prior to reading/processing
         * the last message. Duplicate messages are handled correctly
         * by the peer.
         *
         * If the send queue is non-empty, then we have a recent
         * message than that stored in lastMessage. To avoid sending
         * stale message, we should send the message in the send queue.
         * 如果队列中没有要发送的消息,则发送lastMessage以确保对等方收到了最后一条消息。如果self或对等端在读取/处理最后一条消息之前关闭它们的连接(并退出线程),则可以删除该消息。对等方正确地处理重复消息。
         */
        ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
        if (bq == null || isSendQueueEmpty(bq)) {
           ByteBuffer b = lastMessageSent.get(sid);
           if (b != null) {
               LOG.debug("Attempting to send lastMessage to sid=" + sid);
               send(b);
           }
        }
    } catch (IOException e) {
        LOG.error("Failed to send last message. Shutting down thread.", e);
        this.finish();
    }
}

4.消息接收
Zookeeper为每个远程服务器都分配单独一个RecvWorker,RecvWorker是单独的一个线程,它会一直从TCP连接中读取消息,读取到的消息加入到recvQueue队列中。

RecvWorker源码

@Override
public void run() {
    threadCnt.incrementAndGet();
    try {
        while (running && !shutdown && sock != null) {
            /**
             * Reads the first int to determine the length of the
             * message
             */
            int length = din.readInt();
            if (length <= 0 || length > PACKETMAXSIZE) {
                throw new IOException(
                        "Received packet with invalid packet: "
                                + length);
            }
            /**
             * Allocates a new ByteBuffer to receive the message
             */
            byte[] msgArray = new byte[length];
            din.readFully(msgArray, 0, length);
            ByteBuffer message = ByteBuffer.wrap(msgArray);
            addToRecvQueue(new Message(message.duplicate(), sid));
        }
    } catch (Exception e) {
        LOG.warn("Connection broken for id " + sid + ", my id = "
                 + QuorumCnxManager.this.mySid + ", error = " , e);
    } finally {
        LOG.warn("Interrupting SendWorker");
        sw.finish();
        if (sock != null) {
            closeSocket(sock);
        }
    }
}

3.Zookeeper对节点的watch监听通知是永久的吗?

不是,我们使用原生Zookeeper客户端,绑定watch的时候,这个watch只被回调一次就会失效了,通过源码可以知道,Client这边是直接map.remove返回watch,然后再执行的。

4.集群一般配置多少台机器?

2n+1台。因为过半机制的影响,通过源码可以知道,过半机制的判断是要大于 n / 2的。所以比如4台和3台的效果其实是一样的,都是只能容忍挂掉一台。

5.集群支持动态添加机器吗?

不行。因为需要修改配置文件。Zookeeper的集群数量,是启动时根据配置文件配置的server数量决定的。这也是过半机制的这个数量来设计的。
所以需要添加机器的时候,是需要每一台机器的配置文件都需要修改,然后逐台重启的。

6.如果一台服务器挂掉,重启的时候数据是怎么同步的?

但是如果是之前在集群中的服务器挂掉时,重启就可以,启动时这台服务器也会自己投票给自己,但是这个时候已经有leader了,其他服务器会通知到它谁是leader,之后再同步数据。
ps:多个事务操作之后才会打一次快照。所以快照里面的数据不一定的最新的,但是打完快照之后的事务操作都记录在一个list里面。
在同步数据过程中,就是Follower启动的过程,这个时间段客户端是不可以访问的。
同步数据的方式:通过快照数据先同步一部分,比快照更新的数据,通过一个list(committedLog)来获取。
大概流程如下
Leader:
1.找到要同步的数据
2.发送给指定的follower
3.发送完毕之后发送UPTODATE指令
Follower:
1.接收leader发送过来要同步的数据
2.循环等待UPTODATE指令
3.收到UPTODATE指令表示数据同步完,break退出循环。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值