You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

加餐 | ZAB协议主节点崩溃了怎么办

你好,我是韩健。

咱们都知道系统在运行中不可避免会出现各种各样的问题比如进程崩溃了、服务器死机了这些问题会导致很严重的后果让系统没办法运行。学完了15讲后你应该还记得在ZAB中写请求是必须在主节点上处理的而且提案的广播和提交也是由主节点来完成的。既然主节点那么重要如果它突然崩溃宕机了该怎么办呢

答案是选举出新的领导者(也就是新的主节点)。

在我看来领导者选举关乎着节点故障容错能力和集群可用性是ZAB协议非常核心的设计之一。你想象一下如果没有领导者选举主节点故障了整个集群都无法写入了这将是极其严重的灾难性故障。

而对你来说理解领导者选举也就是快速领导者选举Fast Leader Election能更加深刻地理解ZAB协议并在日常工作中游刃有余地处理集群的可用性问题。比如如果写请求持续失败可以先排查下集群的节点状态。

既然领导者选举这么重要那么ZAB是如何选举领导者的呢带着这个问题我们进入今天的学习。

ZAB如何选举领导者

既然要选举领导者那就涉及成员身份变更那么在ZAB中支持哪些成员身份呢

有哪些成员身份?

ZAB支持3种成员身份领导者、跟随者、观察者

  • 领导者Leader 作为主Primary节点在同一时间集群只会有一个领导者。需要你注意的是所有的写请求都必须在领导者节点上执行。

  • 跟随者Follower作为备份Backup节点 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。需要你注意的是,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。

  • 观察者Observer作为备份Backup节点类似跟随者但是没有投票权也就是说观察者不参与领导者选举和提案提交的投票。你可以对比着Paxos中的学习者来理解。

需要你注意的是虽然ZAB支持3种成员身份但是它定义了4种成员状态。

  • LOOKING选举状态该状态下的节点认为当前集群中没有领导者会发起领导者选举。
  • FOLLOWING :跟随者状态,意味着当前节点是跟随者。
  • LEADING :领导者状态,意味着当前节点是领导者。
  • OBSERVING 观察者状态,意味着当前节点是观察者。

为什么多了一种成员状态呢这是因为ZAB支持领导者选举在选举过程中涉及了一个过渡状态也就是选举状态

现在你已经了解了成员身份那么在ZAB中是如何变更成员身份来选举领导者呢接下来我们就来看一下领导者的具体选举过程。

如何选举?

为了帮你更好地理解ZAB的领导者选举我举个例子演示一下为了演示方便和更容易理解我们聚焦最核心的领导者PK假设投票信息的格式是<proposedLeader, proposedEpoch, proposedLastZxidnode>,其中:

  • proposedLeader节点提议的领导者的集群ID也就是在集群配置比如myid配置文件时指定的ID。
  • proposedEpoch节点提议的领导者的任期编号。
  • proposedLastZxid节点提议的领导者的事务标识符最大值也就是最新提案的事务标识符
  • node投票的节点比如节点B。

假设一个ZooKeeper集群由节点A、B、C组成其中节点A是领导者节点B、C是跟随者为了方便演示假设epoch分别是1和1lastZxid分别是101和102集群ID分别为2和3。那么如果节点A宕机了会如何选举呢

首先当跟随者检测到连接领导者节点的读操作等待超时了跟随者会变更节点状态将自己的节点状态变更成LOOKING然后发起领导者选举为了演示方便我们假设这时节点B、C都已经检测到了读操作超时

接着每个节点会创建一张选票这张选票是投给自己的也就是说节点B、C都“自告奋勇”推荐自己为领导者并创建选票<2, 1, 101, B>和<3, 1, 102, C>然后各自将选票发送给集群中所有节点也就是说B发送给B、CC也发送给B、C。

一般而言节点会先接收到自己发送给自己的选票因为不需要跨节点通讯传输更快也就是说B会先收到来自B的选票C会先收到来自C的选票

需要你注意的是集群的各节点收到选票后为了选举出数据最完整的节点对于每一张接收到选票节点都需要进行领导者PK也就将选票提议的领导者和自己提议的领导者进行比较找出更适合作为领导者的节点约定的规则如下

  • 优先检查任期编号Epoch任期编号大的节点作为领导者
  • 如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
  • 如果事务标识符的最大值相同比较集群ID集群ID大的节点作为领导者。

如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。

当节点B、C接收到的选票后因为选票提议的领导者与自己提议的领导者相同所以领导者PK的结果是不需要调整选票信息那么节点B、C正常接收和保存选票就可以了。

接着节点B、C分别接收到来自对方的选票比如B接收到来自C的选票C接收到来自B的选票

