Zookeeper核心原理

2022-10-27 14:56:34 浏览数 (2)

Zookeeper的角色

  » 领导者(leader),负责进行投票的发起和决议,更新系统状态

  » 学习者(learner),包括跟随者(follower)和观察者(observer),follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票

  » Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度

  » 客户端(client),请求发起方

  • Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协

   议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者

   崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后

   ,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

  • 为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(

   proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识

   leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的

   统治时期。低32位用于递增计数。

  • 每个Server在工作过程中有三种状态:

    LOOKING:当前Server不知道leader是谁,正在搜寻

    LEADING:当前Server即为选举出来的leader

    FOLLOWING:leader已经选举出来,当前Server与之同步

  其他文档:

http://www.cnblogs.com/lpshou/archive/2013/06/14/3136738.html

Zookeeper 的读写机制

  » Zookeeper是一个由多个server组成的集群

  » 一个leader,多个follower

  » 每个server保存一份数据副本

  » 全局数据一致

  » 分布式读写

  » 更新请求转发,由leader实施

Zookeeper 的保证 

  » 更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行

  » 数据更新原子性,一次数据更新要么成功,要么失败

  » 全局唯一数据视图,client无论连接到哪个server,数据视图都是一致的

  » 实时性,在一定事件范围内,client能读到最新数据

Zookeeper节点数据操作流程

    注:1.在Client向Follwer发出一个写的请求

      2.Follwer把请求发送给Leader

      3.Leader接收到以后开始发起投票并通知Follwer进行投票

      4.Follwer把投票结果发送给Leader

      5.Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给 Leader,然后commit;

      6.Follwer把请求结果返回给Client

Follower主要有四个功能:

    • 1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);

    • 2 .接收Leader消息并进行处理;

    • 3 .接收Client的请求,如果为写请求,发送给Leader进行投票;

    • 4 .返回Client结果。

Follower的消息循环处理如下几种来自Leader的消息:

    • 1 .PING消息:心跳消息;

    • 2 .PROPOSAL消息:Leader发起的提案,要求Follower投票;

    • 3 .COMMIT消息:服务器端最新一次提案的信息;

    • 4 .UPTODATE消息:表明同步完成;

    • 5 .REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的 session还是允许其接受消息;

    • 6 .SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。

Zookeeper leader 选举    

选举机制(全新集群paxos)

以一个简单的例子来说明整个选举的过程.

假设有五台服务器组成的zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的.假设这些服务器依序启动,来看看会发生什么.

1) 服务器1启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的选举状态一直是LOOKING状态

2) 服务器2启动,它与最开始启动的服务器1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3),所以服务器1,2还是继续保持LOOKING状态.

3) 服务器3启动,根据前面的理论分析,服务器3成为服务器1,2,3中的老大,而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的leader.

4) 服务器4启动,根据前面的分析,理论上服务器4应该是服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了.

5) 服务器5启动,同4一样,当小弟.

非全新集群的选举机制(数据恢复)

那么,初始化的时候,是按照上述的说明进行选举的,但是当zookeeper运行了一段时间之后,有机器down掉,重新选举时,选举过程就相对复杂了。

需要加入数据id、leader id和逻辑时钟。

数据id:数据新的id就大,数据每次更新都会更新id。

Leader id:就是我们配置的myid中的值,每个机器一个。

逻辑时钟:这个值从0开始递增,每次选举对应一个值,也就是说: 如果在同一次选举中,那么这个值应该是一致的 ; 逻辑时钟值越大,说明这一次选举leader的进程更新.

选举的标准就变成:

1、逻辑时钟小的选举结果被忽略,重新投票

2、统一逻辑时钟后,数据id大的胜出

3、数据id相同的情况下,leader id大的胜出

根据这个规则选出leader。

