前言
在日常工作中经常用到Zookeeper
,但是一直以来都没有好好去深入了解它,以至于对于我而言它就是一个黑匣子,充满了未知的乐趣,下面就记录了我的学习笔记过程。
Zookeeper是什么
ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. All of these kinds of services are used in some form or another by distributed applications. Each time they are implemented there is a lot of work that goes into fixing the bugs and race conditions that are inevitable. Because of the difficulty of implementing these kinds of services, applications initially usually skimp on them, which make them brittle in the presence of change and difficult to manage. Even when done correctly, different implementations of these services lead to management complexity when the applications are deployed.
这是官网的原文,下面用翻译软件进行中文的翻译如下:
ZooKeeper是用于维护配置信息,命名,提供分布式同步和提供组服务的集中式服务。 所有这些类型的服务都以某种形式被分布式应用程序使用。 每次实施它们时,都会进行很多工作来修复不可避免的错误和竞争条件。 由于难以实现这类服务,因此应用程序最初通常会跳过它们,这会使它们在存在更改的情况下变得脆弱并且难以管理。 即使部署正确,这些服务的不同实现也会导致管理复杂。
总感觉官网的译文有点晦涩难懂,所以用《从Paxos到Zookeeper分布式一致性原理与实践》一书来概括如下:
Zookeeper为分布式应用提供了高效且可靠的分布式协调服务,提供了诸如统一命名服务、配置管理和分布式锁等分布式的基础服务。在解决分布式数据一致性方面,Zookeeper并没有直接采用
Paxos
算法,而是采用了一种被称为ZAB(Zookeeper Atomic Broadcast)
的一致性协议。
Zookeeper分布式一致性特性
-
顺序一致性
从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到
Zookeeper
中去。 -
原子性
所有事务请求处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况出现。
-
单一视图
不论客户端连接的是哪个
Zookeeper
服务器,其看到的服务端数据模型都是一致的。 -
可靠性
一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会一直保留下来,除非有另外一个事务又对其进行了变更。
-
实时性
Zookeeper
仅仅保证在一定时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
Zookeeper的设计目标
-
简单的数据模型
Zookeeper
使得分布式程序能够通过一个共享的、树形结构的名字空间来进行相互协调。这里的树形结构名字空间是指Zookeeper
服务器内存中的数据模型,由一系列的称为ZNode
的数据节点组成。类似我们常用系统中的文件系统或者一棵树模型。和文件系统不同之处在于Zookeeper
将全量数据保存在内存中,以此来实现提供服务器吞吐、减少延迟的目的。 -
可以构建集群
一个Zookeeper集群通常有一组机器构成,一般3~5台机器就可以组成一个可用的集群,如下图所示。
组成Zookeeper
的集群的每台机器都会在内存中维护当前的服务器状态,并且每台服务器之间都保持着通信。只要集群中存在超过一半的机器能正常工作,那么整个集群就能够正常对外服务。
Zookeeper
的客户端程序会选择和集群中的任意一台机器共同来创建一个TCP
连接,而一旦客户端和某台Zookeeper
服务器之间的连接断开后,客户端会自动连接到集群中的其它机器。
-
顺序访问
对于来自客户端的每个更新请求,
Zookeeper
都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序可以使用Zookeeper
这个特性来实现跟高层次的同步原语。 -
高性能
由于
Zookeeper
将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它非常适用于以读操作为主的应用场景。
Zookeeper基本概念
集群角色
Zookeeper
中加入了Leader、Follower和Observer三种角色。Zookeeper
集群中的所有机器通过Leader选举过程来选定一台被称为“Leader”的机器,Leader服务端为客户端提供读写服务。Follower和Observer都能提供读服务,唯一区别在于,Observer机器不参与Leader选举过程,也不参与写操作的”过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能,关于Zookeeper
的Observer节点详细说明,可以参考博客ZooKeeper 增加Observer部署模式提高性能。
会话
客户端通过TCP
协议与服务器建立连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话连接,也能够向Zookeeper
发送请求并接收响应,同时还能接收来自服务器Watch
事件通知。
数据节点
Zookeeper
中的节点分为机器节点(集群的机器)和数据节点(ZNode)。Zookeeper
将所有数据都存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割路径,就是一个ZNode,例如/foo/path1。每个ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息。
ZooKeeper
中的ZNode还分为临时节点和持久节点两类。
- 持久节点:节点一旦创建,除非主动移除,否则该节点会一直保存在
Zookeeper
上; - 临时节点:生命周期与客户端会话绑定,一旦客户端会话失效,该客户端创建的临时节点也会随之移除。
Zookeeper
还允许用户为节点设置特殊属性:SEQUENTIAL。拥有该属性的节点会采用顺序递增的方式添加到节点名的后面。
版本
Zookeeper
的每个ZNode上都会存储数据,每个ZNode在ZooKeeper
中都会维护一个叫做Stat的数据结构,Stat中记录了这个ZNode的三个数据版本:
- version:当前ZNode版本;
- cversion:当前ZNode子节点的版本;
- aversion:当前ZNode的ACL版本;
Watcher
Zookeeper
允许用户在指定的节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper
服务端将事件通知到感兴趣的客户端上去,该机制是Zookeeper
分布式协调服务的重要特性。
ACL
Zookeeper
采用ACL
(Access Control Lists)策略来进行权限控制,类似于UNIX
文件系统的权限控制。Zookeeper
定义了如下5种权限。
- CREATE:创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIN:设置节点ACL权限。
尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。
ZAB协议
Zookeeper
为高可用的一致性协调框架,自然的Zookeeper
也有着一致性算法的实现,Zookeeper
使用的是ZAB协议作为数据一致性的算法,ZAB(Zookeeper Atomic Broadcast)全称为:原子消息广播协议。
ZAB可以说是在Paxos
算法基础上进行扩展而来,ZAB协议设计支持了崩溃恢复,Zookeeper
使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数据状态变更以事务Proposal的形式广播到所有Follower上;
由于在分布式环境中事务存在一定的依赖关系,例如变更C需要依赖变更A和变更B,所以ZAB协议提出一个要求:
一个状态变更已经被处理了,那么所有其依赖的状态变更都应该已经被提前处理了。
ZAB协议支持的崩溃恢复可以保证在Leader进程崩溃后重新选出Leader并且保证数据的完整性;
在Zookeeper中所有的事务请求必须由Leader服务器来协调处理,而余下的则成为Follower服务器。Loader服务器将客户端的事务请求转换为成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求将其前一个Proposal进行提交。
消息广播
ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程,参考博客分布式系统一致性协议--2PC,3PC。针对客户端的事务请求,Leader服务器会将其生成的对应的事务Proposal,并将其发送给集群中的其余机器,然后再分别收集各自的应答消息(ACK),最后进行事务的提交(Commit),下图是ZAB协议消息广播流程示意图。
在ZAB协议的二阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器。同时,ZAB协议将二阶段提交中的中断逻辑移除意味着我们可以在过半的Follower服务器已经反馈ACK之后就开始提交事务Proposal了,而不需要等待集群中所有的Follower服务器都反馈响应。
整个消息的广播是采用FIFO来进行网络通信的,所以能保证消息在广播过程中接收与发送的顺序性。
Leader服务器在广播事务Proposal之前,Leader服务器会为这个事务Proposal分配一个全局单调递增的唯一ID,我们称之为事务ID(即ZXID)。由于ZAB协议需要保证每一个消息严格因果关系,因此必须将每一个事务Proposal按照其ZXID的先后顺序进行排序与处理。
在消息广播过程中,Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列中去,并且根据FIFO策略进行发送。每一个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给Leader服务器一个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会广播一个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每一个Follower服务器再接收到Commit消息后,也会完成对事务的提交。
奔溃恢复
当Leader服务器出现崩溃或者说由于网络原因导致Leader服务器失去了与过半Follower服务器的联系,那么就会进入崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后会出现一个新的Leader服务器。
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。
上图表示在一个正常的集群环境中,Server1是Leader服务器,其先后广播了P1、P2、C1、P3和C2,其中当Leader服务器将消息C2(C2是Commit Of Proposal2的缩写)发出后立即崩溃退出了。针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有机器都被提交成功,否则将出现不一致。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。
如果在恢复过程中出现一个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务Proposal,如下图所示。
假设初始的Leader服务器Server1在提出了一个事务Proposal3之后就崩溃退出了,从而导致集群中的其它服务器都没有收到这个事务的Proposal。于是,当Server1恢复过来再次加入到集群中的时候,ZAB协议需要确保丢弃Proposal3这个事务。
崩溃恢复过程中,为了保证数据一致性需要处理特殊情况:
- 已经被Leader服务器提交的Proposal确保最终被所有Follower服务器提交;
- 确保那些只在Leader服务器被提出的Proposal被丢弃。
数据同步
-
正常情况
针对客户端的事务请求,Leader服务器会将其生成的对应的事务Proposal,并且Leader服务器会为每个Follower服务器生成对应的消息队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,然后再分别收集各自的应答消息(ACK),最后再发送一个Commit消息,以表示该事务已提交。等到Follower服务器将所有其尚未同步的事务从Proposal都从Leader服务器上同步过来并成功应用到数据库中后,Leader服务器就会将该Follower服务器加入到真正可用的Follower列表中。
-
恢复丢弃
在ZAB协议的事务编号ZXID设计中,ZXID是一个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,原因很简单,因为当前集群中一定包含一个Quorum集合,该集合中的机器一定包含了更高epoch的事务Proposal,因此这台机器的事务Proposal肯定不是最高,也就无法成为Leader了。当这台机器加入到集群中,以Follower角色连接上Leader服务器之后,Leader服务器会根据自己服务器上最后被提交的Proposal来和Follower服务器的Proposal进行比对,比对的结果当然是Leader会要求Follower进行一个回退操作。之后Leader也会要求Follower去除之前的Proposal。
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
算法类似的写阶段。
参考资料
《从Paxos到Zookeeper分布式一致性原理与实践》
ZooKeeper 增加Observer部署模式提高性能
分布式系统一致性协议--2PC,3PC