Zookeeper

Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。

Zookeeper提供了一个类似于Linux文件系统的树形结构 (可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件), 同时提供了对于每个节点的监控与通知机制。

据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

分布式应用程序可以基于Zookeeper实现

  • 数据发布/订阅
  • 负载均衡
  • 命名服务
  • 分布式协调/通知
  • 集群管理
  • Master选举
  • 分布式锁和分布式队列

Zookeeper保证了如下分布式一致性特性:

  • 顺序一致性
  • 原子性
  • 单一视图
  • 可靠性
  • 实时性(最终一致性)

客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。 对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功。 因此,随着zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。

有序性是zookeeper中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳, 这个时间戳称为zxid(Zookeeper Transaction Id)。 而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper最新的zxid

<a id="wjxt>Zookeeper 文件系统</a>

Zookeeper提供一个多层级的节点命名空间(节点称为znode)。与文件系统不同的是,这些节点都可 以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。 Zookeeper为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M

<a id="tzjz>Zookeeper 通知机制</a>

Zookeeper允许客户端对服务端的某个znode注册一个watcher监听事件,当服务端的一些指定事件触发了这个watcher, 服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据watcher通知状态和事件类型做出业务上的改变。

大致分为三个步骤:

  • 客户端注册watcher
    • 调用getDatagetChildrenexist三个API ,传入watcher对象。
    • 标记请求request,封装watcherWatchRegistration
    • 封装成Packet对象,发服务端发送request
    • 收到服务端响应后,将watcher注册到ZKWatcherManager中进行管理。
    • 请求返回,完成注册。
  • 服务端处理watcher
    • 服务端接收watcher并存储
    • watcher触发,调用process方法来触发watcher
  • 客户端回调watcher
    • 客户端SendThread线程接收事件通知,交由EventThread线程回调watcher
    • 客户端的watcher机制同样是一次性的,一旦被触发后,该watcher就失效了。

总结:客户端会对某个znode建立一个watcher事件,当该znode发生变化时, 这些客户端会收到Zookeeper的通知,然后客户端可以根据znode变化来做出业务上的改变等。

<a id="tzjztd>Zookeeper 通知机制的特点</a>

  • 一次性触发数据发生改变时,一个watcher event会被发送到客户端,但是客户端只会收到一次这样的信息。
  • watcher event异步发送watcher的通知事件从服务端发送到客户端是异步的,这就存在一个问题,
    • 不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,
    • 由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视znode发生了变化。
    • 所以我们使用Zookeeper不能期望能够监控到节点每次的变化。
    • Zookeeper只能保证最终的一致性,而无法保证强一致性。
  • 数据监视Zookeeper有数据监视和子数据监视getData()exists()设置数据监视,getchildren()设置了子节点监视。
  • 注册watchergetDataexistsgetChildren
  • 触发watchercreatedeletesetData
  • setData()会触发znode上设置的data watch(如果set成功的话)。
    • 一个成功的create()操作会触发被创建的znode上的数据watch,以及其父节点上的child watch
    • 一个成功的delete()操作将会同时触发一个znodedata watchchild watch(因为这样就没有子节点了),同时也会触发其父节点的child watch
  • 当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。
    • 当与一个服务器失去连接的时候,是无法接收到watch的。而当客户端重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。
    • 有在一个特殊情况下,watch可能会丢失:对于一个未创建 的znodeexist watch,如果在客户端断开连接期间被创建了, 并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。
  • watch是轻量级的,其实就是本地JVMCallback,服务器端只是存了是否有设置了watcher的布尔类型。

<a id="znode>Zookeeper节点ZNode和相关属性</a>

ZNode有两种类型 :

  • 持久的(PERSISTENT):客户端和服务器端断开连接后,创建的节点不删除(默认)。
  • 短暂的(EPHEMERAL):客户端和服务器端断开连接后,创建的节点自己删除。