• 半数通过

    – 3台机器 挂一台 2>3/2

    – 4台机器 挂2台 2!>4/2

  • A提案说,我要选自己,B你同意吗?C你同意吗?B说,我同意选A;C说,我同意选A。(注意,这里超过半数了,其实在现实世界选举已经成功了。

   但是计算机世界是很严格,另外要理解算法,要继续模拟下去。)

  • 接着B提案说,我要选自己,A你同意吗;A说,我已经超半数同意当选,你的提案无效;C说,A已经超半数同意当选,B提案无效。

  • 接着C提案说,我要选自己,A你同意吗;A说,我已经超半数同意当选,你的提案无效;B说,A已经超半数同意当选,C的提案无效。

  • 选举已经产生了Leader,后面的都是follower,只能服从Leader的命令。而且这里还有个小细节,就是其实谁先启动谁当头。

zxid

  • znode节点的状态信息中包含czxid, 那么什么是zxid呢?

  • ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生.

   创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.

Zookeeper工作原理

  » Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。

   当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server的完成了和leader的状态同步以后,恢复模式就结束了。

   状态同步保证了leader和server具有相同的系统状态

  » 一旦leader已经和多数的follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个server加入zookeeper服务中,它会在恢复模式下启动,

   发现leader,并和leader进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在Broadcast状态,直到leader崩溃了或者leader失去了大部分

   的followers支持。

  » 广播模式需要保证proposal被按顺序处理,因此zk采用了递增的事务id号(zxid)来保证。所有的提议(proposal)都在被提出的时候加上了zxid。

   实现中zxid是一个64为的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch。低32位是个递增计数。

  » 当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的server都恢复到一个正确的状态。 

  » 每个Server启动以后都询问其它的Server它要投票给谁。

  » 对于其他server的询问,server每次根据自己的状态都回复自己推荐的leader的id和上一次处理事务的zxid(系统启动时每个server都会推荐自己)

  » 收到所有Server回复以后,就计算出zxid最大的哪个Server,并将这个Server相关信息设置成下一次要投票的Server。

  » 计算这过程中获得票数最多的的sever为获胜者,如果获胜者的票数超过半数,则改server被选为leader。否则,继续这个过程,直到leader被选举出来  

  » leader就会开始等待server连接

  » Follower连接leader,将最大的zxid发送给leader

  » Leader根据follower的zxid确定同步点

  » 完成同步后通知follower 已经成为uptodate状态

  » Follower收到uptodate消息后,又可以重新接受client的请求进行服务了

数据一致性与paxos 算法  

  • 据说Paxos算法的难理解与算法的知名度一样令人敬仰,所以我们先看如何保持数据的一致性,这里有个原则就是:

  • 在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。

  • Paxos算法解决的什么问题呢,解决的就是保证每个节点执行相同的操作序列。好吧,这还不简单,master维护一个

   全局写队列,所有写操作都必须 放入这个队列编号,那么无论我们写多少个节点,只要写操作是按编号来的,就能保证一

   致性。没错,就是这样,可是如果master挂了呢。

  • Paxos算法通过投票来对写操作进行全局编号,同一时刻,只有一个写操作被批准,同时并发的写操作要去争取选票,

   只有获得过半数选票的写操作才会被 批准(所以永远只会有一个写操作得到批准),其他的写操作竞争失败只好再发起一

   轮投票,就这样,在日复一日年复一年的投票中,所有写操作都被严格编号排 序。编号严格递增,当一个节点接受了一个

   编号为100的写操作,之后又接受到编号为99的写操作(因为网络延迟等很多不可预见原因),它马上能意识到自己 数据

   不一致了,自动停止对外服务并重启同步过程。任何一个节点挂掉都不会影响整个集群的数据一致性(总2n 1台,除非挂掉大于n台)。

  总结

  • Zookeeper 作为 Hadoop 项目中的一个子项目,是 Hadoop 集群管理的一个必不可少的模块,它主要用来控制集群中的数据,

   如它管理 Hadoop 集群中的 NameNode,还有 Hbase 中 Master Election、Server 之间状态同步等。

   关于Paxos算法可以查看文章 Zookeeper全解析——Paxos作为灵魂

   推荐书籍:《从Paxos到Zookeeper分布式一致性原理与实践》

