这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战
基本概念
- Paxos算法: 是一种通用的分布式一致性算法
- ZAB协议: 是一种特别为Zookeeper设计的崩溃恢复的原子消息广播算法
- Paxos算法是Zookeeper的灵魂,但是Zookeeper并不是完全采用Paxos算法,而是使用ZAB协议作为Zookeeper保证数据一致性的核心算法
ZAB协议
- ZAB协议: Zookeeper Atomic Broadcast
- 是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议
- 在Zookeeper中,主要依赖ZAB协议来实现分布式数据一致性,保证分布式事务的最终一致性
- 基于ZAB协议 ,Zookeeper实现了一种主从模式(Leader-Follower)的系统架构来保证集群中各个副本之间的数据一致性
- ZAB协议中:
- 所有的写操作都必须通过Leader完成 ,Leader写入本地日志后再复制大所有的Follower节点
- 如果Leader节点无法工作 ,ZAB协议会自动从Follower节点中重新选举出一个合适的替代者作为领导者Leader, 这个过程就是集群选举过程
- 集群选举过程是ZAB协议中最为重要且复杂的过程
ZAB协议请求处理
- Zookeeper中的客户端会随机连接到Zookeeper集群中的一个节点并发送请求
- 如果是读请求: 直接从当前节点中读取数据
- 如果是写请求: 节点向Leader节点提交事务 ,Leader节点接收到事务提交,就会广播这个事务,只要超过半数的节点写入成功,这个事务就完成提交
ZAB协议两种模式
- ZAB协议包含两种基本模式:
- 崩溃恢复
- 消息广播
崩溃恢复
- 当整个服务框架在启动过程中,或者是当Leader服务器出现网络中断,崩溃退出以及重启等异常情况时 ,ZAB协议就会进入恢复模式并选举产生新的Leader服务器
- 当选举产生新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成状态同步之后 ,ZAB协议就会退出恢复模式
- 状态同步: 是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致
消息广播
- 当整个集群中已经有过半的Follower服务器完成和Leader服务器的状态同步,那么整个Zookeeper服务框架就可以进入消息广播模式
- 当一台新的ZAB协议的服务器加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播时,新加入的服务器会自动进入数据恢复模式: 即寻找Leader服务器,并与Leader服务器进行数据同步,然后一起参与到消息广播的流程中
- ZAB协议的消息广播步骤:
- 客户端发起一个写操作请求
- Leader服务器将客户的写操作请求转化为事务proposal提议,同时为每个提议proposal分配一个全局的ID, 也就是zxid
- Leader服务器为每个Follower服务器分配一个单独的队列,然后将需要广播的提议proposal依次放到队列中来获取,根据FIFO策略进行消息发送
- Follower服务器接收到提议proposal后,首先将提议proposal以事务日志的方式写入到本地磁盘中,写入成功后向Leader服务器回复一个ACK响应消息
- Leader服务器接收到超过半数以上的Follower服务器的ACK响应消息后,就认为消息发送成功,可以发送commit消息
- Leader服务器向所有Follower服务器广播commit消息,同时自身完成事务提交 .Follower服务器接收到commit消息后,会将上一条事务提交
- Zookeeper使用ZAB协议就是要保证只要有一台服务器提交了proposal提议,就要确保所有的服务器最终都能正确提交proposal提议,这也是CAP或者BASE实现最终一致性的体现
- Leader服务器和每一个Follower服务器间都维护一个单独的FIFO消息队列来收发消息,使用队列消息可以做到异步解耦 ,Leader服务器和Follower服务器只要做到向队列中发送消息即可.如果使用同步的方式会引发阻塞,导致性能下降
ZAB协议保证
- ZAB协议需要保证在Leader服务器上commit提交的事务最终会被所有的服务器提交.也就是说,已经commit提交的数据不会丢失
- ZAB协议需要保证丢弃只在Leader服务器上提出但是没有commit提交的事务.也就是说,未被commit提交的数据对客户端不可见
- 如果Leader服务器以T1和T2的顺序广播,那么所有的服务器Server必须先执行T1, 再执行T2
- 如果任意一个服务器Server以T1,T2的顺序commit执行,那么其余所有的服务器Server也必须以T1,T2的顺序执行
- 在新的领导者Leader广播事务之前,旧的领导者Leader服务器commit提交的事务都会先执行
- 在任意时刻,不可能同时存在2个服务器Server有法定人数quorum个的支持者.这里的quorum是指一半以上的服务器Server的数目,这里的Server是指有投票权力的服务器Server, 也就是不包括Observer
Zookeeper架构总结
- Zookeeper本身是一个分布式程序,只要Zookeeper集群中半数以上的节点正常运行 ,Zookeeper集群就能正常服务
- 为了保证高可用,推荐以集群的形态部署Zookeeper,Zookeeper集群可以容忍一定的机器故障,只要集群中大部分机器是可用的,那么Zookeeper本身依然是可用的
- Zookeeper将数据保存在内存中,这样保证了集群的高吞吐量和低延迟. 但是内存限制了Zookeeper能够存储的数据容量,这也是要保持znode中较小的数据量的原因
- Zookeeper是高性能的. 尤其在读多于写的应用程序中表现更好.因为写会导致所有的服务器间的同步,读多于写是分布式协调服务的典型场景
- Zookeeper中有临时节点. 当创建临时节点的客户端会话session一直保持活动时,临时节点就会一直存在. 当客户端会话session终结时,临时节点就会被删除. 持久节点是指一旦这个节点znode创建后,除非主动进行节点znode的删除操作,否则这个节点znode将会一直保存在Zookeeper上
- Zookeeper的两个基本功能:
- 管理应用程序提交的数据,包括对数据的存储和读取
- 为应用程序提供数据节点的监听服务
- Zookeeper使用主从复制模式,所有的写操作都要由Leader主导完成,读操作可以通过任意节点完成.所以Zookeeper的读性能要远远好于写性能,更加适合读多写少的场景
- Zookeeper使用主从复制模式,同一时间只有一个Leader, 但是通过failover机制保证了集群不会存在单点失败SPOF的问题
- ZAB协议保证failover过程中的数据一致性
- Zookeeper中服务器接收到数据后先写入本地文件再进行处理,保证了数据的持久性
非公平模式
两阶段提交协议问题
- 两阶段提交协议问题:
- 如果Leader服务器发送了提议proposal后发生宕机或者暂时失去连接,这时会导致整个集群处在不确定的状态中 ,Follower服务器便无法知道是放弃这次提交还是执行这次提交
- 这时Zookeeper会选出新的领导者Leader, 请求处理会转移到新的Leader服务器上,不同的Leader服务器具有不同的epoch标识
- 切换领导者Leader时,需要解决以下几个问题:
- commit过的数据可能会丢失
- 未commit过的消息对客户端可见
- 如何通知Follower服务器对外服务
commit过的数据不丢失
- Leader服务器在保存消息后进行广播,接收到超过半数的Follower服务器的ACK确认,消息就可以被commit提交,客户端可见此消息,但是,此时可能还是存在部分Follower服务器没有同步到commit提交的消息.这时发生旧的领导者Leader宕机,那么新的领导者Leader必须保证这个事务commit提交
- 保证已经commit提交的数据不会丢失:
- 选举出新的领导者Leader之后,各个跟随者Follower将自身最大的zxid发送给新的领导者Leader服务器
- 新的Leader服务器会将Follower服务器的zxid和自身zxid之间的所有commit提交过的消息,也就是超过半数的Follower服务器都commit的消息同步给Follower服务器
- 这时Follower服务器zxid的值小于Leader服务器的zxid的值的Follower服务器会根据广播,进行写入和ACK确认.这样来保证已经commit提交的数据不会丢失
- Leader服务器需要确保所有的Follower服务器能够接收到每一条事务的proposal提议,并且能够将所有已经commit提交的事务proposal提议应用到内存数据中.直到Follower服务器将所有没有同步的事务proposal提议都从Leader服务器上同步过并且应用到内存数据中后 ,Leader服务器才会将这个Follower服务器加入到真正可用的Follower列表中
未commit过的消息对客户端不可见
- Leader服务器在保存消息后进行广播,接收到少于半数的Follower服务器的ACK确认.这时消息没有被commit提交,客户端不可见这个消息.但是,此时已经有部分Follower服务器已经同步到这个消息,这时发生旧的领导者Leader宕机,那么新的领导者Leader必须丢弃这个proposal提议,需要部分节点将已经ACK确认的消息删除
- 保证未commit提交过的消息对客户端不可见:
- 选举出新的领导者Leader之后,各个跟随者Follower将自身最大的zxid发送给新的领导者Leader服务器.
- 新的Leader服务器会将Follower服务器的zxid和自身zxid进行比较,获取到自身包含的commit过的消息中的最小zxid的值min_zxid和最大zxid的值max_zxid, 以及各个Follower服务器发送给自身commit提交过的最大消息的zxid的值max_zxid和未commit过的所有消息zxid_set
- 新的Leader服务器根据获取到的值的信息进行以下操作:
- 如果Follower服务器的max_zxid和Leader服务器的max_zxid的值相等: 说明Follower服务器和Leader服务器完全同步,不需要同步任何数据
- 如果Follower服务器的max_zxid在Leader服务器的min_zxid和max_zxid范围内 : Leader服务器会通过TRUNC命令通知Follower服务器将这个Follower服务器中未commit过的所有消息zxid_set中所有大于Follower服务器的max_zxid的消息全部删除
通知Follower对外服务
- 数据同步完成后,新的Leader服务器会向Follower服务器发送NEWLEADER命令并等待Follower服务器的ACK确认消息,然后向所有服务器广播UPTODATE命令
- 接收到UPTODATE命令的服务器可以对外服务
版本控制
- 使用Watcher机制,可以实现分布式服务的协调和通知:
- 比如存在两个客户端同时对节点进行写入操作
- 这两个客户端存在竞争关系,通常需要对节点进行加锁操作
- Zookeeper中通过version版本号来控制乐观锁中的写入校验机制
- 节点znode中维护一个stat数据结构,在stat中记录了节点znode的三种数据版本:
- version : 当前znode节点的版本
- cversion : 当前znode子节点的版本
- aversion : 当前znode节点的ACL版本
分布式锁
- 分布式锁和集群管理Leader选举的实现机制和实现原理类似
- 最多获取一个锁:
- 这里的分布式锁是一个排他锁
- 任意时刻只有一个进程可以获得锁
- 锁重入:
- 分布式锁需要保证获得锁的进程在释放锁之前可以再次获得锁
- 释放锁:
- 分布式锁的获得者能够正确释放已经获得的锁,并且当获得锁的的进程宕机时,锁可以自动释放,从而使得竞争者可以获得这个锁,避免出现死锁的状态
- 感知锁释放:
- 分布式锁的获得者一方释放锁时,其余锁的竞争方可以感知到锁的释放,并且尝试再次获取锁
集群管理Leader选举
- 集群管理Leader选举和分布式锁的实现机制和实现原理类似
- 集群管理选举Leader:
- 集群管理Leader选举在任意时间,最多只有一个成功当选领导者Leader
- 否则的话,会出现脑裂split brain的情况
- 确认自身是Leader:
- 集群管理领导者Leader可以确认自身已经获得领导者,也就是确认自身是领导者Leader
- Leader放弃领导权:
- 集群领导者Leader可以主动放弃领导权,并且当领导者Leader进程宕机时,领导权可以自动释放,从而使得其余的参与者可以重新竞争领导者Leader, 避免进入无主的状态
- 感知领导权的放弃:
- 领导者Leader放弃领导权时,其余参与方可以感知到领导者Leader放弃领导权的事件,并且重新发起选举Leader的流程