本文内容为《从PAXOS到ZOOKEEPER分布式一致性原理与实践》一书学习笔记。本文主要概述第四章内容。
ZooKeeper简介:一个分布式协调服务,一个分布式数据一致性的解决方案。ZooKeeper的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
ZK可保证以下分布式一致特性:
ZooKeeper出现背景:ZooKeeper起源于雅虎研究院的一个研究小组。研究人员发现很多大型系统都基本需要依赖一个类似的系统来进行分布式协调,但这些系统往往都存在分布式单点问题。所以雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。
ZooKeeper的基本概念
ZooKeeper集群角色:
通常分布式系统中采用的集群模式就是Master/Slave模式(主备模式)。把能够处理写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据并提供读服务的机器称为Slave机器。ZooKeeper并没有沿用这种模式,而是引入Leader、Follower和Observer。
Leader: ZooKeeper所有机器通过选举选出Leader。Leader服务器为客户端提供读和写服务。
Follower和Observer: Follower和Observer都能提供读服务,但Observer机器不参与选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能。
会话(Session):
Session指客户端会话。先讲客户端连接:客户端和服务器之间的一个TCP长连接即一个客户端连接。ZooKeeper的对外服务端口默认是2181。客户端启动的时候,会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也就开始了。通过这个连接,客户端能通过心跳检测保证与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。
数据节点:
节点有两类:机器节点:构成集群的机器。数据节点(ZNode):数据模型中的数据单元:ZooKeeper中所有数据存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割的路径,就是一个Znode,例如/foo/path1。
ZNode分为持久节点和临时节点。持久节点:一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode一直保存在ZooKeeper上。临时节点:生命周期和客户端会话绑定,客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
Watcher:
ZooKeeper允许用户在指定节点上注册一些Watcher,并在特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去。有关Watcher的详细工作机制会在后续章节进行介绍。
ACL:
ZAB协议
ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZooKeeper基于ZAB协议实现了一种主备模式的系统架构来保证集群中各副本之间数据的一致性。
消息广播过程:
ZAB协议的消息广播过程使用的是原子广播协议,类似于2PC。针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,将其发送给集群中其余所有的机器,然后再收集各自的选票,最后进行事务提交。
与2PC相比,ZAB协议的二阶段提交过程中移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器;移除中断逻辑同时也意味着可以在过半的Follower服务器已经反馈Ack之后就开始提交事务Proposal了,不需要等待集群中所有的Follower服务器都反馈响应。这种简化的2PC模型也无法处理Leader服务器崩溃退出而带来的数据不一致问题,所以ZAB协议采用下文的崩溃恢复模式来解决这个问题。此外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,可以保证消息广播过程中消息接收与发送的顺序性。
具体消息广播过程:Leader服务器为每个事务生成对应的Proposal,广播前会为这个事务Proposal分配一个全局单调递增的唯一ID(事务ID,也即ZXID)。(由于ZAB协议要保证消息严格的因果关系,因此必须按照ZXID的先后顺序来排序和处理Proposal。)Leader服务器会给每个Follower服务器各自分配一个单独的队列,将Proposal放入并根据FIFO策略发送。每个Follower服务器接收到Proposal后会将其以事件日志的形式写入到本地磁盘中,并在成功写入给Leader服务器反馈ACK。当Leader服务器接收到超过半数Follower的ACK响应后,就会广播通知所有Follower服务器进行事务提交,Leader自身也会进行事务提交。每个Follower服务器完成对事务的提交。
崩溃恢复:
Leader服务器出现崩溃,或由于网络原因导致Leader服务器失去了与过半Follower的联系,进入崩溃恢复模式。崩溃恢复模式要求:确保已经在Leader服务器上提交的事务最终被所有服务器都提交(假设一个事务在Leader服务器上被提交了,并且已经得到过半Follower服务器的Ack反馈,但是在它将Commit消息发送给所有Follower机器之前,Leader服务器挂了);确保丢弃那些只在Leader服务器上被提出的事务(假设初始的Leader服务器Server1在提出一个事务Proposal1后就崩溃退出了,从而导致集群中其他服务器都没有收到这个事务,于是当Server1恢复过来再次加入到集群中的时候,ZAB协议需要确保丢弃Proposal1这个事务)。整个恢复过程结束优先选择拥有集群中所有机器最高编号(即ZXID最大)的Proposal的服务器作为的新的Leader服务器,因为该服务器一定具有所有已经提交的提案。同时,也可以省去Leader服务器检查Proposal的提交和丢弃工作的这一步操作了。
数据同步:
完成 Leader 选举后(新的 Leader 具有最高的zxid),在正式开始工作之前(接收事务请求,然后提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有的 Proposal 是否已经被集群中过半的服务器 Commit,即是否完成集群同步。
正常情况下:Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被Follower服务器同步的的事务以Proposal消息的形式逐个发送,并在每个Proposal消息后面紧跟着发送一个Commit消息表示该事务已经提交。等到Follower从Leader处同步完所有Proposal并成功应用到本地数据库后,Leader服务器才会将该Follower服务器加入到真正可用的Follower列表中。
处理需要被丢弃的Proposal:在 Zab 的事务编号zxid设计中,zxid是一个64位的数字。其中低32位可以看成一个简单的单增计数器,针对客户端每一个事务请求,Leader在产生新的Proposal事务时,都会对该计数器加1。而高32位则代表了Leader周期的epoch编号(epoch编号可以理解为当前集群所处的年代,或者周期。每次Leader变更之后都会在epoch 的基础上加1,这样旧的 Leader 崩溃恢复之后,其他Follower也不会听它的了,因为 Follower只服从epoch最高的Leader命令)。每当选举产生一个新的 Leader,就会从这个 Leader 服务器上取出本地事务日志中最大编号Proposal的zxid,并从zxid中解析得到对应的epoch编号,然后再对其加1,之后该编号就作为新的epoch值,并将低32位数字归零,由0开始重新生成zxid。Zab 协议通过 epoch 编号来区分 Leader 变化周期,能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况。
基于以上策略,当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,肯定无法成为Leader(自:因为epoch编号没现在的Leader大)。当这台机器加入集群中,以 Follower 角色连上 Leader 服务器后,Leader 服务器会根据自己服务器上最后提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对的结果肯定是 Leader 要求 Follower 进行一个回退操作,回退到一个确实已经被集群中过半机器 Commit 的最新 Proposal。
Zab协议原理:参考自:链接。Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播。
发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是体现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。