ZAB(Zookeeper Atomic Broadcast)协议是一个能保证操作顺序性的,基于主备模式的原子广播协议。
为什么不采用Paxos协议和Raft协议
Multi-Paxos,虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。而这就是 Zookeeper 没有采用 Multi-Paxos 的原因
Raft 出来的比较晚,直到 2013 年才正式提出,而ZooKeeper是在2007年开发的
ZAB 是如何实现操作的顺序性的?
在 ZAB 中,写操作必须在主节点上执行。如果客户端访问的节点是备份节点,它会将写请求转发给主节点。
当主节点接收到写请求后,它会基于写请求中的指令(也就是 X,Y),来创建一个提案(Proposal),并使用一个唯一的 ID,即事务标识符(Transaction ID,也就是 zxid) 来标识这个提案。
事务标识符是 64 位的 long 型变量,有任期编号 epoch 和计数器 counter 两部分组成,格式为 ,高 32 位为任期编号,低 32 位为计数器:
- 任期编号,就是创建提案时领导者的任期编号,需要你注意的是,当新领导者当选时,任期编号递增,计数器被设置为零。比如,前领导者的任期编号为 1,那么新领导者对应的任期编号将为 2。
- 计数器,就是具体标识提案的整数,需要你注意的是,每次领导者创建新的提案时,计数器将递增。比如,前一个提案对应的计数器值为 1,那么新的提案对应的计数器值将为 2
在创建完提案之后,主节点会基于 TCP 协议,并按照顺序将提案广播到其他节点。这样就能保证先发送的消息,会先被收到,保证了消息接收的顺序性。
当主节点接收到指定提案的“大多数”的确认响应后,该提案将处于提交状态(Committed),主节点会通知备份节点提交该提案。
主节点提交提案是有顺序性的。主节点根据事务标识符大小,按照顺序提交提案,如果前一个提案未提交,此时主节点是不会提交后一个提案的。也就是说,指令 X 一定会在指令 Y 之前提交。
总结:
zab协议的有序性保证是通过几个方面来体现的:
- 服务之间用TCP协议进行通讯,保证在网络传输中的有序性;
- 节点之间都维护了一个FIFO的队列,保证全局有序性;
- 通过全局递增的zxid保证因果有序性。
读操作一致性
虽然 ZAB 协议可以实现强一致性,但为了提升读并发能力,Zookeeper 提供的是最终一致性,也就是读操作可以在任何节点上执行,客户端可能会读到旧数据
如果客户端必须要读到最新数据,怎么办呢?Zookeeper 提供了一个解决办法,那就是 sync 命令。你可以在执行读操作前,先执行 sync 命令,这样客户端就能读到最新数据了
ZAB 如何选举领导者?
成员身份
ZAB 支持 3 种成员身份:
- 领导者(Leader): 作为主(Primary)节点,在同一时间集群只会有一个领导者。需要你注意的是,所有的写请求都必须在领导者节点上执行。
- 跟随者(Follower):作为备份(Backup)节点, 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。需要你注意的是,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。
- 观察者(Observer):作为备份(Backup)节点,类似跟随者,但是没有投票权,也就是说,观察者不参与领导者选举和提案提交的投票。(类似Paxos 中的学习者)
虽然 ZAB 支持 3 种成员身份,但是它定义了 4 种成员状态:
- LOOKING:选举状态,该状态下的节点认为当前集群中没有领导者,会发起领导者选举。
- FOLLOWING :跟随者状态,意味着当前节点是跟随者。
- LEADING :领导者状态,意味着当前节点是领导者。
- OBSERVING: 观察者状态,意味着当前节点是观察者。
- 当跟随者检测到连接领导者节点的读操作等待超时了,跟随者会变更节点状态,将自己的节点状态变更成 LOOKING,然后发起领导者选举
- 接着,每个节点会创建一张选票,这张选票是投给自己的。集群的各节点收到选票后,为了选举出数据最完整的节点,对于每一张接收到选票,节点都需要进行领导者 PK,也就将选票提议的领导者和自己提议的领导者进行比较,找出更适合作为领导者的节点,约定的规则如下:
- 优先检查任期编号(Epoch),任期编号大的节点作为领导者;
- 如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
- 如果事务标识符的最大值相同,比较集群 ID,集群 ID 大的节点作为领导者。
如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。
- 赢得大多数选票的节点成为新的领导者。
ZAB 本质上是通过“见贤思齐,相互推荐”的方式来选举领导者的。
成员发现和数据同步
在 ZAB 中,选举出了新的领导者后,该领导者不能立即处理写请求,还需要通过成员发现、数据同步 2 个阶段进行故障恢复。这是 ZAB 协议的设计决定的,不是所有的共识算法都必须这样,比如 Raft 选举出新的领导者后,领导者是可以立即处理写请求的。
成员发现
领导者和大多数跟随者建立连接,并再次确认各节点对自己当选领导者没有异议,确立自己的领导关系;
在当选后,领导者会递增自己的任期编号,并基于任期编号值的大小,来和跟随者协商,最终建立领导关系。具体说的话,就是跟随者会选择任期编号值最大的节点,作为自己的领导者,而被大多数节点认同的领导者,将成为真正的领导者。
ZAB 定义了 4 种状态,来标识节点的运行状态。
- ELECTION(选举状态):表明节点在进行领导者选举;
- DISCOVERY(成员发现状态):表明节点在协商沟通领导者的合法性;
- SYNCHRONIZATION(数据同步状态):表明集群的各节点以领导者的数据为准,修复数据副本的一致性;
- BROADCAST(广播状态):表明集群各节点在正常处理写请求。
只有当集群大多数节点处于广播状态的时候,集群才能提交提案。
选举完成后,LEADING节点和FOLLOWING节点会把自己的 ZAB 状态设置为成员发现(DISCOVERY),FOLLOWING节点会主动联系 LEADING节点,发送给它包含自己接收过的领导者任期编号最大值。而后者接受到前者的信息后,会将包含自己事务标识符最大值的 LEADINFO 消息发给跟随者。当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举;如果是,跟随者返回 ACKEPOCH 消息给领导者。
最后,当领导者接收到来自大多数节点的 ACKEPOCH 消息时,就设置 ZAB 状态为数据同步。
如何处理冲突数据?
当进入到数据同步状态后,领导者会根据跟随者的事务标识符最大值,判断以哪种方式处理不一致数据(有 DIFF、TRUNC、SNAP 这 3 种方式)。通过以领导者的数据为准的方式,来实现各节点数据副本的一致,基于“大多数”的提交原则和选举原则,能确保被复制到大多数节点并提交的提案,就不再改变。
在 ZooKeeper 中,被复制到大多数节点上的提案,最终会被提交,并不会再改变;而只在少数节点存在的提案,可能会被提交和不再改变,也可能会被删除。
在 ZooKeeper 中,一个提案进入提交(Committed)状态,有两种方式:
- 被复制到大多数节点上,被领导者提交或接收到来自领导者的提交消息(leader.COMMIT)而被提交。在这种状态下,提交的提案是不会改变的。
- 另外,在 ZooKeeper 的设计中,在节点退出跟随者状态时,会将所有本地未提交的提案都提交。此时提交的提案,可能并未被复制到大多数节点上,而且这种设计,就会导致 ZooKeeper 中出现,处于“提交”状态的提案可能会被删除(也就是接收到领导者的 TRUNC 消息而删除的提案)。
ZooKeeper 处理读写请求的原理
首先,在 ZooKeeper 中,与领导者“失联”的节点,是不能处理读写请求的。比如,如果一个跟随者与领导者的连接发生了读超时,设置了自己的状态为 LOOKING,那么此时它既不能转发写请求给领导者处理,也不能处理读请求,只有当它“找到”领导者后,才能处理读写请求。
举个例子:当发生分区故障了,C 与 A(领导者)、B 网络不通了,那么 C 将设置自己的状态为 LOOKING,此时在 C 节点上既不能执行读操作,也不能执行写操作。
其次,当大多数节点进入到广播阶段的时候,领导者才能提交提案,因为提案提交,需要来自大多数节点的确认。
最后,写请求只能在领导者节点上处理,所以 ZooKeeper 集群写性能约等于单机。而读请求是可以在所有的节点上处理的,所以,读性能是能水平扩展的。也就是说,你可以通过分集群的方式来突破写性能的限制,并通过增加更多节点,来扩展集群的读性能。
写操作
读操作
相比写操作,读操作的处理要简单很多,因为接收到读请求的节点,只需要查询本地数据,然后响应数据给客户端就可以了。
ZAB 和 Raft 的异同
- 领导者选举:ZAB 采用的“见贤思齐、相互推荐”的快速领导者选举(Fast Leader Election),Raft 采用的是“一张选票、先到先得”的自定义算法。Raft 的领导者选举,需要通讯的消息数更少,选举也更快。
- 日志复制:Raft 和 ZAB 相同,都是以领导者的日志为准来实现日志一致,而且日志必须是连续的,也必须按照顺序提交。
- 读操作和一致性:ZAB 的设计目标是操作的顺序性,在 ZooKeeper 中默认实现的是最终一致性,读操作可以在任何节点上执行;而 Raft 的设计目标是强一致性(也就是线性一致性),所以 Raft 更灵活,Raft 系统既可以提供强一致性,也可以提供最终一致性。
- 写操作:Raft 和 ZAB 相同,写操作都必须在领导者节点上处理。
- 成员变更:Raft 和 ZAB 都支持成员变更,其中 ZAB 以动态配置(dynamic configuration)的方式实现的。那么当你在节点变更时,不需要重启机器,集群是一直运行的,服务也不会中断。
- ZAB 和 ZooKeeper 强耦合,你无法在实际系统中独立使用;而 Raft 的实现(比如 Hashicorp Raft)是可以独立使用的,编程友好。
- 其他:相比 ZAB,Raft 的设计更为简洁,比如 Raft 没有引入类似 ZAB 的成员发现和数据同步阶段,而是当节点发起选举时,递增任期编号,在选举结束后,广播心跳,直接建立领导者关系,然后向各节点同步日志,来实现数据副本的一致性。