Observer  

  • Zookeeper需保证高可用和强一致性;

  • 为了支持更多的客户端,需要增加更多Server;

  • Server增多,投票阶段延迟增大,影响性能;

  • 权衡伸缩性和高吞吐率,引入Observer

  • Observer不参与投票;

  • Observers接受客户端的连接,并将写请求转发给leader节点;

  • 加入更多Observer节点,提高伸缩性,同时不影响吞吐率

为什么zookeeper集群的数目,一般为奇数个?

  •Leader选举算法采用了Paxos协议;

  •Paxos核心思想:当多数Server写成功,则任务数据写成功如果有3个Server,则两个写成功即可;如果有4或5个Server,则三个写成功即可。

  •Server数目一般为奇数(3、5、7)如果有3个Server,则最多允许1个Server挂掉;如果有4个Server,则同样最多允许1个Server挂掉由此,

   我们看出3台服务器和4台服务器的的容灾能力是一样的,所以为了节省服务器资源,一般我们采用奇数个数,作为服务器部署个数。

Zookeeper 的数据模型 

  » 层次化的目录结构,命名符合常规文件系统规范

  » 每个节点在zookeeper中叫做znode,并且其有一个唯一的路径标识

  » 节点Znode可以包含数据和子节点,但是EPHEMERAL类型的节点不能有子节点

  » Znode中的数据可以有多个版本,比如某一个路径下存有多个数据版本,那么查询这个路径下的数据就需要带上版本

  » 客户端应用可以在节点上设置监视器

  » 节点不支持部分读写,而是一次性完整读写

Zookeeper 的节点

  » Znode有两种类型,短暂的(ephemeral)和持久的(persistent)

  » Znode的类型在创建时确定并且之后不能再修改

  » 短暂znode的客户端会话结束时,zookeeper会将该短暂znode删除,短暂znode不可以有子节点

  » 持久znode不依赖于客户端会话,只有当客户端明确要删除该持久znode时才会被删除

  » Znode有四种形式的目录节点

  » PERSISTENT(持久的)

  » EPHEMERAL(暂时的)

  » PERSISTENT_SEQUENTIAL(持久化顺序编号目录节点)

  » EPHEMERAL_SEQUENTIAL(暂时化顺序编号目录节点)

ZNode

这个应该算是Zookeeper中的基础,数据存储的最小单元。在Zookeeper中,类似文件系统的存储结构,被Zookeeper抽象成了树,树中的每一个节点(Node)被叫做ZNode。ZNode中维护了一个数据结构,用于记录ZNode中数据更改的版本号以及ACL(Access Control List)的变更。

有了这些数据的版本号以及其更新的Timestamp,Zookeeper就可以验证客户端请求的缓存是否合法,并协调更新。

而且,当Zookeeper的客户端执行更新或者删除操作时,都必须要带上要修改的对应数据的版本号。如果Zookeeper检测到对应的版本号不存在,则不会执行这次更新。如果合法,在ZNode中数据更新之后,其对应的版本号也会一起更新

这套版本号的逻辑,其实很多框架都在用,例如RocketMQ中,Broker向NameServer注册的时候,也会带上这样一个版本号,叫DateVersion

接下来我们来详细看一下这个维护版本号相关数据的数据结构,它叫Stat Structure,其字段有:

举个例子,通过stat命令,我们可以查看某个ZNode中Stat Structure具体的值。

关于这里的epoch、zxid是Zookeeper集群相关的东西,后面会详细的对其进行介绍。

ACL

ACL(Access Control List)用于控制ZNode的相关权限,其权限控制和Linux中的类似。Linux中权限种类分为了三种,分别是执行,分别对应的字母是r、w、x。其权限粒度也分为三种,分别是拥有者权限群组权限其他组权限,举个例子:

代码语言:javascript复制
drwxr-xr-x  3 USERNAME  GROUP  1.0K  3 15 18:19 dir_name

什么叫粒度?粒度是对权限所作用的对象的分类,把上面三种粒度换个说法描述就是**对用户(Owner)、用户所属的组(Group)、其他组(Other)**的权限划分,这应该算是一种权限控制的标准了,典型的三段式。

Zookeeper中虽然也是三段式,但是两者对粒度的划分存在区别。Zookeeper中的三段式为Scheme、ID、Permissions,含义分别为权限机制、允许访问的用户和具体的权限。

Scheme代表了一种权限模式,有以下5种类型:

  • world 在此中Scheme下,ID只能是anyone,代表所有人都可以访问
  • auth 代表已经通过了认证的用户
  • digest 使用用户名 密码来做校验。
  • ip 只允许某些特定的IP访问ZNode
  • X509 通过客户端的证书进行认证

同时权限种类也有五种:

  • CREATE 创建节点
  • READ 获取节点或列出其子节点
  • WRITE 能设置节点的数据
  • DELETE 能够删除子节点
  • ADMIN 能够设置权限

同Linux中一样,这个权限也有缩写,举个例子:

getAcl方法用户查看对应的ZNode的权限,如图,我们可以输出的结果呈三段式。分别是:

  • scheme 使用了world
  • id 值为anyone,代表所有用户都有权限
  • permissions 其具体的权限为cdrwa,分别是CREATE、DELETE、READ、WRITE和ADMIN的缩写

Session机制

了解了Zookeeper的Version机制,我们可以继续探索Zookeeper的Session机制了。

我们知道,Zookeeper中有4种类型的节点,分别是持久节点、持久顺序节点、临时节点和临时顺序节点。

在之前的文章我们聊到过,客户端如果创建了临时节点,并在之后断开了连接,那么所有的临时节点就都会被删除。实际上断开连接的说话不是很精确,应该是说客户端建立连接时的Session过期之后,其创建的所有临时节点就会被全部删除。

那么Zookeeper是怎么知道哪些临时节点是由当前客户端创建的呢?

答案是Stat Structure中的**ephemeralOwner(临时节点的Owner)**字段

上面说过,如果当前是临时顺序节点,那么ephemeralOwner则存储了创建该节点的Owner的SessionID,有了SessionID,自然就能和对应的客户端匹配上,当Session失效之后,才能将该客户端创建的所有临时节点全部删除

对应的服务在创建连接的时候,必须要提供一个带有所有服务器、端口的字符串,单个之间逗号相隔,举个例子。

127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888

Zookeeper的客户端收到这个字符串之后,会从中随机选一个服务、端口来建立连接。如果连接在之后断开,客户端会从字符串中选择下一个服务器,继续尝试连接,直到连接成功。

除了这种最基本的IP 端口,在Zookeeper的3.2.0之后的版本中还支持连接串中带上路径,举个例子。

127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888/app/a

这样一来,/app/a就会被当成当前服务的根目录,在其下创建的所有的节点路经都会带上前缀/app/a。举个例子,我创建了一个节点/node_name,那其完整的路径就会为/app/a/node_name。这个特性特别适用于多租户的环境,对于每个租户来说,都认为自己是最顶层的根目录/

当Zookeeper的客户端和服务器都建立了连接之后,客户端会拿到一个64位的SessionID和密码。这个密码是干什么用的呢?我们知道Zookeeper可以部署多个实例,如果客户端断开了连接又和另外的Zookeeper服务器建立了连接,那么在建立连接使就会带上这个密码。该密码是Zookeeper的一种安全措施,所有的Zookeeper节点都可以对其进行验证。这样一来,即使连接到了其他Zookeeper节点,Session同样有效。

Session过期有两种情况,分别是:

  • 过了指定的失效时间
  • 指定时间内客户端没有发送心跳

