月泉的博客

ZooKeeper的一些原理了解一下?

月泉 ZooKeeper分布式

嗯,我想想从什么地方写起比较好呢,我觉得从分布式的Leader选举,还有它消息同步的分布式事务说起吧,大纲走一波

  • 为什么需要选举Leader
  • 如何选举Leader的及使用什么机制达到数据一致性
  • 简单阅读下Leader选举的源码

为什么需要选举Leader

要了解这个问题,首先要很明确的知道ZooKeeper集群下几个角色的主要分工(Leader、Follower、Observer)

Leader

  • 处理跟事务相关的操作(增、删、改)
  • 将消息同步到旗下的Follower
  • 集群内部各服务器的调度者同时保证事务处理的顺序性

Follower

  • 将跟事务处理相关的操作转发到其对应的Leader
  • 参与事务请求Propsal投票(因为在同步消息时,只有大多*数Follower都事务提交成功,该次提交才算成功,否则进行回滚)
  • 参与Leader的选举

Observer

仅作为观察者,观察ZooKeeper中集群的最新状态并且把集群的最新状态更新到Observer中,它不参与Propsal投票也不参与选举,只是在一旁静静的在不影响事务性能的情况下提升非事务处理的能力。

在明白了这些问题了后,来聊聊为什么需要选择Leader?在搞清楚这个问题之前我们再回归到本源,我们为什么需要集群?假如我们使用了ZooKeeper把它用作我们的注册中心

在有很多个服务都注册在ZooKeeper上,那我们要如何保障其高可用呢?最简单和有效的办法那便是做集群,一旦我们做了集群,那么首先解决的问题就是单点故障的问题,不会因为一台机器的宕机从而导致所有依赖ZooKeeper的服务都不能使用,从而再从集群的这个实现中分摊了来自各服务的流量,从而提升了性能。

那从上面的点出发,看我们要一个高性能高可用的集群,那么ZooKeeper是如何做到的,很简单它直接化繁至简,在ZooKeeper这种分布式协调服务其自身又需要存储数据,那么首先就要保障集群中的数据一致性,那么要是每个节点都能写入数据,那么为了达到数据一致性,那么是不是都要通知各个服务器去同步这个节点更新了的数据,单单是这一步就已经很麻烦了,还要对更新不成功的节点进行重新更新等,整个事情的复杂度就变得很高了,所以这里直接定义一个Leader节点,将所有的事务节点都转发到Leader节点上来处理,通过Leader节点来下发事务给各个Follower节点进行同步,所有的非事务请求任何节点接收到都可以自行处理掉,整个事情就变得简单起来了,但Leader节点一旦宕机挂掉后,这些Follower节点也会立马选举一个Leader节点出来继续干Leader节点的事情,那么是如何选举Leader的呢?如果有十个事务,Leader节点处理到一半还剩五个事务未处理,新选举出来的Leader又是如何处理的呢?旧的Leader上线后对自身未完成的事务又该何去何从呢?跟着我的思路,看下一个小节

如何选举Leader的及使用什么机制达到数据一致性

了解这个问题之前先了解一件事情,通常ZooKeeper都是由2n + 1台Server组成,对于2n+1台Server只要有n+1台Server可用,那么整个系统就保持可用,怎么理解这个概念呢?就是过半的意思理解吧?如果有3台机器组成的集群其挂掉1台还剩2台任然可以正常工作,如果有5台机器的集群能够在对2台机器挂掉的情况下还能正常工作并且做出相应的容灾处理,反正过半就对了,之所以要满足一个这样的集群是因为其集群中会有一个节点会成为该集群中的Leader需要有超过该集群过半数的节点支持,有了这个概念,我们继续深入的来理解下是如何选举Leader的。

这里可以很简单的描述,但你不一定能看懂,但没关系跟着我的节奏你肯定能看懂,ZooKeeper的选举通常情况由

  • zxid
  • myid

