四、Zookeeper与Paxos

本章概要:
首先对ZooKeeper进行一个整体上的介绍,包括ZooKeeper的设计目标、由来以及它的基本概念,然后将会重点介绍ZAB这一ZooKeeper中非常重要的一致性协议。

1. 初识ZooKeeper

1.1 ZooKeeper介绍

ZooKeeper由雅虎创建,是Chubby的开源实现。设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。

1.1.1 ZooKeeper是什么

ZooKeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举 、分布式锁和分布式队列等功能。ZooKeeper可以保证如下分布式一致性特性。

.1 顺序一致性

从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去。

.2 原子性

所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定 不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。

.3 单一视图(Single System Image)

无论客户端连接的是哪ZooKeeper服务器,其看到的服务端数据模型都是一致性。

.4 可靠性

一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。

.5 实时性

通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是, ZooKeeper仅仅保证在一定额时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。

1.1.2 ZooKeeper的设计目标

ZooKeeper致力于提供一个高性能、高可用,且具有严格的顺序访问控制能力(主要是写操作的严格顺序性)的分布式协调服务。高性能使得ZooKeeper能够应用于 那些对系统吞吐有明确要求的大型分布式系统中,高可用使得分布式的单点问题得到了很好的解决,而严格的顺序访问控制使得客户端能够基于ZooKeeper实现 一些复杂的同步原语。下面我们来具体看一下ZooKeeper的四个设计目标。

.1 目标一:简单的数据模型

ZooKeeper使得分布式程序能够通过一个共享的、树型结构的名字空间来进行相互协调。

这里所说的树型结构的名字空间,是指ZooKeeper服务器内存中的一个数据模型,其由一系列被称为ZNode的数据节点组成,总的来说,其数据模型类似于一个文件系统, 而ZNode之间的层级关系,就像文件系统的目录结构一样。不过和传统的磁盘文件系统不同的是,ZooKeeper将全量数据存储在内存中,以此来实现提高 服务器吞吐、减少延迟的目的。

.2 目标二:可以构建集群

一个ZooKeeper集群通常由一组机器组成,一般3~5台机器就可以组成一个可用的ZooKeeper集群了(2n+1)。

组成ZooKeeper集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。值得一提的,只要集群中存在超过一半的机器 能够正常工作,那么整个集群就能够正常对外服务。

ZooKeeper的客户端程序会选择和集群中任意一台机器共同来创建一个TCP连接,而一旦客户端和某台ZooKeeper服务器之间的连接断开后,客户端会自动连接 到集群中的其他机器。

.3 顺序访问

对于来自客户端的每个更新请求,ZooKeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序可以使用ZooKeeper 的这个特性来实现更高层次的同步原语。

.4 高性能

由于ZooKeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。

1.2 ZooKeeper从何而来

ZooKeeper最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是 这些系统往往都存在分布式单点问题。所以雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人眼将精力集中在处理业务逻辑上。

关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的Pig项目),雅虎 的工程师希望给合格项目也取一个动物的名字,大家纷纷表示就叫动物园管理员吧————因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看 上去就像一个大型的动物园了,而ZooKeeper正好要用来进行分布式环境的协调————于是,ZooKeeper的名字也就由此诞生了。

1.3 ZooKeeper的基本概念

1.3.1 集群角色

通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式(主备模式)。在这种模式下,我们把能够处理所有 写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。

而在ZooKeeper中,这些概念被颠覆了。它没有沿用传统的Master/Slave概念,而是引入了Leader、Follower和Observer三种角色。ZooKeeper集群中的所有 机器通过一个Leader选举过程来选定一台被称为“Leader”的机器,Leader服务器为客户端提供读和写服务。除Leader外,其他机器包括Follower和Observer。 Follower和Observer都能够提供读服务,唯一的区别在于,Observer机器不参与Leader选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以 在不影响写性能的情况下提升集群的读性能。

1.3.2 会话(Session)

Session是指客户端会话,在讲解会话之前,我们首先来了解一下客户端连接。在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接。 ZooKeeper对外的服务端口默认是2181,客户端启动的时候,首先会服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了, 通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。 Session的sessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时, 只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

