ZooKeeper源码解析(6)-Zab实现解析

在阅读了Zab的论文<<Zab:High-performance broadcast for primary-backup systems>>之后,总感觉对Zab还是一头雾水.于是,就阅读了ZooKeeper中相关的部分的源码来了解了一下.

在写这篇文章的时候,部分是参考了网上的一篇我认为写的不错的文章:荐 ZooKeeper的一致性算法赏析

上面的那篇文章中,基本上已经介绍的八九不离十了,如果你读过Zab的源码,但是有的地方不是很了解的话,去读一下那篇文章,应该会有一个更加清晰的认识.

但是,即使上面的那篇文章已经写的不错了,但是有一些必须注意的东西,确实没有提到的.

Zab中必须要注意的一些地方

Zab是ZooKeeper实现的核心,它有一些地方也是依赖与ZooKeeper的特性的.

比如,ZooKeeper的操作是幂等性的特性,让它在处理一些细节的时候,可以做到简单的只需要让客户端重新发送一遍即可,而无需考虑客户端发送来的请求是否已经被处理过.

另外,Zab是通过TCP连接的消息的有序性来保证客户端发送过来的消息的有序性以及Leader和Follower之间的消息的有序性.

倘若没有这两点做保证,很多地方的处理,Zab估计就不会那么洒脱了.

所以,如果我们要通过套用Zab来实现一个分布式应用,我们就必须考虑,这个应用能否做到幂等性.如果不满足幂等性,我们就得对Zab稍作修改才能使用.

ZooKeeper中Server的几种状态

在介绍Zab的实现之前,我们必须清楚,ZooKeeper中,Server都有哪几种状态.在Zab的paper中,主要有三种状态:

  • ELECTION state:server正在寻找一个leader
  • FOLLOWING state:集群中已经有一个leader了,并且server作为follower正在跟随这个leader
  • LEADING state:这台server就是那个leader

但是,实际上在ZooKeeper的实现中,Server会处于下面的几种状态:

  • LOOKING state:对应ELECTION状态
  • FOLLOWING state:对应Zab paper中的FOLLOWING state
  • LEADING state:对应Zab paper中的LEADING state
  • OBSERVING state:是ZooKeeper中自己提出的一种状态,主要对应的是Observer Server.Observer跟Follower的区别在于,它不参与投票,即当Leader统计某个proposal的票数时,是不会把Observer计算在内的.

那么一台Server是如何在上面的四个状态中转换的呢?

当一台Server启动时,它会首先处于LOOKING状态,此时会尝试找到一个leader,如果找到,那么就看看这个leader是不是就是它,如果不是的话,就转换为FOLLOWING/OBSERVING状态,如果是它,那么就转换为LEADING状态.

当处于FOLLOWING/OBSERVING状态的server检测到它跟它知道的leader失去联系时,就会重新进入到LOOKING状态,并尝试重新找一个leader.

同样,当处于LEADING状态的server发现它跟处于FOLLOWING状态的server失去联系时,也会进入到LOOKING状态并尝试重新找到一个leader.

具体的leader的选举过程,我们会在下文中介绍.

而且,细心地读者可能已经注意到了,在上文中,我们说的是处于LEADING状态的server,而不是直接说leader.这是为什么呢?

因为leader并不一定处于LEADING state.为什么呢?

因为每台server都会记录当前它认为的leader,这个leader并不一定就是此时集群中的leader.leader也会定时向Follower或Observer发送PING信息,看它是否还是集群中的leader.假如此时集群中的leader发生了故障,并变化为LOOKING state,而集群中的Follower或Observer需要隔一段时间才能感知到它认为的leader此时已经跟它失去了联系.

Zab在ZooKeeper中的实现

在Zab的实现中,主要是经历了这样三个阶段:

Fast Leader Election ->Synchronization -> Broadcast

Fast Leader Election

从名字中,我们也能看出来,这个阶段就是选择一个leader的.

这部分的源码主要是在FastLeaderElection.javalookForLeader()方法中.

那什么时候,才需要选择一个leader呢?

  • 当集群启动时,所有的server都处于LOOKING状态
  • 当server从故障中恢复时
  • 当有新的server想要加入集群时