ZNode有四种形式:

  • PERSISTENT-持久节点
    • 客户端与Zookeeper断开连接后,除非手动删除,否则节点一直存在于Zookeeper
  • PERSISTENT_SEQUENTIAL-持久顺序节点
    • 基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
  • EPHEMERAL-临时节点
    • 临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与Zookeeper连接断开不一定会话失效), 那么这个客户端创建的所有临时节点都会被移除。
  • EPHEMERAL_SEQUENTIAL-临时顺序节点
    • 基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

创建ZNode时设置顺序标识,ZNode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护。

节点属性

znode节点不仅可以存储数据,还有一些其他特别的属性。

节点属性 注解
cZxid 该数据节点被创建时的事务Id
ctime 该数据节点创建时间
mZxid 该数据节点被修改时最新的事物Id
mtime 该数据节点最后修改时间
pZxid 当前节点的父级节点事务Id
cversion 子节点版本号(子节点修改次数,每修改一次值+1递增)
dataVersion 当前节点版本号(每修改一次值+1递增)
aclVersion 当前节点acl版本号(节点被修改acl权限,每修改一次值+1递增)
ephemeralOwner 临时节点标示,当前节点如果是临时节点,则存储的创建者的会话id(sessionId),如果不是,那么值=0
dataLength 当前节点所存储的数据长度
numChildren 当前节点下子节点的个数

<a id="jqjs>Zookeeper 集群中的角色</a>

Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种

  • Leader
    • 一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各FollowerObserver间的心跳。
    • 所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。 只要有超过半数节点(不包括observer节点)写入成功,该写请求就会被提交(类2PC协议)。
  • Follower

    • 一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳,
    • Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。
  • Observer

    • Follower类似,但是无投票权。
    • Zookeeper需保证高可用和强一致性,为了支持更多的客户端,需要增加更多Server
      • Server增多,投票阶段延迟增大,影响性能
    • 引入ObserverObserver不参与投票
      • Observers接受客户端的连接,并将写请求转发给leader节点
      • 加入更多Observer节点,提高伸缩性,同时不影响吞吐率。

<a id="gzzt>Zookeeper 集群中Server工作状态</a>

  • LOOKING
    • 寻找Leader状态;当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态
  • FOLLOWING
    • 跟随者状态;表明当前服务器角色是Follower
  • LEADING
    • 领导者状态;表明当前服务器角色是Leader
  • OBSERVING
    • 观察者状态;表明当前服务器角色是Observer

<a id="fwqtx>Zookeeper 集群中服务器之间通信</a>

Leader服务器会和每一个Follower/Observer服务器都建立TCP连接, 同时为每个Follower/Observer都创建一个叫做LearnerHandler的实体。

LearnerHandler主要负责LeaderFollower/Observer之间的网络通讯,包括数据同步,请求转发和proposal提议的投票等。

Leader服务器保存了所有Follower/ObserverLearnerHandler

<a id="zab>ZAB 协议</a>

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。 实现这个机制的协议叫做ZAB协议,它也是分布式共识算法的一种。

ZAB协议有两种模式,它们分别是恢复模式和广播模式。

<a id="zxid>事务编号Zxid(事务请求计数器 + epoch)</a>

ZAB(ZooKeeper Atomic BroadcastZooKeeper原子消息广播协议)协议的事务编号Zxid设计中, Zxid是一个64位的数字,其中低32位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加1。 而高32位则代表Leader周期epoch的编号,每个当选产生一个新的Leader服务器, 就会从这个Leader服务器上取出其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1, 以此作为新的epoch,并将低32位从0开始计数。

ZxidTransaction id)类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal(提议)ID。 为了保证顺序性,该id必须单调递增。

<a id="epoch>epoch</a>

epoch:可以理解为当前集群所处的年代或者周期,每个Leader就像皇帝,都有自己的年号, 所以每次改朝换代,leader变更之后,都会在前一个年代的基础上加1。 这样就算旧的Leader崩溃恢复之后,也没有人听他的了,因为Follower只听从当前年代的Leader的命令。

<a id="lzms>ZAB 协议有两种模式-恢复模式(选主)、广播模式(同步)</a>