对于C而言它提议的领导者是C而选票<2, 1, 101, B>提议的领导者是B因为节点C的任期编号与节点B相同但节点C的事务标识符的最大值比节点B的大那么按照约定的规则相比节点B节点C更适合作为领导者也就是说节点C不需要调整选票信息正常接收和保存选票就可以了。

但对于对于节点B而言它提议的领导者是B选票<3, 1, 102, C>提议的领导者是C因为节点C的任期编号与节点B相同但节点C的事务标识符的最大值比节点B的大那么按照约定的规则相比节点B节点C应该作为领导者所以节点B除了接收和保存选票信息还会更新自己的选票为<3, 1, 102, B>也就是推荐C作为领导者并将选票重新发送给节点B、C

接着当节点B、C接收到来自节点B新的选票时因为这张选票<3, 1, 102, B>提议的领导者与他们提议的领导者是一样的都是节点C所以他们正常接收和存储这张选票就可以。

最后因为此时节点B、C提议的领导者节点C赢得大多数选票了2张选票那么节点B、C将根据投票结果变更节点状态并退出选举。比如因为当选的领导者是节点C那么节点B将变更状态为FOLLOWING并退出选举而节点C将变更状态为LEADING并退出选举。

你看这样我们就选举出新的领导者节点C这个选举的过程很容易理解我在这里只是假设了一种选举的情况还会存在节点间事务标识符相同、节点在广播投票信息前接收到了其他节点的投票等情况这里你可以思考一下课下自己动手操作一下。

为了帮你在线下更好的阅读代码自我学习我想补充一下逻辑时钟logicclock也就是选举的轮次会影响选票的有效性具体来说逻辑时钟大的节点不会接收来自值小的节点的投票信息。比如节点A、B的逻辑时钟分别为1和2那么节点B将拒绝接收来自节点A的投票信息。

在这里,我想强调的是,领导者选举的目标,是从大多数节点中选举出数据最完整的节点,也就是大多数节点中,事务标识符值最大的节点。**另外ZAB本质上是通过“见贤思齐相互推荐”的方式来选举领导者的。**也就说根据领导者PK节点会重新推荐更合适的领导者最终选举出了大多数节点中数据最完整的节点。

当然了文字和代码是2种不同的表达一些细节仅仅通过文字是无法表达出来的所以为了帮你更通透地理解领导者选举的实现接下来我将以最新稳定版的ZooKeeper为例也就是3.6.0),具体说一说代码的实现。

ZooKeeper是如何实现的

首先我们来看看在ZooKeeper中是如何实现成员身份的

在ZooKeeper中成员状态是在QuorumPeer.java中实现的为枚举型变量就像下面的样子。

public enum ServerState {
    LOOKING,
    FOLLOWING,
    LEADING,
    OBSERVING
}

其实在ZooKeeper中没有直接定义成员身份而是用了对应的成员状态来表示比如处于FOLLOWING状态的节点为跟随者。

在这里我想补充一点如果你想研究相关成员的功能和实现那么你可以把对应的成员状态作为切入点来研究比如你想研究领导者的功能实现那么你可以在代码中搜索LEADING关键字然后研究相应的上下文逻辑就能得到自己想要的答案了。

如果跟随者将自己的状态从跟随者状态变更为选举状态这就表示跟随者在发起领导者选举那么在ZooKeeper中领导者选举是如何实现的呢

如何实现选举?

领导者选举是在FastLeaderElection.lookForLeader()中实现的。我来讲一下核心的流程,大概的流程,就像下面的样子。

为帮助你更好的理解这个流程,我们来一起走读下核心代码,加深下印象。

1.在集群稳定运行时处于跟随者状态的节点在Follower.followLeader()函数中,周期性地读数据包和处理数据包:

QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
    // 读取数据包
    readPacket(qp);
    // 处理数据包
    processPacket(qp);
}

2.当跟随者检测到连接到领导者的读操作超时了比如领导者节点故障了这时会抛出异常Exception跳出上面的读取数据包和处理数据包的循环并最终跟随者将节点状态变更为选举状态。

public void run() {
    case FOLLOWING:
        ......
        finally {
            // 关闭跟随者节点
            follower.shutdown();
            setFollower(null);
            // 设置状态为选举状态
            updateServerState();
        }
        break;
    ......
}

3.当节点处于选举状态时将调用makeLEStrategy().lookForLeader()函数实际对应的函数为FastLeaderElection.lookForLeader()),发起领导者选举。

setCurrentVote(makeLEStrategy().lookForLeader());

4.在FastLeaderElection.lookForLeader()函数中节点需要对逻辑时钟也就是选举的轮次的值执行加1操作表示我们开启一轮的领导者选举然后创建投票提案默认推荐自己为领导者并通知所有节点

