目录
ZAB简介
ZAB(ZooKeeper Atomic Broadcast)是zk维持数据一致性采取的算法。zk可以通过改协议来保证在主备模式的系统架构下集群中各副本的一致性。
ZAB基础概念
术语介绍:
- Leader 主: 接收客户端请求,负责将一个客户端事务请求转换成一个 Proposal,并将该 Proposal 分发给集群中的所有 Follower。之后 Leader 等待所有 Follower 的反馈,当过半的 Follower 服务器正确反馈后,也称 Quorum,就会再次向所有 Follower 分发 Commit 消息,将 Proposal 提交。(对标raft协议中的leader)
- Follower 从: 追随主,将写请求转发给主,可以负责读请求。所以,集群扩容的时候会增加读请求的性能,相反,写性能会有所下降,因为需要同步的机器更多了。
- Proposal 提案:
<v, z>
,v 表示值,z 表示 zxid。 - Commit 提交: 事务提交。
- Quorum 仲裁: 一般是指过半机制。(对标raft协议中的大多数状态)
- Oberver 观察者: 从的一种形式,但是不参与选举。引入只是为了系统的可扩展性。
- Epoch 纪元:即每一个 Leader 的任期。(对标raft协议中的term任期)
ZXID
zxid(Zookeeper Transaction Id)是 ZAB 协议的事务编号,是一个 64 位的整数。
epoch | proposal counter |
- 低 32 位代表事务的计数器,每当有一个新事务,counter就会加1
- 高 32 位代表 Leader 的 epoch 编号,类似于 Raft 的任期。每当选举一个新 Leader 时,就会从新 Leader 取出本地日志中的最大事务 Proposal 的 zxid,解析出 epoch 然后加 1,并将低 32 位置 0 来开始新的 zxid。
节点状态
在 ZAB 协议中,每一个进程都有可能处于以下三种状态之一。
Looking | 选举状态 |
Following | 主从下的从节点状态 |
Leading | 主从下的主节点状态 |
每个节点保存以下数据,
history | 接收的提案(proposal) |
lastZxid | history中最新的zxid |
acceptedEpoch | 接受的最新一次的NewEpoch消息中的epoch(NewEpoch是在确定leader后,leader需要统计出Follower中的最大Epoch任期而接收的消息) |
currentEpoch | 接收的最新一次的NewLeader消息中的epoch(NewLeader是leader向follower发送的心跳信息) |
ZAB各个阶段操作术语:
- CEpoch:Follower 发送自己处理过的最后一个事务 Proposal 的 epoch 值。
- NewEpoch:Leader 根据接收 Follower 的 epoch,来生成新一轮 epoch 值。
- Ack-E:Follower 确认接收 Leader 的新 epoch。
- NewLeader:确立领导地位,向其他 Follower 发送 NewLeader 消息。
- Ack-LD:Follower 确认接收 Leader 的 NewLeader 消息。
- Commit-LD:提交新 Leader 的 proposal。
- Propose:Leader 开启一个新的事务。
- Ack:Follower 确认接收 Leader 的 Proposal。
- Commit:Leader 发送给 Follower,要求所有 Follower 提交事务 Proposal。
流程
ZAB 协议主要包括消息广播和崩溃恢复两个过程。具体又可以分为Discovery发现、Synchronization同步以及Broadcast广播三个阶段 。下图是 ZAB 协议的简介 ,我们将每个阶段又分为 3 小节,后续说明每个小节分别做了什么。颜色箭头表示节点间的通信方向,黑色箭头表示进入各阶段的过程。
崩溃恢复
当出现以下情况时,ZAB 协议会进入恢复模式并选举新的 Leader。
- 服务框架重启
- 网络中断
- 崩溃退出
ZAB 协议会进入恢复模式并选举新的 Leader。当选举出新 Leader ,同时集群中已经有过半机器与新 Leader 完成了数据同步之后,ZAB 协议会退出恢复模式。
选举过程(0 Election)
选举过程中,zookeeper默认采取的是快速选举方式:根据选票(3,6)来决定leader节点。
(sid,zxid):选举过程中,首先判断zxid,zxid是我们的事务编号,代表了数据的版本,zxid越大,说明数据版本越新;其次,在zxid一致的情况下,比对sid(也就是集群模式下,配置的myid),sid代表的是节点的权重,sid越大,优先级越高。
- 各个节点先投票给自己,然后广播投票结果给所有节点。也就是说
server 1
把自己的选票(3, 6)
广播给所有节点。 - 如果得到大多数节点的同意,那么就认为自己是 Leader。图中所有节点都选出了
(3, 6)
然后向所有节点广播,确定了 Leader 是 server 1。
根据流程图中三个节点的选票,可以判断出server1成功竞选为leader节点,由Looking状态转换成Leading状态;server2、server3变成server1的从节点,由Looking状态转变成Following状态
发现过程(1 Discovery)
发现过程中包括三个子流程:CEpoch、NewEpoch和Ack-E。整个发现过程主要是为了获取到大多数节点中所有事务集合内的最大的任期Epoch,并在其基础上加1,并广播到对应的大多数Follower进行更新,告诉他们现在来到了一个新的leader的任期
- 第一阶段 CEpoch:Follower 将自己最后处理的事务 Proposal 的 epoch 值发送给 Leader,消息
CEpoch(F.p)
,F.p
可以提取出 zxid。 - 第二阶段 NewEpoch:当 Leader 接收到过半的 Follower 的 CEpoch 消息后,Leader 生成
NewEpoch(e')
发送给这些过半的 Follower,e'
是比任何从 CEpoch 消息中收到的 epoch 值都要大,毕竟要改朝换代嘛。 - 第三阶段 Ack-E:
- Follower 一旦从 Leader 处收到
NewEpoch(e')
消息,如果F.p
<e'
,则进入下一阶段同步,否则,认为当前的epoch是大于leader的epoch的,要求重新选举(理论上一开始就参加了竞选的Follower任期epoch是小于等于leader的任期的,但是可能存在网络分区问题,导致某些后续加上来的节点的zxid是大于leader的,这时候就会开始重新选举,发现存在更大任期的节点,此时leader会变回Looking状态,由另一个节点竞选成leader,并重复后续操作) - Leader 一旦收到了过半的 Follower 的确认消息。它会从这些过半的 Follower 中选取一个 F,并使用它作为初始化事务的集合 S'(用于同步集群数据),然后结束发现阶段。
- Follower 一旦从 Leader 处收到
- 如何选取这个 Follower 呢?因为既然要选择需要同步的事务集合,必然要选择事务最全的吧。所以,须满足epoch 是最大的且 zxid 也是最大的。
同步过程(2 Synchronization)
同步过程同样也包含有三个步骤:NewLeader、Ack-LD和commit-LD。整个同步过程主要是leader将自身proposal事务发送给从属于自身的大多数follower,在确认大多数follower接收到事务消息后,通知他们更新事务,保证follower和leader事务一致。
在完成发现流程之后(即确定了数据源 Follower F 的事务集合 S'
),接下来就进入了同步阶段了。
-
第一阶段 NewLeader:Leader 将新 epoch 和
S'
以NewLeader(e', S')
的消息形式发送给所有过半 (Quorum) 的 Follower。在上一阶段L.history = F.history
,所以 S' 就是流程图中的L.history
。 -
第二阶段 Ack-LD:当 Follower 接收到
NewLeader(e', S')
消息后,- 如果 Follower 的 epoch 等于
e'
,也就是确认是不是该主的子民,因为前一阶段段已经存储了最新的e'
。Follower 将会执行事务应用操作,将接收S'
中的所有事务 Proposal,注意只是接收。 - 如果 Follower 的 epoch 不等于
e'
,即不是这一轮的 Follower,直接进入下一代循环。 - Leader 在接收到过半的 Follower 的 Ack-LD 消息后,发送 Commit 消息所有的 Follower,之后进入下一阶段即 Broadcast(消息广播)。
- 如果 Follower 的 epoch 等于
-
第三阶段 Commit-LD:在收到 Leader 的 Commit 消息后,按顺序依次调用
abdeliver(<v, z>)
处理S'
的每一个事务,随后完成这一阶段。
消息广播
消息广播流程:leader收到客户端的数据后,通知所有follower;follower收到事务处理消息后,保存事务记录到history中,并且返回响应给leader,告知已收到写事务;leader收到大多数follower的响应后,就会发起提交,通知所有follower去提交事务。然后重复上述流程,直到leader出现异常。期间,如果有新的节点加入,也会重复上述的发现-同步流程,将新节点纳入自己的follower集群下。
当主从数据同步完成之后,集群就可以对外服务了,Leader 负责写请求(如果有写请求落到 Follower 上,会转发给 Leader)。
- 第一阶段 Propose:Leader 收到来自客户端新的事务请求后,会生成对应的事务 Proposal,并根据 zxid 的顺序(递增)向追随自己的所有 Follower 发送
P<e', <v, z>>
,其中epoch(z) == e'
。 - 第二阶段 Ack:Follower 根据收到消息的次序来处理这些 Proposal,并追加到 H 中去,然后反馈给 Leader。
- 第三阶段 Commit:一旦 Follower 收到来自 Leader 的
Commit(e', <v, z>)
消息,将调用abdeliver(<v, z>)
提交事务<v, z>
。需要注意的是,此时 Follower 必定提交了z' < z
之前的事务。
接下来集群出去消息广播,正常对外服务的状态,直到下一次选举开始。
广播的流程
在进入消息广播阶段后,Leader 会为每一个 Follower 分配一个 FIFO 形式的队列进行通信,确保了同一时刻一个 Follower 只能和一个 Leader 保持同步,Leader 和 Follower 彼此之间通过心跳检测来感知。
分两种情况 Leader 会终止当前周期的领导,
- Leader 掉线了,Follower 的心跳包会超时,然后 Follower 进入 Looking 状态。
- Leader 已经没有过半的 Follower 追随了,Leader 自己进入 Looking 状态,追随它的 Follower 也会转为 Looking 状态。
当异常情况发生后,就开始新一轮的崩溃恢复过程。
zk推荐奇数台集群搭建以及脑裂问题解析
之所以要推荐用户搭建奇数台的zk集群,主要是因为ZAB协议中的如下限制:集群中存活的节点数必须要超过总节点数的半数才能继续提供服务。
比方数我们有3台服务器的时候,其中一台坏了,存活节点为2,2>3/2.所以能继续提供服务,而当集群中两台出现故障,存活数就为1了,1<3/2,所以就不能对外提供服务了。
而当我们集群中有4台服务器,坏了1台,存活数为3,3>4/2.所以能继续提供服务,而当两台出现故障,存活数就为2. 2=4/2。zk中规定的是必须要大于半数,所以就不能提供服务了。
也就是说,n(奇数)和n+1台机器所具备的容错能力是一致的,多加一台机器只会导致资源上的浪费。当然了,如果是为了提高查询的效率,多加一台也是无可厚非的。
脑裂问题
什么是zk的脑裂问题呢?所谓的脑裂问题,就是在同一个集群环境,存在多个leader.
比方说,我们搭建了一个zk的集群,他们分别搭建在上海和昆明的服务器上。假设一开始,上海的某台机器竞选成立zk的主节点,其他机器作为从节点正常工作。
某一天,由于两地之间的光纤电缆被挖断了,这样就造成了一个网络分区的现象。此时,上海的服务器都正常工作。而昆明的zk节点由于失去了leader的心跳,会重新竞选出一个主节点 。然后,两地的zk节点相互独立,各自提供服务给客户端。倘若,电缆维修好了,此时网络通讯正常,就会出现两个主节点的现象,导致脑裂问题出现。
zk为了避免脑裂现象的出现,也就提出了上面的限制条件:集群中存活的节点数必须要超过总节点数的半数才能继续提供服务。
通过这条限制,即便两地的网络出现问题,无法正常通讯了,昆明的zk节点想要竞选出主节点,也无法达到投票总数达到集群半数的情况,也就无法竞选出主节点,从而是去了服务能力。
后续,网络恢复后,昆明的zk节点又会重新纳入到上海leader节点的管理下,对外提供服务。
为什么限制是必须超过集群半数才能正常提供服务呢?
倘若限制条件是等于集群半数的话,就会出现如下情况:
我们还是以上面的图来说,如果上海和昆明各有3台服务器,总数是6台服务器。两地之间网络断开的时候,如果是等于半数,就会分选出各自的leader,出现脑裂的问题。
综上所述:ZK为了避免脑裂的问题,给出了一个规定:集群中存活的节点数必须要超过总节点数的半数才能继续提供服务,而正是由于这个规定,导致集群中n台和n+1台你的容灾能力是一样的(n为奇数),都只能坏一台。