1.3.3 数据节点(Znode)

在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器。然后,在ZooKeeper中,“节点”分为两类,第一类同样是指构成集群的机器,我们称之为机器节点; 第二类则是指数据模型中的数据单元,我们称之为数据几点————Znode。ZooKeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由/进行分割 的路径,就是一个Znode,例如/foo/path1。每个Znode上都会保存自己的数据内容,同时还会保存一系列属性信息。

在ZooKeeper中,Znode可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个Znode被创建了,除非主动进行Znode的移除操作,否则这个Znode将一直保存在ZooKeeper 上。临时节点生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。另外,ZooKeeper还允许用户为每个节点 添加一个特殊的属性:SEQUENTIAL。一旦节点被标注上这个属性,那么在这个节点被创建的时候,ZooKeeper会自动在其节点名后面追上一个整型数字,这个 整型数字是一个由父节点维护的自增数字。

1.3.4 版本

在前面我们已经提到,ZooKeeper的每个Znode上都会存储数据,对应于每个Znode,ZooKeeper都会为其维护一个叫作Stat的数据结构,Stat中记录了这个Znode 的三个数据版本,分别是version(当前Znode的版本)、cversion(当前Znode字节点的版本)、aversion(当前Znode的ACL版本)。

1.3.5 Watcher

Watcher(事件监听器),是ZooKeeper中得意一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端 会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。

1.3.6 ACL

ZooKeeper采用ACL(Access Control Lists)策略来进行权限控制。

  • CREATE:创建子节点的权限。
  • READ:获取节点数据和子节点列表的权限。
  • WRITE:更新节点数据的权限。
  • DELETE:删除子节点的权限。
  • ADMIN:设置节点ACL的权限。

其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。

1.4 为什么选择ZooKeeper

在解决分布式数据一致性上,除了ZooKeeper之外,目前还没有一个成熟稳定且被大规模应用的解决方案。ZooKeeper无论从性能、易用性还是稳定性上来说, 都已经达到了一个工业级产品的标准。并且开发源码、免费。

2 ZooKeeper的ZAB协议

2.1 ZAB协议

事实上,ZooKeeper并没有完全采用Paxos算法,而是使用了一种称为ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议)的协议作为其数据一致性的核心算法。

ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZAB协议的开发设计人员在协议设计之初并没有要求其具有很好的扩展性, 最初只是为雅虎公司内部那些高吞吐量、低延迟、健壮、简单的分布式系统场景设计的。在ZooKeeper的官方文档也指出,ZAB协议不像Paxos算法那样,是一种 通用的分布式一致性算法,它是一种特别为ZooKeeper设计的崩溃可恢复的原子消息广播算法。

在ZooKeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。具体的, ZooKeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。 ZAB协议的这个主备模型架构保增乐同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。另一方面,考虑到在分布式环境中, 顺序执行的一些状态变更其前后会存在一定的依赖关系,有些状态变更必须依赖于比它早生成的那些状态变更,例如变更C需要依赖变更A和变更B。这样的依赖关系 也对ZAB协议提出了一个要求:ZAB协议必须能够保证一个全局的变更序列被顺序应用,也就是说,ZAB协议需要保证如果一个状态变更已经被处理了,那么所有其 依赖的状态变更都应该已经被提前处理掉了。最后,考虑到主进程在任何时候都有可能出现崩溃退出或重启现象。因此,ZAB协议还需要做到在当前主进程出现上述异常情况的时候,依旧能够正常工作。

ZAB协议的核心是定义了哪些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即:

所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的其他服务器则成为Follower服务器。 Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader 服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower 服务器分发Commit消息,要求其将前一个Proposal进行提交。

2.2 协议介绍

ZAB协议包括两种基本的模式,分别是崩溃恢复和消息广播。当整个服务框架在启动过程中,或是当Leader服务器出现网络中断、崩溃退出与重启等异常情况时, ZAB协议就会进入恢复模式并选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后, ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。

当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守ZAB协议的服务器启动后加入到 集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器, 并与其进行数据同步,然后一起参与到消息广播流程中区。正如上文介绍所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求和处理。 Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求, 那么这些非Leader服务器首先将这个事务请求转发给Leader服务器。

