JAVA复习笔记分布式篇:zookeeper

    前言:终于到分布式篇,前面把JAVA的一些核心知识复习了一遍,也是一个JAVA程序员最基本要掌握的知识点,接下来分布式的知识点算是互联网行业的JAVA程序员必备的技能;

    概念:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是谷歌的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等

    关键词分布式,一致性服务 

    讲得通俗一点,zookeeper的诞生就是为了解决分布式一致性的问题。

    什么是分布式一致性?

    分布式:分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

    一致性:指对每个节点一个数据的更新,整个集群都知道更新,并且是一致的 ;

     PS:通俗一点以前一台服务器支撑一个系统的模式升级为多台服务器(多个容器)去支撑一个系统,这样这个系统的并发、容灾等各方面的能力将得到大大提升(传统的ngix和集群也能做到),分布式和集群最大的一个区别是分布式的节点都是不同业务(比如账号     系统,订单系统,物流系统,商品系统各自部署在不同服务器上)这样做的好处是可以根据不同模块的特性合理地分配资源(比如账号系统访问量很大,需要部署3-5台机器,而物流系统的访问量较小,只需要部署1-2台服务器即可),一致性就是虽然账号系统部署       了3台服务器,对于client而言是透明的(订单系统调用账号系统的服务,不用知道调用的哪台服务器上的系统,  结果每次都能得到幂等),那么如何保证每次都能得到这个幂等结果?也就是zookeeper的要解决的问题

基本架构图

 

 角色

  领导者(leader): Leader作为整个ZooKeeper集群的主节点,负责响应所有对ZooKeeper状态变更的请求;

  追随者(follower):除了响应本服务器上的读请求外(exists,getData,getChildren等只读请求),follower还要处理leader的提议,并在leader提交该提议时在本地也进行提交

  观察者(observer):Observer和Follower比较相似,只有一些小区别:首先observer不参加选举也不响应提议;其次是observer不需要将事务持久化到磁盘,一旦observer被重启,需要从leader重新同步整个名字空间。

       整个zookeeper集群只有这3种角色,可以看到leader只有一个,所有对数据变更的请求都需要有leader去调度(leader会向所有节点发送原子广播,收到一半以上的节点都同意的消息后才真正提交这个事务),所以leader这个节点相当重要,那如何确立这个            learder       呢?

    答案是选举,zookeeper采用选举的方式确立leader(触发选举的场景分为2种,一种是服务器初始化启动时候,还有一种是服务器运行期间无法和Leader保持连接)

讲解选举过程前先需要了解几个名词

electionEpoch(时钟):每执行一次leader选举,electionEpoch就会自增,用来标记leader选举的轮次

peerEpoch:每次leader选举完成之后,都会选举出一个新的peerEpoch,用来标记事务请求所属的轮次

zxid:事务请求的唯一标记,由leader服务器负责进行分配。由2部分构成,高32位是上述的peerEpoch,低32位是请求的计数,从0开始。所以由zxid我们就可以知道该请求是哪个轮次的,并且是该轮次的第几个请求。

lastProcessedZxid:最后一次commit的事务请求的zxid

选举过程:

整个选举流程如上图

每个节点开始第一轮选举,逻辑时钟(当前投票轮数)+1,投票给自己节点(myid,zxid),广播给所有节点,并且开始接收其他节点发来的投票信息(第一轮的投票每个节点都会投给自己),每接收到一个其他节点的投票就先判断对方节点是否满足投票条件(只有PARTICIPANT类型的节点并且处于Looking才能参与选举),判断对方的Epoch和自己节点的逻辑时钟大小

1:如果对方的Epoch小与自己节点的逻辑时钟,那么就舍弃对方的这次投票(认为它是过期无效的),判断当前节点的状态节点是否满足投票条件,如果满足则继续接收其他节点的投票

2:如果对方的Epoch等于自己节点的逻辑时钟,则判断zxid大小

  2.1如果自己节点所投票的zxid小于对方的zxid,则需要更新自己的投票为对方的投票,并广播所有节点,把对方的票放到自己的投票箱

  2.2如果自己节点所投票的xid等于对方的zxid,则需要判断节点id大小(对方节点id较大,执行2.1的逻辑,反之执行2.3)

  2.3如果自己节点所投票的zxid大于对方的zxid,把对方的票放到自己的投票箱

