学习资料来源MIC老师,仅供学习使用。
关于Zookeeper中的一致性
前面我们在讲Zookeeper的数据同步时,提到zookeeper并不是强一致性服务,它是一个最终一致性模型,具体情况如图-4所示。
ClientA/B/C假设只串行执行, clientA更新zookeeper上的一个值x。ClientB和clientC分别读取集群的不同副本,返回的x的值是不一样的。clientC的读取操作是发生在clientB之后,但是却读到了过期的值。很明显,这是一种弱一致模型。如果用它来实现锁机制是有问题的。
顺序一致性模型
顺序一致性提供了更强的一致性保证,我们来观察图-5所示,从时间轴来看,B0发生在A0之前,读取的值是0,B2发生在A0之后,读取到的x的值为1.而读操作B1/C0/C1和写操作A0在时间轴上有重叠,因此他们可能读到旧的值为0,也可能读到新的值1. 但是在强顺序一致性模型中,如果B1得到的x的值为1,那么C1看到的值也一定是1。
需要注意的是:
由于网络的延迟以及系统本身执行请求的不确定性,会导致请求发起的早的客户端不一定会在服务端执行得早。最终以服务端执行的结果为准。
简单来说:顺序一致性是针对单个操作,单个数据对象。属于CAP中C这个范畴。一个数据被更新后,能够立马被后续的读操作读到。
但是zookeeper的顺序一致性实现是缩水版的,在下面这个网页中,可以看到官网对于一致性这块做了解释
ZooKeeper: Because Coordinating Distributed Systems is a Zoo
zookeeper不保证在每个实例中,两个不同的客户端具有相同的zookeeper数据视图,由于网络延迟等因素,一个客户端可能会在另外一个客户端收到更改通知之前执行更新,考虑到2个客户端A和B的场景,如果A把znode /a的值从0设置为1,然后告诉客户端B读取 /a, 则客户端B可能会读取到旧的值0,具体取决于他连接到那个服务器,如果客户端A和B要读取必须要读取到相同的值,那么client B在读取操作之前执行sync方法。 zooKeeper.sync();
除此之外,zookeeper基于zxid以及阻塞队列的方式来实现请求的顺序一致性。如果一个client连接到一个最新的follower上,那么它read读取到了最新的数据,然后client由于网络原因重新连接到zookeeper节点,而这个时候连接到一个还没有完成数据同步的follower节点,那么这一次读到的数据不久是旧的数据吗?实际上zookeeper处理了这种情况,client会记录自己已经读取到的最大的zxid,如果client重连到server发现client的zxid比自己大,连接会失败。
Single System Image的理解
zookeeper官网还说它保证了“Single System Image”,其解释为“A client will see the same view of the service regardless of the server that it connects to.”。实际上看来这个解释还是有一点误导性的。
其实由上面zxid的原理可以看出,它表达的意思是“client只要连接过一次zookeeper,就不会有历史的倒退”。
https://github.com/apache/zookeeper/pull/931
Zookeeper数据同步流程
在zookeeper中,客户端会随机连接到zookeeper集群中的一个节点,如果是读请求,就直接从当前节点中读取数据,如果是写请求,那么请求会被转发给leader提交事务,然后leader会广播事务,只要有超过半数节点写入成功,那么写请求就会被提交(类2PC事务)。
所有事务请求必须由一个全局唯一的服务器来协调处理,这个服务器就是Leader服务器,其他的服务器就是follower。leader服务器把客户端的失去请求转化成一个事务Proposal(提议),并把这个Proposal分发给集群中的所有Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈,那么Leader就会再次向所有的Follower服务器发送Commit消息,要求各个follower节点对前面的一个Proposal进行提交。
那么问题来了
-
1. 集群中的leader节点如何选举出来?
-
2. leader节点崩溃以后,整个集群无法处理写请求,如何快速从其他节点里面选举出新的leader呢?
-
3. leader节点和各个follower节点的数据一致性如何保证
ZAB协议
ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB协议包含两种基本模式,分别是:
-
-
崩溃恢复
-
-
-
原子广播
-
当整个集群在启动时,或者当leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的Leader,当leader服务器选举出来后,并且集群中有过半的机器和该leader节点完成数据同步后(同步指的是数据同步,用来保证集群中过半的机器能够和leader服务器的数据状态保持一致),ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower节点完成了和Leader状态同步以后,那么整个集群就进入了消息广播模式。这个时候,在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和leader节点进行数据同步。
同步完成后即可正常对外提供非事务请求的处理。
需要注意的是:
leader节点可以处理事务请求和非事务请求,follower节点只能处理非事务请求,如果follower节点接收到非事务请求,会把这个请求转发给Leader服务器。
消息广播的实现原理
如图-8所示,前面我们说过,消息广播的过程实际上是一个简化版本的二阶段提交过程
-
leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较既可以实现因果有序这个特征
-
leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的follower
-
当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack
-
当leader接收到合法数量(超过半数节点)的ACK后,leader就会向这些follower发送commit命令,同时会在本地执行该消息
-
当follower收到消息的commit命令以后,会提交该消息
和完整的2pc事务不一样的地方在于,zab协议不能终止事务,follower节点要么ACK给leader,要 么抛弃leader,只需要保证过半数的节点响应这个消息并提交了即可,虽然在某一个时刻follower节点和leader节点的状态会不一致,但是也是这个特性提升了集群的整体性能。 当然这种数据不 一致的问题,zab协议提供了一种恢复模式来进行数据恢复。
这里需要注意的是:
leader的投票过程,不需要Observer的ack,也就是Observer不需要参与投票过程,但是Observer必须要同步Leader的数据从而在处理请求的时候保证数据的一致性.。
崩溃恢复的实现原理
前面我们已经清楚了ZAB协议中的消息广播过程,ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下是没有任何问题的,但是一旦Leader节点崩溃,或者由于网络问题导致Leader服务器失去了过半的Follower节点的联系(leader失去与过半follower节点联系,可能是leader节点和follower节点之间产生了网络分区,那么此时的leader不再是合法的leader了**),那么就会进入到崩溃恢复模式。
崩溃恢复状态下zab协议需要做两件事
-
选举出新的leader
-
数据同步
前面在讲解消息广播时,知道ZAB协议的消息广播机制是简化版本的2PC协议,这种协议只需要集群中过半的节点响应提交即可。但是它无法处理Leader服务器崩溃带来的数据不一致问题。因此在ZAB协议中添加了一个“崩溃恢复模式”来解决这个问题。
那么ZAB协议中的崩溃恢复需要保证,如果一个事务Proposal在一台机器上被处理成功,那么这个事务应该在所有机器上都被处理成功,哪怕是出现故障。为了达到这个目的,我们先来设想一下,在zookeeper中会有哪些场景导致数据不一致性,以及针对这个场景,zab协议中的崩溃恢复应该怎么处理。
已经被处理的消息不能丢弃
当 leader 收到合法数量 follower 的 ACKs 后,就向各个 follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 follower 在收到 COMMIT 命令前leader 就挂了,导致剩下的服务器并没有执行都这条消息。
图中的C2就是一个典型的例子,在集群正常运行过程的某一个时刻,Server1是leader服务器,先后广播了消息P1、P2、C1、P3和C2.
其中当leader服务器把消息C2(Commit事务proposal2)发出后就立即崩溃退出了,那么针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有的服务器上都能被提交成功,否则将会出现不一致。
被丢弃的消息不能再次出现
如图-10所示,当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。
ZAB协议需要满足上面两种情况,就必须要设计一个leader选举算法:能够确保已经被leader提交的事务Proposal能够提交、同时丢弃已经被跳过的事务Proposal。针对这个要求
-
如果leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(ZXID**最** 大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有已经提交的提案。
-
因为所有提案被 COMMIT 之前必须有超过半数的 follower ACK,即必须有超过半数节点的服务器的事务日志上有该提案的 proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被 COMMIT 消息的 proposal 状态.
-
另外一个,zxid是64位,高32位是epoch编号,每经过一次Leader选举产生一个新的leader,新的leader会将epoch号+1,低32位是消息计数器,每接收到一条消息这个值+1,
-
新leader选举后这个值重置为0.这样设计的好处在于老的leader挂了以后重启,它不会被选举为leader,因此此时它的zxid肯定小于当前新的leader。
-
当老的leader作为follower接入新的leader后,新的leader会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除.
关于ZXID
我们前面提到过多次ZXID,它是Zookeeper中的数据对应的事务ID。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。通过stat命令查看节点的信息如下。
[zk: localhost:2181(CONNECTED) 3] stat /node cZxid = 0x5c67 ctime = Thu Aug 19 21:48:16 CST 2021 mZxid = 0x5c67 mtime = Thu Aug 19 21:48:16 CST 2021 pZxid = 0x5c67 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 3 numChildren = 0
实现中zxid是一个64位的数字,它高32位是epoch(ZAB协议通过epoch编号来区分Leader周期变化的策略)用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期。低32位用于递增计数。
比如0x5c67, 实际上是: 500000c67, 前面的5表示epoch,后面的c67是16进制的递增编号。
epoch:可以理解为当前集群所处的年代或者周期,每个 leader 就像皇帝,都有自己的年号,所以每次改朝换代,leader 变更之后,都会在前一个年代的基础上加 1。这样就算旧的 leader 崩溃恢复之后,也没有人听他的了,因为 follower 只听从当前年代的 leader 的命令.
zxid达到最大值后会触发集群重新选举,然后zxid会变为0。
日志中会看到如下信息:
INFO [ProcessThread(sid:31814 cport:-1)::PrepRequestProcessor@137] - zxid lower 32 bits have rolled over, forcing re-election, and therefore new epoch start
Zookeeper的事务日志
Zookeeper的数据是持久化在磁盘上的,默认的目录是在/tmp/zookeeper下,这个目录中会存放事务日志和快照日志。
该路径可以通过zoo.cfg文件来修改,
# 内存数据库快照存放地址
dataDir=/data/zookeeper-3.4.6/data
# 事务日志存储
dataLogDir=/data/zookeeper-3.4.6/data/log
在该目录下可以看到有以下文件内容,在Zab协议中我们知道每当有接收到客户端的事务请求后Leader与Follower都会将把该事务日志存入磁盘日志文件中,该日志文件就是这里所说的事务日志。
其中文件的命名是 log.zxid, 其中zxid表示当前日志文件中开始记录的第一条数据的zxid。
这些内容我们是可以通过Zookeeper自带的工具来查看的,使用命令如下
查看事务日志文件的命令
java -cp :/data/program/apache-zookeeper-3.6.1-bin/lib/slf4j-api-
1.7.25.jar:/data/program/apache-zookeeper-3.6.1-bin/lib/zookeeper-jute-
3.6.1.jar:/data/program/apache-zookeeper-3.6.1-bin/lib/zookeeper-3.6.1.jar
org.apache.zookeeper.server.LogFormatter log.1
查看快照文件的命令
java -cp :/data/program/apache-zookeeper-3.6.1-bin/lib/slf4j-api-
1.7.25.jar:/data/program/apache-zookeeper-3.6.1-bin/lib/zookeeper-jute-
3.6.1.jar:/data/program/apache-zookeeper-3.6.1-bin/lib/zookeeper-
3.6.1.jar:/data/program/apache-zookeeper-3.6.1-bin/lib/snappy-java-1.1.7.jar
org.apache.zookeeper.server.SnapshotFormatter snapshot.0
Leader选举原理分析
接下来再我们基于源码来分析leader选举的整个实现过程。
leader选举存在与两个阶段中,
一个是服务器启动时的leader选举。
另一个是运行过程中leader节点
宕机导致的leader选举 ;
在开始分析选举的原理之前,先了解几个重要的参数
服务器ID(myid)
比如有三台服务器,编号分别是1,2,3。
编号越大在选择算法中的权重越大。
zxid 事务**id**
值越大说明数据越新,在选举算法中的权重也越大
逻辑时钟(**epoch – logicalclock)**
或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断
选举状态
-
LOOKING,竞选状态。
-
FOLLOWING,随从状态,同步leader状态,参与投票。
-
OBSERVING,观察状态,同步leader状态,不参与投票。
-
LEADING,领导者状态。
服务器启动时的leader选举
每个节点启动的时候状态都是LOOKING,处于观望状态,接下来就开始进行选主流程若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下
-
(1) 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID、epoch,使用(myid, ZXID,epoch)来表示,此时Server1的投票为(1, 0,0),Server2的投票为(2, 0,0),然后各自将这个投票发给集群中其他机器。
-
(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。
-
(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下:
i. 优先比较epoch
ii. 其次检查ZXID。ZXID比较大的服务器优先作为Leader
iii. 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
对于Server1而言,它的投票是(1, 0, 0),接收Server2的投票为(2, 0, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
-
(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
-
(5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。
运行过程中的leader选举
当集群中的leader服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的Leader选举,服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。
-
(1) 变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
-
(2) 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票。与启动时过程相同。
-
(3) 处理投票。与启动时过程相同,此时,Server1将会成为Leader。
-
(4) 统计投票。与启动时过程相同。
-
(5) 改变服务器的状态。与启动时过程相同。