来决定谁是Leader,那么这里又会疑问什么是zxid什么是myid?

zxid

zxid是一个64位的有规则编号,其高的32位是epoch编号,其低的32位是消息计数器,没接收到一条消息,该消息计数器就会累加加1,但这里奇怪的是什么是epoch编号,epoch编号理解起来并不难,它实际上就是每一轮投票选举Leader都会+1的一个计数器,可在每台节点机器的/tmp/zookeeper/acceptedEpoch文件来查看当前epoch是几,一个epoch就是一个王的上任

myid

看过上一篇的myid是什么还要解释吗?没看过的赶快去看

接下来说一下其选举机制,首先每个节点启动的状态都是LOOKING状态,处于观望状态,然后其内部开始执行选举,每个Server都会向节点之间互相广播投票,每次投票信息都会包含所推举的服务器的myid和zxid,实际上刚启动大家推的都是自己,我们这里使用 (myid, zxid)来表示,Server1投出去的信息(1, 0)、Server2投出去的信息(2, 0),首先会根据ZXID来选举谁是Leader节点可以看到2者的zxid都为0那么接着比较其myid,Server2的myid要比server1的大那么应该是投给server2来做Leader节点,那么经过这一步后开始统计投票集群中的投票信息,只要根据该步骤选举投票大于过半的机器时此时便认为已经选举出Leader,即刻改变集群节点之间的状态是Follower都就更新自己的节点为Following,是Leader就更新自己的状态为Leading

上面简单的说了下选举机制但有没有考虑过,如果在使用过程中Leader突然挂掉了?Leader是如何重新进行选举的?

说这个问题之前我们要来了解下ZAB协议,ZAB(ZooKeeper Atomic Broadcast)是一种专门为ZooKeeper设计的原子广播协议,用其保证在分布式数据中的数据一致性,该协议有2种最基本的模式分别为:崩溃恢复、原子广播。

当集群刚启动或者Leader节点宕机时整个集群就会进入一个恢复模式来选举一个Leader,当有过半的机器与该Leader节点连接并且完成数据同步时基于ZAB协议就会退出恢复模式,当有过半的Follower节点完成了和Leader节点的数据同步后,整个集群就进入了消息广播模式,如果在一个完整的集群中再添加了一个新的节点,那么这个节点就会进入紧急模式,通过完成与Leader的同步后,再度转为消息广播模式。

原子广播

ZooKeeper的原子广播实际上是基于一个简化版的2PC提交过程,熟悉分布式事务的应该了解2PC,我这里就不详细介绍什么是2PC了,等哪天我可能会心血来潮写一篇分布式事务相关的文章时,再好好介绍什么是2PC什么是3PC吧~

根据我画的图,我在文字详述一遍 1.Leader接收到事务时首先会对该事务生成一个zxid(都还记得这个是啥吧?) 2.在Leader中有一个FIFO队列专门用来存放proposal,这个proposal实际上就是将带有zxid的消息封装成一个proposal,在使用FIFO队列进行有序分发。 3.在Follower接收到请求后,会对该proposal写入磁盘,然后会服务器返回一个结果ACK 4.服务器收到过半的成功ACK时,就会对Follower发送commit指令,同时本地也会执行 5.当Follower收到commit指令后,执行commit

崩溃恢复

一旦有Leader节点宕机或者Leader节点失去了大半Follower节点的连接,可能因为网络波动,那么此时的Leader就不再是一个合法的Leader,那么就会进入崩溃恢复模式,在ZAB协议中为了保障正确性,整个恢复过程结束后需要选举一个新的Leader,为了使得崩溃后还能选举出新的Leader来进行正常工作首先要保证2件事

  • 已经被处理的事务不能丢失
  • 过时的事务不能再出现

已经被处理的事务不能丢失 怎么理解呢?很简单,根据原子广播Leader收到ACK后会对Follower节点发出commit指令,当这个指令指向了一台机器发送还未向其它机器发送,自己就挂了,那么这个事务已经发送给了一个节点commit就代表已经被处理过了