3:如果对方的Epoch大于自己节点的逻辑时钟,更新自己的逻辑时钟为对方的Epoch, 清空自己的投票箱(认为之前的投票都是无效的)执行上面这一步(比较zxid大小)

4:每次投完票都会统计自己投票箱的情况,如果超过一半的票数投了相同的节点,就可以认为那个节点是leader节点。不过还需要再确认一遍(那么当前线程将被阻塞等待一段时间(这个时间在finalizeWait定义))从队列中继续接收,如果没有更优选票,投票就能结束(判断投票的节点id和当前服务器的Id一致,则认为是leader节点,如果节点类型是PARTICIPANT节点Following节点,反之Observing节点)

如果接收到的节点状不是Looking(Following,Observing,leading):

Observing:放弃该投票(Observer不参与投票)

Following或leading:对比当前的逻辑时钟与对方的逻辑时钟大小

 a) 如果逻辑时钟相同,将该数据保存到recvset,如果所接收服务器宣称自己是leader,那么将判断是不是有半数以上的服务器选举它,如果是则设置选举状态退出选举过程
  b) 否则这是一条与当前逻辑时钟不符合的消息,那么说明在另一个选举过程中已经有了选举结果,于是将该选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,如果可以也是保存逻辑时钟,设置选举状态,退出选举过程.

 

可以查看部分核心源码

public Vote lookForLeader() throws InterruptedException {
        try {
            this.self.jmxLeaderElectionBean = new LeaderElectionBean();
            MBeanRegistry.getInstance().register(this.self.jmxLeaderElectionBean, this.self.jmxLocalPeerBean);
        } catch (Exception var23) {
            LOG.warn("Failed to register with JMX", var23);
            this.self.jmxLeaderElectionBean = null;
        }

        if(this.self.start_fle == 0L) {
            this.self.start_fle = System.currentTimeMillis();
        }

        FastLeaderElection.Notification n;
        try {
            HashMap<Long, Vote> recvset = new HashMap();
            HashMap<Long, Vote> outofelection = new HashMap();
            int notTimeout = 200;
            synchronized(this) {
                ++this.logicalclock;
                this.updateProposal(this.getInitId(), this.getInitLastLoggedZxid(), this.getPeerEpoch());
            }

            LOG.info("New election. My id =  " + this.self.getId() + ", proposed zxid=0x" + Long.toHexString(this.proposedZxid));
            this.sendNotifications();

            while(this.self.getPeerState() == ServerState.LOOKING && !this.stop) {
                n = (FastLeaderElection.Notification)this.recvqueue.poll((long)notTimeout, TimeUnit.MILLISECONDS);
                if(n == null) {
                    if(this.manager.haveDelivered()) {
                        this.sendNotifications();
                    } else {
                        this.manager.connectAll();
                    }

                    int tmpTimeOut = notTimeout * 2;
                    notTimeout = tmpTimeOut < '\uea60'?tmpTimeOut:'\uea60';
                    LOG.info("Notification time out: " + notTimeout);
                } else if(!this.self.getVotingView().containsKey(Long.valueOf(n.sid))) {
                    LOG.warn("Ignoring notification from non-cluster member " + n.sid);
                } else {
                    Vote endVote;
                    Vote var6;
                    switch(FastLeaderElection.SyntheticClass_1.$SwitchMap$org$apache$zookeeper$server$quorum$QuorumPeer$ServerState[n.state.ordinal()]) {
                    case 1:
                        if(n.electionEpoch > this.logicalclock) {
                            this.logicalclock = n.electionEpoch;
                            recvset.clear();
                            if(this.totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, this.getInitId(), this.getInitLastLoggedZxid(), this.getPeerEpoch())) {
                                this.updateProposal(n.leader, n.zxid, n.peerEpoch);
                            } else {
                                this.updateProposal(this.getInitId(), this.getInitLastLoggedZxid(), this.getPeerEpoch());
                            }

                            this.sendNotifications();
                        } else {
                            if(n.electionEpoch < this.logicalclock) {
                                if(LOG.isDebugEnabled()) {
                                    LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long.toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(this.logicalclock));
                                }
                                break;
                            }

                            if(this.totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, this.proposedLeader, this.proposedZxid, this.proposedEpoch)) {
                                this.updateProposal(n.leader, n.zxid, n.peerEpoch);
                                this.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(Long.valueOf(n.sid), new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
                        if(!this.termPredicate(recvset, new Vote(this.proposedLeader, this.proposedZxid, this.logicalclock, this.proposedEpoch))) {
                            break;
                        }

                        while((n = (FastLeaderElection.Notification)this.recvqueue.poll(200L, TimeUnit.MILLISECONDS)) != null) {
                            if(this.totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, this.proposedLeader, this.proposedZxid, this.proposedEpoch)) {
                                this.recvqueue.put(n);
                                break;
                            }
                        }

                        if(n == null) {
                            this.self.setPeerState(this.proposedLeader == this.self.getId()?ServerState.LEADING:this.learningState());
                            endVote = new Vote(this.proposedLeader, this.proposedZxid, this.logicalclock, this.proposedEpoch);
                            this.leaveInstance(endVote);
                            var6 = endVote;
                            return var6;
                        }
                        break;

 

Leader与Follower同步数据(ZAB 原子广播)

    Fast Leader选举算法中提到的同步数据时使用的逻辑时钟,它的初始值是0,每次选举过程都会递增的,在leader正式上任之后做的第一件事情,就是根据当前保存的数据id值,设置最新的逻辑时钟值。
     随后,leader构建NEWLEADER封包,该封包的数据是当前最大数据的id,广播给所有的follower,也就是告知follower leader保存的数据id是多少,大家看看是不是需要同步。然后,leader根据follower数量给每个follower创建一个线程LearnerHandler,专门负责接收它们的同步数据请求.leader主线程开始阻塞在这里,等待其他follower的回应(也就是LearnerHandler线程的处理结果),同样的,只有在超过半数的follower已经同步数据完毕,这个过程才能结束,leader才能正式成为leader.

 leader所做的工作:

所以其实leader与follower同步数据的大部分操作都在LearnerHandler线程中处理的,接着看这一块.
leader接收到的来自某个follower封包一定是FOLLOWERINFO,该封包告知了该服务器保存的数据id.之后根据这个数据id与本机保存的数据进行比较:
1) 如果数据完全一致,则发送DIFF封包告知follower当前数据就是最新的了.
2) 判断这一阶段之内有没有已经被提交的提议值,如果有,那么:
    a) 如果有部分数据没有同步,那么会发送DIFF封包将有差异的数据同步过去.同时将follower没有的数据逐个发送COMMIT封包给follower要求记录下来.
    b) 如果follower数据id更大,那么会发送TRUNC封包告知截除多余数据.(一台leader数据没同步就宕掉了,选举之后恢复了,数据比现在leader更新)