我们先介绍几个关键变量:

  • logicalclock:记录此server当前选举的轮次
  • Notification:寻找leader时用到的一个数据结构,在各台Server之间,用于传输选举数据.
  • Notification.version:Notification的版本号,主要是为了向后兼容,之前的版本和当前版本的Notification中的某些属性可能不同
  • Notification.leader:当前Server想要选举为leader的Server
  • Notification.zxid:当前Server记录的它最后收到的proposal的id,zxid由两部分组成,一部分是epoch,一部分是proposal id.其中epoch是leader的任期,transaction id是就是proposal的id.
  • Notification.electionEpoch:当前Server所处的选举轮次
  • Notification.state:当前Server的状态,LOOKING/LEADING/FOLLOWING/OBSERVING中的一个.
  • Notification.sid:当前Server的id
  • Notification.peerEpoch:当前Server记录的它选举的leader的任期

这里比较容易混淆的是electionEpochpeerEpoch,以及electionEpochlogicalclock

electionEpochlogicalclock的区别在于,electionEpoch指的是发出Notification的server的logicalclock,而logicalclock则指的是当前Server所处的选举的轮次,每次调用lookForLeader()方法,它的值都会加一.

electionEpochpeerEpoch的区别在于,electionEpoch记录的选举的轮次,而peerEpoch则指的是当前leader的任期.什么意思呢?

想象这样一种情景,一个Server处于网络分区的状态,它会不断调用lookForLeader()方法来寻找leader,上面我们也介绍过,每次调用lookForLeader()方法,logicalclock的值都会加一,所以这台Server就会不断地向外发送electionEpoch逐渐增加,而peerEpoch不变的notification.直到网络分区恢复,并成功找到了一台leader.

有点难理解?没关系,在下面的选举过程中,你就直到它们都有什么用了.

一台ZooKeeper Server,假设它的代号为A,在启动时,都会先从本地的Snapshot file以及Transaction logs中读取数据,尽可能地恢复数据.此时,从历史数据中,它能够获取到latestLoggedZxid以及peerEpoch

然后,它会将它的sid,以及上面获取到的latestLoggedZxidpeerEpoch,组装成一个Notification,发送给集群中的除Observer外的全部成员.注意,发送的对象包括它自己.

其他的Server,这里我们假设有一台Server,其代号为B,在收到来自A的Vote后,通过比较,发现,诶,你小子要选举的leader的数据竟然比我要选举的leader的完整,那我选你选的那个吧,然后记录下从A中收到的zxid以及peerEpoch,同时,告诉其他Server,我Server B选举Server A选举的那个leader,它的zxid为从A中收到的zxid,它的peerEpoch为从A中收到的peerEpoch

这里有一个问题,就是,Server A想要选举的leader要满足什么条件,Server B才会选举Server要选举的那个leader?

  • A的peerEpoch比B的大
  • 或者A的peerEpoch跟B的一样大,但是A的zxid比B的大
  • 或者A的peerEpoch和zxid都跟B的一样大,但是A的sid比B的大

上面三个条件,满足一个,Server B就会选举Server A要选举的那个leader.

当Server B发现选举Server A选举的那个leader的票数已经超过服务器数的一半了,它就看看是否还有比服务器比Server A选举的那个leader更适合成为leader,如果不存在的话,就进入到FOLLOWING状态,成为leader,如果存在的话,就继续上面的过程.直到最后进入FOLLOWING状态,或者超时.

这里我们思考一下,有没有可能产生出现两个leader的可能性呢?

假设有五台服务器,并产生了网络分区,Server A和Server B处于一个网络分区,另外三台服务器又处于另一个网络分区中.由于Notification采用的是广播的形式,所以,很可能Server A选举它自己为leader,然后告诉Server B,Server B收到后,通过对比,发现Server B更适合成为leader,然后告诉Server A,Server A收到后,发现确实Server B更适合成为leader,到最后,Server B就到了LEADING状态,而Server A就到了FOLLOWING状态.

此时,另一个网络分区中,也已经选举出了一个leader.

Server A此时尽管到了FOLLOWING状态,但是最终它是不会认为Server B此时是leader的,Server B也成不了leader,为什么呢?在Synchronization阶段中,我们会解答这个问题.

最终Server B和Server A只能又回到LOOKING状态.

所以,是不可能产生两个leader的,即使发生了网络分区.

另一个问题是,如果Server A发生了网络分区,那么它会无限次调用lookForLeader()方法,每次调用这个方法,logicalclock都会+1.此时,怎么办呢?