对于第一种情况,过期时间会在Zookeeper客户端建立连接的时候传给服务器,这个过期时间的范围目前只能在2倍tickTime和20倍tickTime之间。

ticktime是Zookeeper服务器的配置项,用于指定客户端向服务器发送心跳的间隔,其默认值为tickTime=2000,单位为毫秒

而这套Session的过期逻辑由Zookeeper的服务器维护,一旦Session过期,服务器会立即删除由Client创建的所有临时节点,然后通知所有正在监听这些节点的客户端相关变更。

对于第二种情况,Zookeeper中的心跳是通过PING请求来实现的,每隔一段时间,客户端都会发送PING请求到服务器,这就是心跳的本质。心跳使服务器感知到客户端还活着,同样的让客户端也感知到和服务器的连接仍然是有效的,这个间隔就是**tickTime**,默认为2秒。

Watch机制

了解完ZNode和Session,我们终于可以来继续下一个关键功能Watch了,在上面的内容中也不止一次的提到**监听(Watch)**这个词。首先用一句话来概括其作用

给某个节点注册监听器,该节点一旦发生变更(例如更新或者删除),监听者就会收到一个Watch Event

和ZNode中有多种类型一样,Watch也有多种类型,分别是一次性Watch和永久性Watch。

  • 一次性Watch 在被触发之后,该Watch就会移除
  • 永久性Watch 在被触发之后,仍然保留,可以继续监听ZNode上的变更,是Zookeeper 3.6.0版本新增的功能

一次性的Watch可以在调用getData()getChildren()exists()等方法时在参数中进行设置,永久性的Watch则需要调用addWatch()来实现。

并且一次性的Watch会存在问题,因为在Watch触发的事件到达客户端、再到客户端设立新的Watch,是有一个时间间隔的。而如果在这个时间间隔中发生的变更,客户端则无法感知。

Zookeeper集群架构

ZAB协议

把前面的都铺垫好之后就可以来从整体架构的角度再深入了解Zookeeper。Zookeeper为了保证其高可用,采用的基于主从的读写分离架构。

我们知道在类似的Redis主从架构中,节点之间是采用的Gossip协议来进行通信的,那么在Zookeeper中通信协议是什么?

答案是**ZAB(Zookeeper Atomic Broadcast)**协议。

ZAB协议是一种支持崩溃恢复的的原子广播协议,用于在Zookeeper之间传递消息,使所有的节点都保持同步。ZAB同时具有高性能、高可用的、容易上手、利于维护的特点,同时支持自动的故障恢复。

ZAB协议将Zookeeper集群中的节点划分成了三个角色,分别是LeaderFollowerObserver,如下图:

总的来说,这套架构和Redis主从或者MySQL主从的架构类似(感兴趣的也可以去看之前的写的文章,都有聊过)

  • Redis主从
  • MySQL主从

不同点在于,通常的主从架构中存在两种角色,分别是Leader、Follower(或者是Master、Slave),但Zookeeper中多了一个Observer。

那问题来了,Observer和Follower的区别是啥呢?

本质上来说两者的功能是一样的, 都为Zookeeper提供了横向扩展的能力,使其能够扛住更多的并发。但区别在于Leader的选举过程中,Observer不参与投票选举

顺序一致性

上文提到了Zookeeper集群中是读写分离的,只有Leader节点能处理写请求,如果Follower节点接收到了写请求,会将该请求转发给Leader节点处理,Follower节点自身是不会处理写请求的。

Leader节点接收到消息之后,会按照请求的严格顺序一一的进行处理。这是Zookeeper的一大特点,它会保证消息的顺序一致性

举个例子,如果消息A比消息B先到,那么在所有的Zookeeper节点中,消息A都会先于消息B到达,Zookeeper会保证消息的全局顺序

zxid

那Zookeeper是如何保证消息的顺序?答案是通过zxid

