zookeeper-一个简单又复杂的东西

zookeeper为什么说是一个简单又复杂的东西,复杂是指从理论上上来看,真的很复杂,很多人根本看不懂,为什么又说简单呢?简单是指从代码层面上来说,实现理论并不复杂,反而异常的清晰。下面说下复杂的东西:paxos。

1:paxos

paxos统治了现在基本上所有一致性算法的理论基础,chubby和zookeeper都是以paxos为理论的一致性算法,但是由于paxos不容易实现和即使实现在使用当中也会出现一些问题,基本上没有中间件完全的用paxos,基本上都是在理论基础上的变种。下面简单的说一下paxos:

 

paxos有三种角色:Proposer,Acceptor,Learner。由于Learner并不参加选举,这里就不多说了。Proposer负责提出议案,Acceptor负责对提案进行投票。算法将围绕下面的图片进行聊:

从上面的图也可以看出来:

  • 阶段 1
    • a) proposer向网络内超过半数的acceptor发送prepare消息
    • b) acceptor正常情况下回复promise消息
  • 阶段 2
    • a) 在有足够多acceptor回复promise消息时,proposer发送accept消息
    • b) 正常情况下acceptor回复accepted消息

 下面说第一种情况:

 

一阶段(prepare):

A1在1发出prepare(maxn=1)发出提议,A3在接收到提议之后,自己的maxn=null,把自己的maxn=1,在2回复A1 ok,promise(maxn=1);其他的A2,A4,A5也和A3一样的回复A1 ok,promise(maxn=1);A1收到过半的promise回复,开启二阶段的accept过程。

二阶段(accept):

A1在3发出accept(maxn=1,value=10%),A3收到accept之后,比较自身的maxn=1,等于A1发出的accpet的maxn=1,在4回复A1ok,accepted(maxn=1,value=10%),其他的A2,A4,A5也和A3一样的回复A1 ok,accepted(maxn=1,value=10%),A1收到过半的accepted回复之后,开启之后的leaner过程。

 

这是最顺利的过程,现实往往不会如此,会存在异常情况,下面分析第二种情况:

一阶段(prepare):

A1在1发出prepare(maxn=1)发出提议,A3在接收到提议之后,自己的maxn=null,把自己的maxn=1,在2回复A1 ok,promise(maxn=1);其他的A2,A4,A5也和A3一样的回复A1 ok,promise(maxn=1);A1收到过半的promise回复,开启二阶段的accept过程。

一阶段和第一种情况的没有什么区别,主要在二阶段上。

二阶段(accept):

A5在5发出prepare(maxn=2)发出提议,A3在接受到提议之后,自己的maxn=1<2,把自己的maxn=2,在6回复A5 ok,promise(maxn=2,value=10%),并带上次accepted的maxn=1,假设A1,A2,A4也和A3一样,回复A5 ok,promise(maxn=2,value=10%),并带上次accepted的maxn=1,这样A5得到半数的promise回复,A5发出accepted(2,10%),同时A1也收到了半数的accepted请求,这只是理想情况,最终的结果就是A1把accept(maxn=1,value=10%)和A5把(maxn=2,value=10%)都收到了过半回复,value=10%都被传播了。

这种情况也很顺利,并没有进行二次选举,只是10%被二次accepte,这也是理论的问题所在,不可控。后面zab就有了leader的概念。下面说下第三种情况:

一阶段(prepare):

A1在1发出prepare(maxn=1)发出提议,A3在接收到提议之后,自己的maxn=null,把自己的maxn=1,在2回复A1 ok,promise(maxn=1);其他的A2,A4,A5也和A3一样的回复A1 ok,promise(maxn=1);A1收到过半的promise回复,开启二阶段的accept过程。

一阶段和第二种情况的没有什么区别,主要在二阶段上。

二阶段(accept):