答案是,还是按照之前说的那三个条件来判断是否Server A更适合成为leader,而那三个条件中,没有一个是根据选举轮次logicalclock进行判断的.也就是说,其他的Server,比如Server B,在收到长时间分区后来又恢复的Server A的vote时,会先将自己的选举轮次也调整为Server A的选举轮次,然后用上面的三个条件进行比较,选择更适合作为leader的Server.

如果集群中全部的Server此时都是LOOKING状态,这样做很正确.但是,如果此时集群中已经存在leader了,有没有可能Server A的数据比此时集群中的leader更新,更适合成为leader呢?

之前这里我纠结了好久,最终发现是不可能的.考虑Server A刚发生分区的时候,此时我们假设Server A确实有一些最新的数据,即Server A是Leader或者Server A已经从Leader中接收到了最新的数据.这里我们只要分清proposed但是未被commited的proposalproposed并且已经被commited的proposal的区别,就很容易理解为什么不可能.

实际上,这里还跟Zab处理proposal的过程相关.跟2PC类似,Zab是将Proposal发送给除Observer之外的全部Server,然后,当收到其中大多数的Follower的回复之后,就再发送一个COMMIT命令,告诉Follower提交这个proposal,收到大多数的Follower的回复之后,然后再告诉Client,请求已经处理完了.

这里需要注意的地方有两点:

  • 只有Leader收到大多数的Follower的回复之后,才会发送COMMIT命令.在Leader发送COMMIT命令之前,proposal是处于proposed但是未被committed状态的,而在发送COMMIT之后,proposal就进入了proposed并且已经被commited的状态.

  • 只有大多数收到Follower的COMMIT回复之后,Leader才告诉Client,请求已经处理完了.

这里我们可以看到,只要Client认为请求已经被处理完,那么肯定有一多半的Follower已经提交了proposal.也就是说,此时,集群中肯定至少有一半的Server的数据跟Server A的数据一样新.

所以,此时选举的时候,选举出的leader肯定包含全部proposed并且已经被committed的proposal.

即使丢失了一些proposed但是未被committed的proposal,这也无所谓,因为leader并没有发送给客户端请求成功的消息.并且ZooKeeper的操作都是幂等性的,所以,再让客户端发送一遍也无所谓.

这里也就是前面我们说的Zab的实现依赖于ZooKeeper的操作是幂等性的特性.试想,如果ZooKeeper的操作不是幂等性的,这里能这么简单地进行处理吗?

还有一个问题是,由于Leader和Follower有各自的超时机制,Leader如果在一段时间内未收到大多数Follower的响应,就会认为它已经失去了集群的支持,就会进入LOOKING状态,而Follower如果在一段时间内未收到来自Leader的PING命令,会认为它已经和Leader失去了响应,从而进入LOOKING状态,重新查找Leader.

但是,这个时间是物理时间,它在各个机器上并不是严格相等的,中间可能存在一定的误差,即使我们知道只要误差小于一定的值,基本上就可以达到同步,但是Zab的实现中可不是这么做的.这就会导致可能Leader已经超时,进入了LOOKING状态,而Follower只能过一段时间才能检测到这一事件,并且也进入LOOKING状态.

如果此时有一台Server想要加入集群,此时Follower都会告诉这台Server此时的leader时那台已经宕机的Server.那么如何解决这个问题呢?

所以,在Zab的实现中,有这么一段代码,用来检测Follower告诉这台Server此时的leader是否给这台Server发送过消息,以及Follower认为的leader此时是否确实处于LEADING状态:

Synchronization

在找到一个leader之后,follower必须向leader发送一条FOLLOWERINFO信息,向leader表明此follower已经接受过的leader的epoch.

当leader收到大多数follower的FOLLOWERINFO之后,它会从中选择一个最大的epoch,然后将其+1作为leader的epoch.

然后,通过LEADERINFO命令,leader将新的epoch发送给各个follower.Follower在收到leader的LEADERINFO命令之后,会将leader的epoch设置为leader的epoch,并且通过ACKEPOCH消息告诉leader该follower的zxid.leader收到ACKEPOCH消息之后,会查看follower是否缺少某些proposal logs,如果缺少就发送给follower,如果follower中存在一些leader中没有的proposal logs,那么就告诉follower截断这部分日志.这都是通过NEWLEADER命令来做的.

在follower接收到来自leader的NEWLEADER命令时,会记录下需要同步的日志,然后回复leader,leader在收到follower的回复之后,就会发送一个COMMIT命令,让follower真正进行同步.