当Leader服务器出现崩溃退出或机器重启,亦或是集群中已经不存在过半的服务器与该Leader服务器保持正常通信时,那么在重新开始新一轮的原子广播事务操作之前, 所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。

一个机器要称为新的Leader,必须获得过半进程的支持,同时由于每个进程都有可能会崩溃,因此,在ZAB协议运行过程中,前后会出现多个Leader,并且每个进程也有可能 会多次成为Leader。进入崩溃恢复模式后,只要集群中存在过半的服务器能够彼此进行正常通信,那么就可以产生一个新的Leader并再次进入消息广播模式。 举个例子来说,一个由3台机器组成的AZB服务,通常由1个Leader、2个Follower服务器组成。某一时刻,加入其中一个Follower服务器挂了,整个ZAB是 不会中断服务的,这是因为Leader服务器依然能够获得过半机器(包括Leader自己)的支持。

2.2.1 消息广播

ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并 将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。

在ZAB协议的二阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器。同时,ZAB协议将二阶段 提交中的中断逻辑移除意味着我们可以在过半的Follower服务器已经反馈Ack之后就开始提交事务Proposal了,而不需要等待集群中所有的Follower服务器都 反馈响应。当然,在这种简化了的二阶段提交模型下,是无法处理Leader服务器崩溃退出而带来的数据不一致问题,因此在ZAB协议中添加了另一个模式, 即采用崩溃恢复模式来解决这个问题。另外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易地保证消息广播过程中 消息接收与发送的顺序性。

在整个消息广播过程中,Leader服务器会为每个事务请求对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配一个 全局单调递增的唯一ID,我们称之为事务ID(即ZXID)。由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每一个事务Proposal按照其ZXID的 先后顺序来进行排序与处理。

具体的,在消息广播过程中,Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列中,并且根据FIFO 策略进行消息发送。每一个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给Leader 服务器一个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会广播一个Comit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身 也会完成对事务的提交,而每一个Follower服务器在接收到Commit消息后,也会完成对事务的提交。

2.2.2 崩溃恢复

一旦Leader服务器出现奔溃,或者说由于网络原因导致Leader服务器失去了与过半Follower的联系,那么就会进入崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行, 整个恢复过程后需要选举出一个新的Leader服务器。因此,AZB协议需要一个高效且可靠的Leader选举算法,从而确保能够快速地选举出新的Leader。同时, Leader选举算法不仅仅需要让Leader自己知道其自身已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生的新的Leader服务器。

.1 基本特性

ZAB协议规定了如果一个事务Proposal在一台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中, 可能会出现的两个数据不一致性的隐患及针对这些情况ZAB协议所需要保证的特性。

ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。

假设一个事务在Leader服务器上被提交了,并且已经得到过半Follower服务器的Ack反馈,但是它将Commit消息发送给所有Follower机器之前,Leader服务器挂了。 例如,在集群正常运行过程中的某一个时刻,Server1是Leader服务器,其先后广播了消息P1、P2、C1、P3、C2,其中,当Leader服务器将消息C2(Commit Of Proposal2) 发出后就立即崩溃退出了。针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不一致。

ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。

假设初始的Leader服务器Server1在提出了一个事务Proposal3之后就崩溃退出了,从而导致集群中的其它服务器都没有收到这个事务Proposal。于是,当Server1 恢复过来再次加入到集群中的时候,ZAB协议需要确保丢弃Proposal3这个事务。

结合上面的这两个崩溃恢复过程中需要处理的特殊情况,就决定了ZAB协议必须设计这样一个Leader选举算法:能够确保提交已经被Leader提交的事务Proposal, 同时丢弃已经被跳过的事务Proposal。针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(即ZXID)的事务 Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交的提案。更为重要的是,如果让具有最高编号事务Proposal的机器来成为Leader,就 可以省去Leader服务器检查Proposal提交和丢弃工作的这一步操作了。

.2 数据同步

完成Leader选举之后,在正式开始工作(即接收客户端的事务请求,然后提出新的提案)之前,Leader服务器会首先确认事务日志中的所有Proposal是否都已经 被集群中过半的机器提交了,即是否完成数据同步。下面我们就来看看ZAB协议的数据同步过程。