A5在5发出prepare(maxn=2)发出提议,A3在接受到提议之后,自己的maxn=1<2,把自己的maxn=2,在6回复A5 ok,promise(maxn=2,value=10%),并带上次accepted的maxn=1,假设现在A2和A4都和A3一样接受了A5发出的prepare(maxn=2)的提议,但是这个时候A2,A4,A5这个时候和A3不一样,这个时候A2,A4,A5,还没有收到A1的accept(maxn=1,value=10%),在7的时候A1发出了accept(maxn=1,value=10%),但是这个时候A2,A4,A5,的maxn=2,所以A1被A2,A4,A5在8的reject拒绝,不得不进行下次的promise(3),而A5得到A2,A4的promise(maxn=2,value=null),A3的promise(maxn=2,value=10%),得到了多数的promise回复,挑选最大的maxn的value,这里选择value=10%,也可能不是,所以进行accept(maxn=2,value=10%或者value=20%),然后又回到了之前,所以这种就会循环,也就是大家所熟知的活锁。其他的情况可以以此类推,不会有太大变化,很类似,不再说其他情况。

 说到这里paxos就结束了,我自己都感觉复杂,对现实情况实现起来更是没有太大的头绪,这也是这个算法的通病,basic paxos难以理解,难以实现。线面到正题了,zookeeper的zab,在paxos理论上变种,单纯的理论意义如果不能实现,那么就没有太大的意义。

2:zookeeper:zab协议

zab协议分为三个部分:崩溃恢复模式,数据同步模式,消息广播模式,也有人把崩溃恢复和数据同步列为一起(一阶段),消息广播模式(二阶段),不影响大家理解。

2.1:崩溃恢复模式

1:初始化投票,每个人都把选票投给自己,(1,1)代表server1把选票投给server1(自己),(2,2)代表server2把选票投给server2(自己),(3,3)代表server1把选票投给server3(自己)

2:发送初始化选票,server1分别发送给server2(1,1)和server3(1,1),server2发送给server1(2,2)和server3(2,2),server3发送给server1(3,3),server2(3,3)

3:接受外部投票,server2和server3在这里接受server1的投票,server1和server3接受server2的投票,server1和server2接受server3的投票。

4:更新选票,更新选票的原则是闲判断全局的时钟(epoch),如果epoch相等,再判断zx_id,事务id的大小,如果zx_id一样大,就判断server_id(my_id),服务器的编码,每个服务器都有一个id,因为是一开始启动服务器,所以只需要比较server_id(my_id)就行,由于server1的my_id小于server2的my_id,由于server2的my_id小于server3的my_id,server1的接到server2的(2,2),变更自己的投票变成(1,2),(2,2),接到server3的投票变成(1,3),(2,2),(3,3),server2接到server1的投票,不进行变更,此时投票(1,1),(2,2),接到server3的投票变成(1,1),(2,3),(3,3),server3的投票接到server1和server2的没有变化,此时(1,1),(2,2),(3,3)。

server1(1,3),(2,2),(3,3)

server2(1,1),(2,3),(3,3)

server3(1,1),(2,2),(3,3)

由于server1和server2的投票变更了,需要把变更的投票投出去,所以server1要吧(1,2)和(1,3)投票投出去,我不列举(1,2)了,只列举(1,3),server1投出去(1,3),server2投出去(2,3),server3没变更投票,无需继续投

5:发送变更的选票,server1发送给server2和server3(1,3),server2发送给server1和server3(2,3),server3不需要发送投票。

5:接着更新选票,server1投给server2和server3都不需要变更投票,只需要记录新的投票,server2投给server1的也不需要变更投票,只需要更新记录,server1接到server2(2,3)的投票,进行更新(1,3)(2,3)(3,3),server2接到server1的投票,更新投票(1,3)(2,3)(3,3),server3接到server1和server2的投票,进行更新(1,3)(2,3)(3,3)

server1(1,3)(2,3)(3,3)

server2(1,3)(2,3)(3,3)

server3(1,3)(2,3)(3,3)

