作为一个优秀高效且可靠的分布式协调框架,ZooKeeper
在解决分布式数据一致性问题时并没有直接使用 Paxos
,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Atomic Broadcast)
原子广播协议,该协议能够很好地支持 崩溃恢复 。
1. ZAB
中的三个角色
和介绍 Paxos
一样,在介绍 ZAB
协议之前,我们首先来了解一下在 ZAB
中三个主要的角色,Leader 领导者
、Follower跟随者
、Observer观察者
。
Leader
:集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。Follower
:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给Leader
。在选举过程中会参与投票,有选举权和被选举权 。Observer
:就是没有选举权和被选举权的Follower
。
在 ZAB
协议中对 zkServer
(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复 。
2. 消息广播模式
说白了就是 ZAB
协议是如何处理写请求的,上面我们不是说只有 Leader
能处理写请求嘛?那么我们的 Follower
和 Observer
是不是也需要 同步更新数据 呢?总不能数据只在 Leader
中更新了,其他角色都没有得到更新吧?
不就是 在整个集群中保持数据的一致性 嘛?如果是你,你会怎么做呢?
废话,第一步肯定需要 Leader
将写请求 广播 出去呀,让 Leader
问问 Followers
是否同意更新,如果超过半数以上的同意那么就进行 Follower
和 Observer
的更新(和 Paxos
一样)。当然这么说有点虚,画张图理解一下。
嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 Queue
哪冒出来的?答案是 ZAB
需要让 Follower
和 Observer
保证顺序性 。何为顺序性,比如我现在有一个写请求A,此时 Leader
将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 Follower
F1因为网络原因没有收到,而 Leader
又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 产生数据不一致问题 。
所以在 Leader
这端,它为每个其他的 zkServer
准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是 **通过 TCP
**来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。
除此之外,在 ZAB
中还定义了一个 全局单调递增的事务ID ZXID
,它是一个64位long型,其中高32位表示 epoch
年代,低32位表示事务id。epoch
是会根据 Leader
的变化而变化的,当一个 Leader
挂了,新的 Leader
上位的时候,年代(epoch
)就变了。而低32位可以简单理解为递增的事务id。
定义这个的原因也是为了顺序性,每个 proposal
在 Leader
中生成后需要 通过其 ZXID
来进行排序 ,才能得到处理。
3. 崩溃恢复模式
说到崩溃恢复我们首先要提到 ZAB
中的 Leader
选举算法,当系统出现崩溃影响最大应该是 Leader
的崩溃,因为我们只有一个 Leader
,所以当 Leader
出现问题的时候我们势必需要重新选举 Leader
。
Leader
选举可以分为两个不同的阶段,第一个是我们提到的 Leader
宕机需要重新选举,第二则是当 Zookeeper
启动时需要进行系统的 Leader
初始化选举。下面我先来介绍一下 ZAB
是如何进行初始化选举的。
假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 server1
,它会首先 投票给自己 ,投票内容为服务器的 myid
和 ZXID
,因为初始化所以 ZXID
都为0,此时 server1
发出的投票为 (1,0)。但此时 server1
的投票仅为1,所以不能作为 Leader
,此时还在选举阶段所以整个集群处于 Looking
状态。
接着 server2
启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1
也会,只是它那时没有其他的服务器了),server1
在收到 server2
的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXID
,ZXID
大的优先为 Leader
,如果相同则比较 myid
,myid
大的优先作为 Leader
。所以此时server1
发现 server2
更适合做 Leader
,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2
收到之后发现和自己的一样无需做更改,并且自己的 投票已经超过半数 ,则 确定 server2
为 Leader
,server1
也会将自己服务器设置为 Following
变为 Follower
。整个服务器就从 Looking
变为了正常状态。
当 server3
启动发现集群没有处于 Looking
状态时,它会直接以 Follower
的身份加入集群。
还是前面三个 server
的例子,如果在整个集群运行的过程中 server2
挂了,那么整个集群会如何重新选举 Leader
呢?其实和初始化选举差不多。
首先毫无疑问的是剩下的两个 Follower
会将自己的状态 从 Following
变为 Looking
状态 ,然后每个 server
会向初始化投票一样首先给自己投票(这不过这里的 zxid
可能不是0了,这里为了方便随便取个数字)。
假设 server1
给自己投票为(1,99),然后广播给其他 server
,server3
首先也会给自己投票(3,95),然后也广播给其他 server
。server1
和 server3
此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid
大的优先,如果相同那么就 myid
大的优先)。这个时候 server1
收到了 server3
的投票发现没自己的合适故不变,server3
收到 server1
的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1
收到了发现自己的投票已经超过半数就把自己设为 Leader
,server3
也随之变为 Follower
。
请注意
ZooKeeper
为什么要设置奇数个结点?比如这里我们是三个,挂了一个我们还能正常工作,挂了两个我们就不能正常工作了(已经没有超过半数的节点数了,所以无法进行投票等操作了)。而假设我们现在有四个,挂了一个也能工作,但是挂了两个也不能正常工作了,这是和三个一样的,而三个比四个还少一个,带来的效益是一样的,所以Zookeeper
推荐奇数个server
。
那么说完了 ZAB
中的 Leader
选举方式之后我们再来了解一下 崩溃恢复 是什么玩意?
其实主要就是 当集群中有机器挂了,我们整个集群如何保证数据一致性?
如果只是 Follower
挂了,而且挂的没超过半数的时候,因为我们一开始讲了在 Leader
中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。
如果 Leader
挂了那就麻烦了,我们肯定需要先暂停服务变为 Looking
状态然后进行 Leader
的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是 确保已经被Leader提交的提案最终能够被所有的Follower提交 和 跳过那些已经被丢弃的提案 。
确保已经被Leader提交的提案最终能够被所有的Follower提交是什么意思呢?
假设 Leader (server2)
发送 commit
请求(忘了请看上面的消息广播模式),他发送给了 server3
,然后要发给 server1
的时候突然挂了。这个时候重新选举的时候我们如果把 server1
作为 Leader
的话,那么肯定会产生数据不一致性,因为 server3
肯定会提交刚刚 server2
发送的 commit
请求的提案,而 server1
根本没收到所以会丢弃。
那怎么解决呢?
聪明的同学肯定会质疑,这个时候 server1
已经不可能成为 Leader
了,因为 server1
和 server3
进行投票选举的时候会比较 ZXID
,而此时 server3
的 ZXID
肯定比 server1
的大了。(不理解可以看前面的选举算法)
那么跳过那些已经被丢弃的提案又是什么意思呢?
假设 Leader (server2)
此时同意了提案N1,自身提交了这个事务并且要发送给所有 Follower
要 commit
的请求,却在这个时候挂了,此时肯定要重新进行 Leader
的选举,比如说此时选 server1
为 Leader
(这无所谓)。但是过了一会,这个 挂掉的 Leader
又重新恢复了 ,此时它肯定会作为 Follower
的身份进入集群中,需要注意的是刚刚 server2
已经同意提交了提案N1,但其他 server
并没有收到它的 commit
信息,所以其他 server
不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 该提案N1最终需要被抛弃掉 。
参考文章: 《JavaGuide | ZooKeeper 相关概念总结》