ZAB协议有两种模式,它们分别是恢复模式(选主)广播模式(同步)。 当服务启动或者在领导者崩溃后,ZAB就进入了恢复模式,当领导者被选举出来, 且大多数Server完成了和Leader的状态同步以后,恢复模式就结束了。 状态同步保证了LeaderServer具有相同的系统状态。

<a id="xyjd>ZAB协议4阶段</a>

  • Leader election(选举阶段-选出准Leader
    • 节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准Leader
    • 只有到达广播阶段(broadcast)准leader才会成为真正的leader
    • 这一阶段的目的是就是为了选出一个准leader,然后进入下一个阶段。
  • Discovery(发现阶段-接受提议、生成epoch、接受epoch
    • 在这个阶段,Followers跟准Leader进行通信,同步Followers最近接收的事务提议。
    • 这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准Leader生成新的epoch, 让Followers接受,更新它们的accepted Epoch
    • 一个Follower只会连接一个Leader,如果有一个节点f认为另一个Follower pLeaderf在尝试连接p时会被拒绝,f被拒绝之后,就会进入重新选举阶段。
  • Synchronization(同步阶段-同步Follower副本)
    • 同步阶段主要是利用Leader前一阶段获得的最新提议历史,同步集群中所有的副本。
    • 只有当大多数节点都同步完成,准Leader才会成为真正的Leader
    • Follower只会接收Zxid比自己的lastZxid大的提议。
  • Broadcast(广播阶段-Leader消息广播)
    • 到了这个阶段,Zookeeper集群才能正式对外提供事务服务,并且Leader可以进行消息广播。
    • 同时如果有新的节点加入,还需要对新节点进行同步。

ZAB提交事务并不像2PC一样需要全部FollowerACK,只需要得到超过半数的节点的ACK就可以了。

<a id="javasx>ZAB协议JAVA实现(FLE-发现阶段和同步合并为Recovery Phase(恢复阶段))</a>

协议的Java版本实现跟上面的定义有些不同,选举阶段使用的是Fast Leader Election(FLE),它包含了选举的发现职责。

因为FLE会选举拥有最新提议历史的节点作为Leader,这样就省去了发现最新提议的步骤。

实际的实现将发现阶段同步合并为Recovery Phase(恢复阶段)。

所以,ZAB的实现只有三个阶段:Fast Leader ElectionRecovery PhaseBroadcast Phase

<a id="tpjz>投票机制</a>

每个Server首先给自己投票,然后用自己的选票和其他Server选票对比,权重大的胜出,使用权重较大的更新自身选票箱。

选举过程如下:

  • 每个Server启动以后都询问其它的Server它要投票给谁。对于其他Server的询问, Server每次根据自己的状态都回复自己推荐的Leaderid和上一次处理事务的Zxid(系统启动时每个Server都会推荐自己)
  • 收到所有Server回复以后,就计算出Zxid最大的哪个Server,并将这个Server相关信息设置成下一次要投票的Server
  • 计算这过程中获得票数最多的的Server为获胜者,如果获胜者的票数超过半数,则改Server被选为Leader。 否则,继续这个过程,直到Leader被选举出来
  • 选举出来后,Leader就会开始等待Server的连接
  • Follower连接Leader,将最大的Zxid发送给Leader
  • Leader根据FollowerZxid确定同步点,至此选举阶段完成。
  • 选举阶段完成Leader同步后通知Follower已经成为uptoDate状态
  • Follower收到uptoDate消息后,又可以重新接受客户端的请求进行服务了

假设目前有5台服务器,每台服务器均没有数据,它们的编号分别是12345,按编号依次启动

选择举过程如下:

  • 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking
  • 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出, 但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING
  • 服务器3启动,给自己投票,同时与之前启动的服务器12交换信息, 由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数, 所以服务器3成为领导者,服务器12成为小弟。
  • 服务器4启动,给自己投票,同时与之前启动的服务器123交换信息, 虽然服务器4的编号大,但服务器3已经成为领导者,所以服务器4只能成为小弟。
  • 服务器5启动,同服务器4一样流程,最后成为小弟。

results matching ""

    No results matching ""