所有正常运行的服务器,要么称为Leader,要么称为Follower并和Leader保持同步。Leader服务器需要确保所有的Follower服务器能够接收每一条事务Proposal, 并且能够正确地将所有已经提交了的事务Proposal应用到内存数据中去。具体的,Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被 各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消息后面紧接着再发送一个Commit消息,以表示该事务 已经被提交。等到Follower服务器将所有其尚未同步的事务Proposal都从Loeader服务器上同步过来并成功应用到本地数据库中后,Leader服务器就会将 Follower服务器加入到真正的可用Follower列表中,并开始之后的其它流程。

上面讲到的是正常情况下的数据同步逻辑,下面来看ZAB协议是如何处理那些需要被丢弃的事务Proposal的。在ZAB协议的事务编号ZXID设计中,ZID是一个64位的数字, 其中低32位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数器进行加1操作; 而高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader服务器,就会从这个Leader服务器上取出本地日志中最大事务Proposal的ZXID,并从 该ZXID中解析出对应的epoch值,然后再对其进行加1操作,之后就会以此编号作为新的epoch,并将低32位置0来开始生成新的ZXID。ZAB协议中的这一通过epoch编号 来区分Leader周期变化的策略,能够有效地避免不同的Leader服务器错误地使用相同的ZXID编号提出不一样的事务Proposal的异常情况,这对于识别在Leader崩溃恢复前后 生成的Proposal非常有帮助,大大简化和提升了数据恢复流程。

基于这样的策略,当一个包含了上一个Leader周期尚无提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,原因很简单,因为当前集群中一定包含一个Quorum集合, 该集合中的机器一定包含了更好epoch的事务Proposal,因此这台机器的事务Proposal肯定不是最高,也就无法成为Leader了。当这台机器加入到集群中,以 Follower角色连接上Leader服务器之后,Leader服务器会根据自己服务器上最后被提交的Proposal来和Follower服务器的Proposal进行比对,比对的结果 当然是Leader会要求Follower进行一个回退操作————回退到一个确实已经被集群中过半机器提交的最新的事务Proposal。(ZXID的高32位是纪元,当已经挂了 的Leader重新恢复变成Leader时,其纪元一定小于当前一直在运行的服务器,因此老的Leader就算恢复了也不会成为Leader。大清亡了,新的时代,)

2.3 ZAB与Paxos算法的联系

ZAB协议并不是Paxos算法的一个典型实现,在讲解ZAB和Paxos之间的区别之前,我们首先来看下两者的联系。

两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行。
Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样的一个标识,只是名字变成了Ballot。
在Paxos算法中,一个新选举产生的主进程会进行两个阶段的工作。第一阶段被称为读阶段,在这个阶段中,这个新的主进程会通过和所有其它进程进程通信的方式来收集上一个主进程提出 的提案,并将它们提交。第二阶段被称为写阶段,在这个阶段,当前主进程开始提出它自己的提案。在Paxos算法设计的基础上,ZAB协议额外添加了一个同步阶段。 在同步阶段之前,ZAB协议也存在一个和Paxos算法中的读阶段非常类似的过程,称为发现(Discovery)阶段。在同步阶段中,新的Leader会确保存在 过半的Follower已经提交了之前Leader周期中的所有事务Proposal。这一同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的 进程都已经完成了对之前所有事务Proposal的提交。一旦完成同步阶段后,那么ZAB就会执行和Paxos算法类似的写阶段。

总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计目标不太一样。ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper, 而Paxos算法则是用于构建一个分布式的一致性状态机系统。

3 小结

ZooKeeper的设计目标、由来以及基本概念。另外还有它的一致性协议————ZAB,并将其与Paxos算法进行了比对。

ZooKeeper为了保证状态的一致性,提出了两个安全属性:

  • 全序(消息a和消息b发送的顺序Client和Server看的都是一样的),通过TCP协议的FIFO队列特性实现。
  • 因果顺序(消息a先于消息b发送,则消息a先于消息b执行)。通过Leader消息先到先执行。