都没有进行变更投票,至此结束了,最终的结果:

 从上面可以看出server3获得了大多数机器的统一,成为了leader。选举的代码类是FastLeaderElection,代码如下,感兴趣的自己看下,我就不分析了,和上面基本完全一样:

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 = System.currentTimeMillis();
        }
        try {
            HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();

            HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();

            int notTimeout = finalizeWait;

            synchronized(this){
                logicalclock++;
                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
             */

            while ((self.getPeerState() == ServerState.LOOKING) &&
                    (!stop)){
                /*
                 * Remove next notification from queue, times out after 2 times
                 * the termination time
                 */
                Notification n = recvqueue.poll(notTimeout,
                        TimeUnit.MILLISECONDS);

                /*
                 * Sends more notifications if haven't received enough.
                 * Otherwise processes new notification.
                 */
                if(n == null){
                    if(manager.haveDelivered()){
                        sendNotifications();
                    } else {
                        manager.connectAll();
                    }

                    /*
                     * Exponential backoff
                     */
                    int tmpTimeOut = notTimeout*2;
                    notTimeout = (tmpTimeOut < maxNotificationInterval?
                            tmpTimeOut : maxNotificationInterval);
                    LOG.info("Notification time out: " + notTimeout);
                }
                else if(self.getVotingView().containsKey(n.sid)) {
                    /*
                     * Only proceed if the vote comes from a replica in the
                     * voting view.
                     */
                    switch (n.state) {
                    case LOOKING:
                        // If notification > current, replace and send messages out
                        if (n.electionEpoch > logicalclock) {
                            logicalclock = 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) {
                            if(LOG.isDebugEnabled()){
                                LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
                                        + Long.toHexString(n.electionEpoch)
                                        + ", logicalclock=0x" + Long.toHexString(logicalclock));
                            }
                            break;
                        } 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.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

                        if (termPredicate(recvset,
                                new Vote(proposedLeader, proposedZxid,
                                        logicalclock, proposedEpoch))) {

                            // Verify if there is any change in the proposed leader
                            while((n = recvqueue.poll(finalizeWait,
                                    TimeUnit.MILLISECONDS)) != null){
                                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) {
                                self.setPeerState((proposedLeader == self.getId()) ?
                                        ServerState.LEADING: learningState());

                                Vote endVote = new Vote(proposedLeader,
                                                        proposedZxid,
                                                        logicalclock,
                                                        proposedEpoch);
                                leaveInstance(endVote);
                                return endVote;
                            }
                        }
                        break;
                    case OBSERVING:
                        LOG.debug("Notification from observer: " + n.sid);
                        break;
                    case FOLLOWING:
                    case LEADING:
                        /*
                         * Consider all notifications from the same epoch
                         * together.
                         */
                        if(n.electionEpoch == logicalclock){
                            recvset.put(n.sid, new Vote(n.leader,
                                                          n.zxid,
                                                          n.electionEpoch,
                                                          n.peerEpoch));
                           
                            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);
                                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));
           
                        if(ooePredicate(outofelection, outofelection, n)) {
                            synchronized(this){
                                logicalclock = n.electionEpoch;
                                self.setPeerState((n.leader == self.getId()) ?
                                        ServerState.LEADING: learningState());
                            }
                            Vote endVote = new Vote(n.leader,
                                                    n.zxid,
                                                    n.electionEpoch,
                                                    n.peerEpoch);
                            leaveInstance(endVote);
                            return endVote;
                        }
                        break;
                    default:
                        LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)",
                                n.state, n.sid);
                        break;
                    }
                } else {
                    LOG.warn("Ignoring notification from non-cluster member " + 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());
        }
    }

2.2:数据同步模式

zookeeper缓存了最近的500个请求,为了方便数据同步,所以数据同步也是以此为基础的,minCommintedLog和maxCommintedLog,代表缓存的最大值和最小值,以此区间判断怎么同步,很多主流的中间件都有这个东西。