需要注意的是,这部分操作都是同步的,也就是说,在同步日志的时候,follower是无法处理来自客户端的请求的.试想,如果follower此时还处理来自客户端的请求,那日志同步哪时候能做完?

在同步完成后,follower会告诉leader同步已经完成.

此时有两个问题.

第一个问题是,这里leader收到的只是大多数follower的FOLLOWERINFO,而不是全部的FOLLOWERINFO,就进行了选择,那么会不会剩下的FOLLOWERINFO中,存在更新的epoch呢?

答案是,不会.因为leader只有在收到follower都认同该epoch的ACKEPOCH回复,才会将自己设置自己的epoch.即使Server B经历了这样的过程,在选举的过程中,它被选举为leader,此时,它收到来自Follower的FOLLOWERINFO,并选择了其中的最大的epoch+1作为它的epoch,然后还没等它将这个epoch发送给follower,就挂掉了.Server B的epoch也不会比集群中其他的Server的更大.

还有一个问题,就是,哪时候follower才会有leader不存在的数据呢?

试想下面的Server B,Server B赢得了选举,被选举为leader,此时,它收到了来自Follower的FOLLOWERINFO,并选择了其中的最大的epoch+1作为它的epoch,然后它将这个epoch发送给follower,同步完之后,开始接受客户端请求,然后,它收到了客户端的请求1,并添加到了它的proposal logs中了,在它发送给follower之前,它崩溃了.然后Server C赢得了选举,当Server C给集群中的其他Server同步的时候,Server B突然恢复了,并且通过lookForLeader()方法发现此时Server C是集群中的leader.在这种情况下,Server B中就存在Server C中leader不存在的数据.

那么,简单的让follower截断这部分leader不存在的数据,是否会有损失数据的风险呢?

同样,托ZooKeeper的操作是幂等性的福,leader中不存在的数据肯定是还没有给客户端成功信息的数据,所以,让客户端再重新发送一次就好了.

Broadcast

在这个阶段,server就开始处理客户端请求了.当leader收到一个客户端请求时,会提出一个zxid为当前zxid+1的proposal,然后广播给集群中的followers,当收到大多数follower的回复时,就告诉follower提交该proposal,在收到大多数follower的提交命令的回复时,就会给客户端一个成功信息.

当follower收到一个提交命令时,会同时提交前面收到的proposal.

同时,leader也会不断向follower发送心跳检测信息,检测一下是否还能连接到集群中的多数follower.follower也有一个超时检测,当一段时间内没有收到leader的心跳检测信息时,就进入到LOOKING状态,重新选择一个leader.

总结

ZooKeeper中Zab的实现中,由于使用TCP连接保证了消息的有序性,又由于ZooKeeper中操作的幂等性,使Zab的实现复杂度大大降低.

同时,ZooKeeper的实现中,还根据是否有readonlymode.enable这个系统变量来确定到底是保证CAP中的CP还是AP.如果这个变量为true,那么就会在server发生分区时,启用一个ReadOnlyServer,这会做到AP,但是无法保证一致性.如果这个变量为false,那么在server发生分区时,不会启用ReadOnlyServer,这会保证CP,但是无法保证可用性.

从上面的解析中,我们也可以看到,在处理proposal时,只要多数Follower同意,那么就提交,而且客户端请求是可以到达Follower/Observer的,所以,ZooKeeper实现的是弱一致性模型.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 一个真正的写数据流程是怎么样的?一个真正的读数据流程是怎么样的?一个真正的同步数据流程是怎么样的?从哪里到哪里?什...
    时待吾阅读 3,967评论 0 14
  • zookeeper是我在使用dubbo的时候认识的一个中间件,它是一个分布式服务框架,当时候,我只知道dubbo通...
    vivi_wong阅读 563评论 0 0
  • Apache Zookeeper是由Apache Hadoop的子项目发展而来,于2010年11月正式成为Apac...
    olostin阅读 6,041评论 2 9
  • 一 我还以为会一直喜欢孙燕姿到长大, 没想到直到现在,最喜欢是戴佩妮。 二 那时候我上小学, 当别人都已经开始参加...
    计廿阅读 197评论 0 0
  • 找重点!找重点! 这样的话听过多少? 为了考试,这样的话听了不知道多少遍 ,但是没有认识到一个问题。重点多了,就没...
    墨幽雨阅读 83评论 0 0