1

引言

由于一台服务器的处理能力是有限的,在大用户量和高并发的情况下,通常需要很多台服务器同时工作对外提供服务。这么多机器同时工作,怎么来管理这些服务器呢?比如某台服务器宕机了,就要确保请求不再发送到这台服务器;某个程序的配置修改了,多台服务器上的配置要作相应修改;或者多个服务必须按照特定的顺序执行;如果多个服务是有事务的,如果中间某个服务失败了,那这多个服务都必须回滚。对于这些情况最好是有一个专门的管理中心来管理,而ZooKeeper就可以作为这个管理中心。

ZooKeeper是什么

ZooKeeper是Apache下的一个Java开源项目(最初由Yahoo开发,后捐献给了Apache)。
ZooKeeper的原始功能很简单,基于它的层次型的目录树的数据结构,并通过对树上的节点进行有效管理,可以设计出各种各样的分布式集群管理功能。此外,ZooKeeper本身也是分布式的。

ZooKeeper的数据模型

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

ZooKeeper树状结构中的每一个节点称作——<font color=#D2691E>Znode</font>。每一个节点都有一个名称,且以 / 开头,其中最顶层的节点是 / ,最终的情况就是每一个节点在树状结构中,会有一个类似绝对路径的唯一标识,如上图中的 Server1 这个Znode 的标识为 /NameService/Server1

Znode的结构

虽然ZooKeeper的树状结构类似文件系统,但是Znode兼有文件和目录的特点,一个Znode既能在它下面创建子节点,作为路径标识的一部分,同时这个节点同时也能存储数据,但这个存储不是设计用来作常规的数据库存储,而主要存放分布式应用的配置信息、状态信息等,这些数据的共同特性就是它们都是很小的数据,通常以KB为单位。

ZooKeeper数据模型中的每个Znode都维护着一个 <font color=#D2691E>stat</font> 结构。
一个stat仅提供一个Znode的元数据。它由版本号,操作控制列表(ACL),时间戳和数据长度组成。

  • 版本号 - 每个Znode都有版本号,这意味着每当与Znode相关联的数据发生变化时,其对应的版本号也会增加。当多个ZooKeeper客户端尝试在同一Znode上执行操作时,版本号的使用就很重要。

    • version : 当前节点内容(数据)的版本号
    • cversion : 当前节点子节点的版本号
    • aversion : 当前节点ACL的版本号
  • 操作控制列表(ACL) - ACL基本上是访问Znode的认证机制。它管理所有Znode读取和写入操作。每一个节点都拥有自己的ACL,这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。权限的种类有:

    • CREATE : 创建子节点的权限
    • READ : 获取节点数据和节点列表的权限
    • WRITE : 更新节点数据的权限
    • DELETE : 删除子节点的权限
    • ADMIN : 设置节点ACL的权限
  • 时间戳 - 致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个Zxid格式的时间戳,并且这个时间戳全局有序。也就是说,每个对节点的改变都将产生一个唯一的Zxid。如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者三个Zxid值,为别为:cZxid、mZxid、pZxid。

    • cZxid: 是节点的创建时间所对应的Zxid格式时间戳。
    • mZxid:是节点的修改时间所对应的Zxid格式时间戳。
  • 数据长度 - 存储在znode中的数据总量是数据长度。最多可以存储1MB的数据。

每个Znode由3部分组成:

  1. stat:此为状态信息, 描述该Znode的版本, 权限等信息
  2. data:与该Znode关联的数据
  3. children:该Znode下的子节点

节点类型

ZooKeeper中的节点有两种,分别为临时(ephemeral)节点和永久(persistent)节点。节点的类型在创建时即被确定,并且不能改变。

  • <font color=#D2691E>临时节点</font>:该节点的生命周期依赖于创建它们的会话。一旦会话(Session)结束,临时节点将被自动删除,当然可以也可以手动删除。虽然每个临时的Znode都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,ZooKeeper的临时节点不允许拥有子节点。<br/>
  • <font color=#D2691E>永久节点</font>:该节点的生命周期不依赖于会话,客户端创建一个永久节点后即使断开连接,改节点仍然存在,并且只有在客户端显式执行删除操作后,永久节点才被删除。默认创建的节点都是永久节点

顺序节点

<font color=#D2691E>顺序节点</font>可以是持久的或临时的。当一个新的Znode被创建为一个顺序节点时,ZooKeeper通过将10位的序列号附加到原始名称来设置Znode的路径。例如,如果将具有路径/myapp的Znode创建为顺序节点,则ZooKeeper会将路径更改为/myapp0000000001,并将下一个序列号设置为0000000002,这个序列号由父节点维护。如果两个顺序节点是同时创建的,那么ZooKeeper不会对每个Znode使用相同的数字。顺序节点在锁定和同步中起重要作用,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序。

综合上面两节内容,ZooKeeper有四种形式的节点:

  • PERSISTENT(永久节点)
  • PERSISTENT_SEQUENTIAL(永久有序节点)
  • EPHEMERAL(临时节点)
  • EPHEMERAL_SEQUENTIAL(临时有序节点)

Znode的属性

一个节点自身拥有表示其状态的许多重要属性,如下图所示。
Znode的属性

ZooKeeper Session(会话)

Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 Session。

会话对于ZooKeeper的操作非常重要。会话中的请求按FIFO顺序执行。一旦客户端连接到服务器,将建立会话并向客户端分配会话ID 。

客户端以特定的时间间隔发送心跳以保持会话有效。如果ZooKeeper集群在超过指定的时间都没有从客户端接收到心跳,则会话会被认为结束(会话超时)。会话超时通常以毫秒为单位。

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会通知客户端。,监视事件可以理解为一次性的触发器。官方定义如下:

a watch event is one-time trigger, sent to the client that set the watch, which occurs 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)。网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序(有序一致性)。

ZooKeeper可以为所有的读操作设置watch,这些读操作包括:exists()、getChildren()及getData()。

ZooKeeper所管理的watch可以分为两类:

  • 数据watch(data watches):getData和exists负责设置数据watch
  • 孩子watch(child watches):getChildren负责设置孩子watch

我们可以通过操作返回的数据来设置不同的watch:

  • getData和exists:返回关于节点的数据信息
  • getChildren:返回孩子列表

因此

  • 一个成功的setData操作将触发Znode的数据watch
  • 一个成功的create操作将触发Znode的数据watch以及孩子watch
  • 一个成功的delete操作将触发Znode的数据watch以及孩子watch

watch注册与处触发

  • exists操作上的watch,在被监视的Znode创建、删除或数据更新时被触发。
  • getData操作上的watch,在被监视的Znode删除或数据更新时被触发。在被创建时不能被触发,因为只有Znode一定存在,getData操作才会成功。
  • getChildren操作上的watch,在被监视的Znode的子节点创建或删除,或是这个Znode自身被删除时被触发。可以通过查看watch事件类型来区分是Znode,还是他的子节点被删除:NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。

Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的。只有一种情况会导致监视事件的丢失,即:通过exists()设置了某个znode节点的监视,但是如果某个客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。

我们使用ZooKeeper,简单地理解就是使用ZooKeeper的<font color=#D2691E>文件系统+通知机制</font>

ZooKeeper集群

Zookeeper服务自身组成一个集群(2n+1个服务允许n个失效)。在Zookeeper集群中,主要分为三者角色,而每一个节点同时只能扮演一种角色,这三种角色分别是:

  • <font color=#D2691E>Leader</font>:

    • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
    • 集群内各服务器的调度者 leader会与每个follower和observer建立一个tcp长连接,并且为每个follower和observer建立一个learnerhandler,进行数据同步,请求转发和proposal投票等功能。
  • <font color=#D2691E>Follower</font>:

    • 处理客户端的非事务请求,转发事务请求给leader
    • 参与事务请求Proposal投票
    • 参与leader选举投票
    • 判断当前请求是否为事务请求,若是则转发给leader完成事务日志记录后,向leader发送ack信息
  • <font color=#D2691E>Observer</font>:

    • 与Leader进行数据交换(同步)
    • 可以接收客户端连接,将写请求转发给Leader节点
    • Observer不参与投票过程,只同步Leader的状态。
Propsal投票:每一个事务都需要集群中超过半数的机器投票认可才能被真正地应用到ZK的内存数据库中。

下图描述了 ZooKeeper集群“客户端-服务端”的结构

ZooKeeper集群的结构

ZooKeeper的一致性特点

Zookeeper提供的一致性是弱一致性,数据的同步有如下规则:ZooKeeper确保对znode树的每一个修改都会被同步到集群中超过半数的机器上,那么就认为更新成功。所以就有可能有节点的数据不是最新的而被客户端访问到。并且会有一个时间点,数据在集群中是不一致的.也就是Zookeeper只保证最终一致性,但是实时的一致性可以由客户端调用自己来保证,通过调用sync()方法

  • 单一视图性(Single System Image):client不论连接到哪个server,看到的数据是一样的。
  • 实时性:伪实时性。Zookeeper保证客户端将在一个时间间隔范围内(集群大的时候可能要十几秒)从server获得的信息是实时的。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
  • 原子性(Atomicity):操作要么全部成功,要么全部失败,没有中间状态。
  • 顺序性:客户端的更新顺序与它们被发送的顺序相一致。具体表现为全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。
  • 可靠性:一旦一个更新操作被应用,那么在客户端再次更新它之前,它的值将不会改变。这个保证将会产生下面两种结果:

    1. 如果客户端成功地获得了正确的返回代码,那么说明更新已经成果。如果不能够获得返回代码(由于通信错误、超时等等),那么客户端将不知道更新操作是否生效。
    2. 当从故障恢复的时候,任何client看到的已经执行成功的更新操作将不会被回滚。

有了这些一致性保证, ZooKeeper 更高级功能的设计与实现将会变得非常容易,例如: leader 选举、队列以及可撤销锁等机制的实现。

用分布式系统的CAP原则来分析ZooKeeper.

  1. C: ZooKeeper保证了最终一致性,在十几秒可以sync到各个节点.
  2. A: ZooKeeper保证了可用性,数据总是可用的,没有锁.并且有一大半的节点所拥有的数据是最新的,实时的. 如果想保证取得是数据一定是最新的,需要手工调用sync()
  3. P: 有2点需要分析的.

    • 节点多了会导致写数据延时非常大,因为需要多个节点同步.
    • 节点多了leader选举非常耗时, 就会放大网络的问题. 可以通过引入observer节点缓解这个问题.

ZooKeeper的工作原理

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

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

4种状态的解释:

  • LOOKING:当前server不知道leader是谁,正在搜寻。
  • LEADING:当前server即为选举出来的leader。
  • FOLLOWING:leader已经选举出来,当前server与之同步。
  • OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。

ZooKeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议(ZooKeeper Atomic Broadcast protocol)。Zab协议有两种模式,它们分别是恢复模式(Recovery选主)和广播模式(Broadcast同步)。
当服务启动或者在leader崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

为了保证事务的顺序一致性,ZooKeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch(每leader选举一次+1),标识当前属于那个leader的统治时期。低32位为事务操作次数(每增加一次事务+1)

关于Leader Election

当leader崩溃或者集群启动,这时候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会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。选主的具体流程图如下所示:
basic paxos流程

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


liaosilzu2007
285 声望30 粉丝