synchronized (this) {
  // 对逻辑时钟的值执行加一操作
  logicalclock.incrementAndGet();
  // 创建投票提案,并默认推荐自己为领导者
  updateProposal(getInitId(), getInitLastLoggedZxid(),    
                 getPeerEpoch());
}
// 广播投票信息给所有节点
sendNotifications();

5.当节点处于选举状态时,会周期性地从队列中读取接收到的投票信息,直到选举成功。

while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
   // 从队列中读取接收到的投票信息             
   Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
   ......
}

6.当接收到新的投票信息时节点会进行领导者PK来判断谁更适合当领导者如果投票信息中提议的节点比自己提议的节点更适合当领导者更新投票信息推荐投票信息中提议的节点作为领导者并广播给所有节点

else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
  // 投票信息中提议的节点比自己提议的节点更适合作为领导者,更新投票信息,并推荐投票信息中提议的节点
  updateProposal(n.leader, n.zxid, n.peerEpoch);
  // 将新的投票信息广播给所有节点
  sendNotifications();
}

7.如果自己提议的领导者赢得大多数选票则执行步骤8变更节点状态退出选举如果自己提议的领导者仍未赢得大多数选票则执行步骤5继续从接收队列中读取新的投票信息。

8.最后,当节点提议的领导者赢得大多数选票时,根据投票结果,判断当前节点的状态,领导者或跟随者,并变更节点状态,退出选举。

if (voteSet.hasAllQuorums()) {
  ......
    // 根据投票结果,判断并设置节点状态
    setPeerState(proposedLeader, voteSet);
    // 退出领导者选举
    Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
    leaveInstance(endVote);
    return endVote;
  ......
}

需要你注意的是,在这里,我们只是演示一种选举情况,更多的情况,比如接收到来自逻辑时钟比当前节点小的节点的投票信息,再比如接收到来自领导者的投票信息,你可以在课下自己研究一下,遇到问题时,欢迎留言,咱们一起讨论。

内容小结

本节课我主要带你了解了ZAB是如何选举领导者的以及在ZooKeeper中是如何实现的。我希望你明确这样几个重点。

1.领导者选举的目标,是选举出大多数节点中数据最完整的节点,也就是大多数节点中事务标识符值最大的节点。

2.任期编号、事务标识符最大值、集群ID的值的大小决定了哪个节点更适合作为领导者按照顺序值大的节点更适合作为领导者。

学到这里有同学可能会说“老韩我研究了一下领导者是大多数节点中已提交提案事务标识符最大的节点因为在领导者选举的实现中事务标识符采用的是dataTree.lastProcessedZxid的值而这个变量标记的是已提交提案的事务标识符最大值。到底要怎么理解呢

我要先为你的探索和思考点个赞我想说的是在领导者选举的实现中事务标识符采用的是dataTree.lastProcessedZxid的值。需要你特别注意的是在跟随者节点正常运行时dataTree.lastProcessedZxid表示的是已提交提案的事务标识符最大值但当跟随者检测到异常退出跟随者状态时在follower.shutdown()函数中ZooKeeper会将所有未提交提案提交并使用lastProcessedZxid表示节点上提案包括刚提交的提案的事务标识符的最大值在接下来的领导者选举中使用的也是该值也就是说ZAB的领导者选举选举出的是大多数节点中数据最完整的节点。

为了方便你理解,我举个具体的例子。

A、B、C三节点A是领导者B、C是跟随者B有2个已提交提案(<1, 1><1, 2>)C有4个未提交提案(<1, 1><1, 2><1, 3><1, 4>)那么当A故障后C就会当选领导者。因为C的dataTree.lastProcessedZxid值也就是<1, 4>大于B的dataTree.lastProcessedZxid值也就是<1, 2>)。

最后,你可能会好奇,我为啥会写这么多内容,来分析源码实现,除了因为代码也是一种表达,能有效弥补文字的无法表达的内容之外,还因为对于一个软件来说,最准确、最新的使用手册和技术内幕就是源码。我希望你也能养成阅读源码的习惯,将源码和文档结合起来,来准确理解软件的功能和原理。

选举出了新领导者它是不是就可以处理写请求了呢当然不可以因为ZAB集群还需要通过成员发现Discovery和数据同步Synchronization来恢复故障然后领导者才能行使“领导”的职能处理写请求这也是我会在下一讲重点带你了解的。

课堂思考

既然我提到在ZAB协议中ZAB协议是通过快速领导者选举来选举出新的领导者的。那么你不妨想想在选举中会出现选票被瓜分、选举失败的问题吗为什么呢欢迎在留言区分享你的看法与我一同讨论。

最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。