1:followerpeerLastZxid 介于 maxCommittedLogminCommittedLog 两者之间

       1.1:followerpeerLastZxid 等于 leaderpeerLastZxidfollowerleader 数据一致,采用 DIFF 方式同步,

       也   即是无需同步。

       1.2:followerpeerLastZxid 小于 leaderpeerLastZxidpeerLastZxid在leader节点存在,采用 DIFF 方式同步

        leader    0x500000001, 0x500000002, 0x500000003, 0x500000004, 0x500000005,0x600000001

       follower peerLastZxid为0x500000003

       只需要把0x500000004和0x500000005,0x600000001发送给follower

      1.3:followerpeerLastZxid 小于 leaderpeerLastZxidpeerLastZxid在leader节点不存在。 

      采用 TRUNC+DIFF(先回滚再差异化同步)。

      leader    0x500000001, 0x500000002, 0x500000003, 0x500000004, 0x500000005,0x600000001

     follower peerLastZxid为0x500000006

     follower把0x500000006进行回滚,然后把0x600000001发送给follower

2:followerpeerLastZxid 大于 leadermaxCommittedLog,则告知 follower 回滚至 maxCommittedLog

该场景 可以认为是 TRUNC+DIFF 的简化模式

3:followerpeerLastZxid 小于 leaderminCommittedLog 或者 leader 节点上不存在提案缓存队列时,

将采用 SNAP 全量同步方式。 该模式下 leader 首先会向 follower 发送 SNAP 报文,随后从内存数据库中获取全量数据序

列化传输给 followerfollower 在接收全量数据后会进行反序列化加载到内存数据库中。

2.3:消息广播模式

 

     

1)客户端发起一个写操作请求。

2)Leader 服务器将客户端的请求转化为事务 Proposal 提案,同时为每个 Proposal 分配一个全局的ID,即zxid。

3)Leader 服务器为每个 Follower 服务器分配一个单独的队列,然后将需要广播的 Proposal 依次放到队列中取,并且根据 FIFO 策略进行消息发送。

4)Follower 接收到 Proposal 后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 Ack 响应消息。

5)Leader 接收到超过半数以上 Follower 的 Ack 响应消息后,即认为消息发送成功,可以发送 commit 消息。

6)Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交。Follower 接收到 commit 消息后,会将上一条事务提交。

在这里我是要贴一段足够经验的代码:

public void run() {
        try {
            int logCount = 0;

            // we do this in an attempt to ensure that not all of the servers
            // in the ensemble take a snapshot at the same time
            setRandRoll(r.nextInt(snapCount/2));
            while (true) {
                Request si = null;
                if (toFlush.isEmpty()) {
                    si = queuedRequests.take();
                } else {
                    si = queuedRequests.poll();
                    if (si == null) {
                        flush(toFlush);
                        continue;
                    }
                }
                if (si == requestOfDeath) {
                    break;
                }
                if (si != null) {
                    // track the number of records written to the log
                    if (zks.getZKDatabase().append(si)) {
                        logCount++;
                        if (logCount > (snapCount / 2 + randRoll)) {
                            randRoll = r.nextInt(snapCount/2);
                            // roll the log
                            zks.getZKDatabase().rollLog();
                            // take a snapshot
                            if (snapInProcess != null && snapInProcess.isAlive()) {
                                LOG.warn("Too busy to snap, skipping");
                            } else {
                                snapInProcess = new ZooKeeperThread("Snapshot Thread") {
                                        public void run() {
                                            try {
                                                zks.takeSnapshot();
                                            } catch(Exception e) {
                                                LOG.warn("Unexpected exception", e);
                                            }
                                        }
                                    };
                                snapInProcess.start();
                            }
                            logCount = 0;
                        }
                    } else if (toFlush.isEmpty()) {
                        // optimization for read heavy workloads
                        // iff this is a read, and there are no pending
                        // flushes (writes), then just pass this to the next
                        // processor
                        if (nextProcessor != null) {
                            nextProcessor.processRequest(si);
                            if (nextProcessor instanceof Flushable) {
                                ((Flushable)nextProcessor).flush();
                            }
                        }
                        continue;
                    }
                    toFlush.add(si);
                    if (toFlush.size() > 1000) {
                        flush(toFlush);
                    }
                }
            }
        } catch (Throwable t) {
            handleException(this.getName(), t);
            running = false;
        }
        LOG.info("SyncRequestProcessor exited!");
    }