3) 如果这一阶段内没有提交的提议值,直接发送SNAP封包将快照同步发送给follower.
4)消息完毕之后,发送UPTODATE封包告知follower当前数据就是最新的了,再次发送NEWLEADER封包宣称自己是leader,等待follower的响应.

follower做的工作:
(1)会尝试与leader建立连接,这里有一个机制,如果一定时间内没有连接上,就报错退出,重新回到选举状态.
(2)其次在发送FOLLOWERINFO封包,该封包中带上自己的最大数据id,也就是会告知leader本机保存的最大数据id.
(3)根据前面对LeaderHandler的分析,leader会根据不同的情况发送DIFF,UPTODATE,TRUNC,SNAP,依次进行处理就是了,此时follower跟leader的数据也就同步上了.
(4)由于leader端发送的最后一个封包是UPTODATE,因此在接收到这个封包之后follower结束同步数据过程,发送ACK封包回复leader.

以上过程中,任何情况出现的错误,服务器将自动将选举状态切换到LOOKING状态,重新开始进行选举.

 

zookeeper的机制

分布式锁:

用zookeeper实现分布式锁有2种

方式一:根据zookeeper命名的唯一性,每个想要竞争资源的节点,创建一个相同名字的临时节点(相同路径),只能有一个节点创建成功,创建成功的节点就获得了锁

方式二:根据zk的“临时顺序”节点 , 每个想要竞争资源的节点,在同一个节点下注册一个临时顺序节点,然后将所有临时节点按小从到大排序,如果自己注册的临时节点正好是最小的,表示获得了锁(zk能保证临时节点序号始终递增,所以如果后面有其它应用也注册了临时节点,序号肯定比获取锁的应用更大),其余节点watch排在前面的节点,只要监测到节点被删除了(锁释放之后就删除=该节点),马上重新竞争锁

 

posted @ 2018-01-29 20:09  等待九月  阅读(371)  评论(0编辑  收藏  举报