可以简单的把zxid理解成Zookeeper中消息的唯一ID,节点之间会通过发送**Proposal(事务提议)**来进行通信、数据同步,proposal中就会带上zxid和具体的数据(Message)。而zxid由两部分组成:

  • epoch 可以理解成朝代,或者说Leader迭代的版本,每个Leader的epoch都不一样
  • counter 计数器,来一条消息就会自增

这也是唯一zxid生成算法的底层实现,由于每个Leader所使用的epoch都是唯一的,而不同的消息在相同的epoch中,counter的值是不同的,这样一来所有的proposal在Zookeeper集群中都有唯一的zxid。

恢复模式

正常运行的Zookeeper集群会处于广播模式。相反,如果超过半数的节点宕机,就会进入恢复模式

什么是恢复模式?

在Zookeeper集群中,存在两种模式,分别是:

  • 恢复模式
  • 广播模式

当Zookeeper集群故障时会进入恢复模式,也叫做Leader Activation,顾名思义就是要在此阶段选举出Leader。节点之间会生成zxid和Proposal,然后相互投票。投票是要有原则的,主要有两条:

  • 选举出来的Leader的zxid一定要是所有的Follower中最大的
  • 并且已有超过半数的Follower返回了ACK,表示认可选举出来的Leader

如果在选举的过程中发生异常,Zookeeper会直接进行新一轮的选举。如果一切顺利,Leader就会被成功选举出来,但是此时集群还不能正常对外提供服务,因为新的Leader和Follower之间还没有进行关键的数据同步

此后,Leader会等待其余的Follower来连接,然后通过Proposal向所有的Follower发送其缺失的数据。

至于怎么知道缺失哪些数据,Proposal本身是要记录日志,通过Proposal中的zxid的低32位的Counter中的值,就可以做一个Diff

当然这里有个优化,如果缺失的数据太多,那么一条一条的发送Proposal效率太低。所以如果Leader发现缺失的数据过多就会将当前的数据打个快照,直接打包发送给Follower。

新选举出来的Leader的Epoch,会在原来的值上 1,并且将Counter重置为0。

到这你是不是以为就完了?实际上到这还是无法正常提供服务

数据同步完成之后,Leader会发送一个NEW_LEADER的Proposal给Follower,当且仅当该Proposal被过半的Follower返回Ack之后,Leader才会Commit该NEW_LEADER Proposal,集群才能正常的进行工作。

至此,恢复模式结束,集群进入广播模式

广播模式

在广播模式下,Leader接收到消息之后,会向其他所有Follower发送Proposal(事务提议),Follower接收到Proposal之后会返回ACK给Leader。当Leader收到了quorums个ACK之后,当前Proposal就会提交,被应用到节点的内存中去。quorum个是多少呢?

Zookeeper官方建议每2个Zookeeper节点中,至少有一个需要返回ACK才行,假设有N个Zookeeper节点,那计算公式应该是n/2 1

这样可能不是很直观,用大白话来说就是,超过半数的Follower返回了ACK,该Proposal就能够提交,并且应用至内存中的ZNode。

Zookeeper使用2PC来保证节点之间的数据一致性(如上图),但是由于Leader需要跟所有的Follower交互,这样一来通信的开销会变得较大,Zookeeper的性能就会下降。所以为了提升Zookeeper的性能,才从所有的Follower节点返回ACK变成了过半的Follower返回ACK即可。

ZooKeeper 设计目的

1. 最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。

2. 可靠性:具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受。

3. 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。

但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。

4. 等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。

5. 原子性:更新只能成功或者失败,没有中间状态。

6. 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。

ZooKeeper数据模型

Zookeeper会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统,如图所示:

Zookeeper这种数据结构有如下这些特点:

1)每个子目录项如NameService都被称作为znode,这个znode是被它所在的路径唯一标识,如Server1这个znode的标识为/NameService/Server1。

2)znode可以有子节点目录,并且每个znode可以存储数据,注意EPHEMERAL(临时的)类型的目录节点不能有子节点目录。