这段代码说的什么,说的是如果我们提交一个写请求,如果此时系统很空闲,那么我们进行提交写操作,如果此时系统很频繁的工作,就积攒写请求到1000个,进行处理,以前我认为的实时写操作,写完一个执行完,把结果返回用户,但是实际上的都是异步的,我能想到网络I/O上的异步,却没想到这么执行写操作,开拓了眼界。值得吐槽的地方是:zk对文件的操作有点low,把数据保存的时候序列化一下就算完了,所有的数据加载到内存里,没有所谓的索引常驻内存一说,就是一个大字典,所以zk吃内存,这个东西和redis有的比,真的很像,特别像,只不过redis还有一些出色的数据结构。

这篇只说了怎么选举,怎么保证一致性,对于zk的一些基本知识:

PERSISTENT                持久化节点

PERSISTENT_SEQUENTIAL     顺序自动编号持久化节点,这种节点会根据当前已存在的节点数自动加 1

EPHEMERAL                 临时节点, 客户端session超时这类节点就会被自动删除

EPHEMERAL_SEQUENTIAL      临时自动编号节点

这些东西我没有说,还有leader,follower,observer这些概念没有说,zookeeper为什么使用EPHEMERAL节点做分布式锁,没用用过的还是要看下这些内容,我用过zk,但是大型的集群也没使用过,可能有地方理解有误差。这篇真的很难写,很多东西我都是一点点梳理的,太耗时间了,本来想再写一片redis的,时间不够了。下个周写下redis吧。

3:分布式一致性(分布式事务(2pc))和 共识性算法(Zookeeper) 关系

     分布式一致性(consistency)和 共识性算法(consensus)在概念上的确是二个东西,却有着千丝万缕的关系,主要分析下他们之间的联系和区别。

3.1:2PC分布式事务

 

这里只说下2pc,不说3pc了,2pc大体分为2个过程,3个步骤

1:preCommit(预提交)

    1.1:事务询问

     协调者向所有参与者发送事务询问

    1.2:执行事务

    参与者执行事务,事务成功,写入redo log 和 undo log,并不进行commint操作,返回协调者事务成功,如果事务失败,

    返回给协调者事务失败

2:doCommit

    2.1:如果所有参与者返回成功,进行commit,如果有任何一个返回失败,进行rollback操作。

如果协调者挂了怎么办?一直阻塞,一直不可用,能不能再选出来一个?所有的参与者都得回复完才能提交?如果网络出问题了怎么办?异地机房网络波动很可能发生,怎么办?

3.2:共识性算法zab

上面我们提到的问题如果碰到,都是一个灾难性的问题,这里称zookeeper为公共性算法,而不是一致性算法,是有原因的。

协调者:leader相当于2pc的协调者

参与者:follower相当于2pc的参与者

为了解决2pc协调者贮机之后,整个系统不可用的状态,所以让大家重新选出一个leader,这就需要超过半数参与者达成共识

这就是选 举的意义所在,之后就是进行2pc了,

1:preCommit(预提交)

    1.1:事务询问

     leader向所有follower发送事务询问

    2.1:执行事务

    follower执行事务,事务成功,写入txn log,并不进行commint操作,返回leader事务成功,如果事务失败,

    返回给leader事务失败

2:doCommit

    2.1:如果参与者中的超过一半返回成功,进行commit,否则就不会进行commit,类似rollback操作。

 

    为什么是超过一半?同城多机房的时候,不会出现脑裂的问题,特别是异地多机房的应用,请尽量不要使用zk,

    可能集群因为网络延迟,大多数时间都在选举和数据复制。

   

   总结一下,zab为什么能够实现一致性,这里指最终一致性,可以说zab是一个二阶段2pc的协议,但也可以说不是,

   不同的人有不同的理解,我认为zab是为了2pc为了实现可用性的进阶版,解决了2pc在理论上存在的致命问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值