过时的事务不能再出现 就比如我Leader节点目前接收了10个事务,生成了10个proposal,当还没有发送就已经挂了,也就是没有一个follower接收到了该事务,此时会进入崩溃恢复模式,选举出新的Leader,当这台老Leader恢复了,连接上了它也只能做一个Follower节点,其自身没有处理过的事务也应该视为过时应该进行丢弃

那么这里就很灵性了?想想选举机制崩溃后如何选举新的Leader?如果有节点执行了commit那么它的zxid是不是比较大,那么这些执行了commit的节点就是极有可能是leader节点,如果只有一台执行了commit,不出其它牛角尖的话,那么它就是Leader,因为它是最新的嘛!

简单看下ZooKeeper选举源码

首先我们从一个具有main函数的QuorumPeerMain类开始,我们就以它为入口吧

会发现他创建了一个QuorumPeerMain的实例并执行了其initializeAndRun方法

如果是集群就会执行runFromConfig这个方法里做了一堆配置,简单看下不用太过关心,重点不是这,接着继续看,可以看到它调用了一个start方法

loadDataBase就恢复其自身数据库其实就是从本地文件中恢复数据,然后获取最新的zxid,关键是这里的startLeaderElection看到这个方法名,就知道了吧?在这里开始选举惹~

这里会获取自身的myidzxid还有epoch封装成Vote实例,顾名思义吧?先将自己的投票设置成自己的再说,关键最后一步,创建选举算法,跟着来看看

默认从config中读取出的是3,可以在runFromConfig中看到相关配置,首先创建了一个QuorumCnxManager的实例,该类主要负责Leader选举的IO,然后启动已绑定端口的选举监听现场,等待其他机器进行连接,然后创建一个FastLeaderElection实例

关键看starter方法

里面创建了2个队列,分别是一个接收队列和一个发送队列,然后创建了一个Messenger实例

该构造函数中设置了发送队列和接收队列的线程,并启动线程,WorkSender是负责发送投票的,WorkerReceiver是负责接收投票的,再回到start

还发现其调用了父类的start方法,那么就直接看它的run方法不就好了嘛,这不就是覆盖了下Threadstart嘛? 直接看run方法的关键点,看哪里还要我解释吗?当然是LOOKING状态了,这是一个观望状态

重点是这里通过makeLESStrategy()来选用一个选举算法进行Leader选举,接着看lookForLeader

这2个一个是用来存放收到的投票一个是存储选举结果的

逻辑锁+1,并且更新自己的zxid和epoch

一直循环到选出Leader

从接收队列中拿投票信息

如果队列拿出来的为空,那就是发完了,继续发送一直到选出leader

收到投票确认是否是本集群中的node投票

如果收到的节点epoch大于logicallock则表示当前是一轮新的选举

拿自己的票和收到的票PK一波

先判断epoch是不是比当前的大,如果是比当前的大,那么我这个就是旧的,因为每一轮选举后都会epoch+1那么则是对方的大,那么对方胜出,对方是leader,如果epoch相等那么就判断zxid,如果对方的zxid大,那么对方胜出对方是leader,如果 epoch和zxid都相等,那么就判断myid,谁大谁胜出谁是它认可的Leader

然后发送广播消息

过完那一堆if后,添加本次收到的投票的对象,用来选举时的最终判断

这里判断选举是否结束,默认算法就是超过半数server投票通过,这里的while结束以后

确定Leader和修改自身状态,即选举完毕

就介绍到这里~~~ 我想想我下一篇是写《ZooKeeper Java API 及 Wacher原理》还是写Dubbo。

如果你觉得作者写的这篇文章对你有帮助或者收获很大的话,你可以尝试着请我喝一瓶《东方树叶》或者吃一块《鸡胸》来激励我!!!

微信请客

支付宝请客

月泉
伪文艺中二青年,热爱技术,热爱生活。