3)znode是有版本的(version),每个znode中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据,version号自动增加。

4)znode的类型:

  • Persistent 节点,一旦被创建,便不会意外丢失,即使服务器全部重启也依然存在。每个 Persist 节点即可包含数据,也可包含子节点。
  • Ephemeral 节点,在创建它的客户端与服务器间的 Session 结束时自动被删除。服务器重启会导致 Session 结束,因此 Ephemeral 类型的 znode 此时也会自动删除。
  • Non-sequence 节点,多个客户端同时创建同一 Non-sequence 节点时,只有一个可创建成功,其它匀失败。并且创建出的节点名称与创建时指定的节点名完全一样。
  • Sequence 节点,创建出的节点名在指定的名称之后带有10位10进制数的序号。多个客户端创建同一名称的节点时,都能创建成功,只是序号不同。

5)znode可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是Zookeeper的核心特性,Zookeeper的很多功能都是基于这个特性实现的。关注顶级架构师公众号回复“offer”,送你一份惊喜礼包。

6)ZXID:每次对Zookeeper的状态的改变都会产生一个zxid(ZooKeeper Transaction Id),zxid是全局有序的,如果zxid1小于zxid2,则zxid1在zxid2之前发生。

ZooKeeper Session

Client和Zookeeper集群建立连接,整个session状态变化如图所示:

如果Client因为Timeout和Zookeeper Server失去连接,client处在CONNECTING状态,会自动尝试再去连接Server,如果在session有效期内再次成功连接到某个Server,则回到CONNECTED状态。

注意:如果因为网络状态不好,client和Server失去联系,client会停留在当前状态,会尝试主动再次连接Zookeeper Server。client不能宣称自己的session expired,session expired是由Zookeeper Server来决定的,client可以选择自己主动关闭session。

ZooKeeper Watch

Zookeeper watch是一种监听通知机制。Zookeeper所有的读操作getData(), getChildren()和 exists()都可以设置监视(watch),监视事件可以理解为一次性的触发器

官方定义如下:

a watch event is one-time trigger, sent to the client that set the watch, whichoccurs when the data for which the watch was set changes。

Watch的三个关键点:

  • (一次性触发)One-time trigger

当设置监视的数据发生改变时,该监视事件会被发送到客户端。

例如,如果客户端调用了getData("/znode1", true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件;

而如果 /znode1 再一次发生了变化,除非客户端再次对/znode1 设置监视,否则客户端不会收到事件通知。

  • (发送至客户端)Sent to the client

Zookeeper客户端和服务端是通过 socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的。

Zookeeper 本身提供了顺序保证(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的znode发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event)。

网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。

  • (被设置 watch 的数据)The data for which the watch was set

这意味着znode节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() 和exists()设置数据监视,getChildren()设置子节点监视。

或者你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回znode节点的相关信息,而getChildren() 返回子节点列表。

因此,setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。

一次成功的 delete操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。

Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的。

只有一种情况会导致监视事件的丢失,即:通过exists()设置了某个znode节点的监视,但是如果某个客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。

Consistency Guarantees

Zookeeper是一个高效的、可扩展的服务,read和write操作都被设计为快速的,read比write操作更快。

顺序一致性(Sequential Consistency):从一个客户端来的更新请求会被顺序执行。

原子性(Atomicity):更新要么成功要么失败,没有部分成功的情况。

唯一的系统镜像(Single System Image):无论客户端连接到哪个Server,看到系统镜像是一致的。

可靠性(Reliability):更新一旦有效,持续有效,直到被覆盖。

时间线(Timeliness):保证在一定的时间内各个客户端看到的系统信息是一致的。

ZooKeeper的工作原理

在zookeeper的集群中,各个节点共有下面3种角色和4种状态:

  • 角色:leader,follower,observer
  • 状态:leading,following,observing,looking

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议(ZooKeeper Atomic Broadcast protocol)。Zab协议有两种模式,它们分别是恢复模式(Recovery选主)和广播模式(Broadcast同步)。

当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。关注顶级架构师公众号回复“架构整洁”,送你一份惊喜礼包。

为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。

实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。

每个Server在工作过程中有4种状态:

LOOKING:当前Server不知道leader是谁,正在搜寻。

LEADING:当前Server即为选举出来的leader。

FOLLOWING:leader已经选举出来,当前Server与之同步。

OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。

Leader Election

当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。

Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。

系统默认的选举算法为fast paxos。先介绍basic paxos流程:

1. 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;

2. 选举线程首先向所有Server发起一次询问(包括自己);

3. 选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;

4. 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;

5. 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。

通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n 1,且存活的Server的数目不得少于n 1.

每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。

Leader工作流程

Leader主要有三个功能:

  1. 恢复数据;
  2. 维持与follower的心跳,接收follower请求并判断follower的请求消息类型;
  3. follower的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。

说明:

PING消息是指follower的心跳信息;REQUEST消息是follower发送的提议信息,包括写请求及同步请求; ACK消息是follower的对提议的回复,超过半数的follower通过,则commit该提议; REVALIDATE消息是用来延长SESSION有效时间。

Follower工作流程

Follower主要有四个功能:

  1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
  2. 接收Leader消息并进行处理;
  3. 接收Client的请求,如果为写请求,发送给Leader进行投票;
  4. 返回Client结果。

Follower的消息循环处理如下几种来自Leader的消息:

  1. PING消息:心跳消息
  2. PROPOSAL消息:Leader发起的提案,要求Follower投票
  3. COMMIT消息:服务器端最新一次提案的信息
  4. UPTODATE消息:表明同步完成
  5. REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息
  6. SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。

Zab: Broadcasting State Updates

Zookeeper Server接收到一次request,如果是follower,会转发给leader,Leader执行请求并通过Transaction的形式广播这次执行。

Zookeeper集群如何决定一个Transaction是否被commit执行?通过“两段提交协议”(a two-phase commit):

  • Leader给所有的follower发送一个PROPOSAL消息。
  • 一个follower接收到这次PROPOSAL消息,写到磁盘,发送给leader一个ACK消息,告知已经收到。
  • 当Leader收到法定人数(quorum)的follower的ACK时候,发送commit消息执行。

Zab协议保证:

  • 如果leader以T1和T2的顺序广播,那么所有的Server必须先执行T1,再执行T2。
  • 如果任意一个Server以T1、T2的顺序commit执行,其他所有的Server也必须以T1、T2的顺序执行。

“两段提交协议”最大的问题是如果Leader发送了PROPOSAL消息后crash或暂时失去连接,会导致整个集群处在一种不确定的状态(follower不知道该放弃这次提交还是执行提交)。

Zookeeper这时会选出新的leader,请求处理也会移到新的leader上,不同的leader由不同的epoch标识。切换Leader时,需要解决下面两个问题:

1. Never forget delivered messages

Leader在COMMIT投递到任何一台follower之前crash,只有它自己commit了。新Leader必须保证这个事务也必须commit。

2. Let go of messages that are skipped

Leader产生某个proposal,但是在crash之前,没有follower看到这个proposal。该server恢复时,必须丢弃这个proposal。

Zookeeper会尽量保证不会同时有2个活动的Leader,因为2个不同的Leader会导致集群处在一种不一致的状态,所以Zab协议同时保证:

  • 在新的leader广播Transaction之前,先前Leader commit的Transaction都会先执行。
  • 在任意时刻,都不会有2个Server同时有法定人数(quorum)的支持者。 这里的quorum是一半以上的Server数目,确切的说是有投票权力的Server(不包括Observer)。

总结

简单介绍了Zookeeper的基本原理,数据模型,Session,Watch机制,一致性保证,Leader Election,Leader和Follower的工作流程和Zab协议。

0 人点赞