首页 > BigData, 技术,让世界更美好。 > 【zookeeper】zookeeper知识小结

【zookeeper】zookeeper知识小结

2018年3月5日

1.前言

 

  最近想整理下自己的大数据生态知识点,就先从zookeeper开始。说实话,我并没有使用zookeeper进行开发(如果把spark streamning 的kafka的offset存进zookeeper也算的话)。但是zookeeper作为大数据生态的中要的一员,自己负责搭建和维护大数据集群,多多少少都绕不过zookeeper。

2.zookeeper是什么?

 

  我不喜欢用比喻,因为比喻虽然让人更容易解释新的知识和形象,但比喻也会损失一部分的信息不利于理解本质,所以这里就不用动物管理员那套说法了。

官方的说法是:分布式应用程序的分布式协调服务(A Distributed Coordination Service for Distributed Applications),zookeeper它为分布式应用程序提供同步、配置维护以及组和命名功能,应用程序通过zookeeper实现更高级的服务。

zookeeper的目的是为了减轻分布式服务需要重头做分布式服务的的责任(重复发明轮子)。

  究其本质,zookeeper是一套通知机制 + 文件系统。

  • 通知机制
    • 当zookeeper里的数据发生变化,zookeeper主动告知相应的客户端,从而让客户端做出应对。
    • 当客户端和zookeeper断开的时候,客户端也能收到本地的通知。
  • 文件系统
    • zookeeper维护一个分布式的文件系统,这里注意这个文件系统与我们传统的目录和文件组成的文件系统不同,zookeeper的文件系统中,每个文件都是目录,每个目录也都是文件,叫做znode。
    • zookeeper维护znode的基本数据结构(版本号,ACL更改和时间戳),znode是以原子的方式读取和写入的。
    • znode有一种临时znode,只有当创建znode的会话在存在的时候才能生存,会话结束,znode就被删除。这在实现某些分布式机制的时候很有用。

zookeeper可以保证:

  • 顺序一致性 – 客户端的更新将按照它们发送的顺序进行应用。
  • 原子性 – 更新成功或失败。没有部分结果。
  • 单系统映像 – 无论服务器连接到哪个服务器,客户端都会看到相同的服务视图。
  • 可靠性 – 一旦应用更新,它将一直持续到客户覆盖更新为止。
  • 及时性 – 系统的客户观点在一定的时间范围内保证是最新的。

3.zookeeper可以做什么?

zookeeper文件系统提供的api接口非常简单(create、delete、exists、get data、set data、get children和sync),但是我们可以通过其和通知机制实现很多高级的服务。

  • 命名服务:zookeeper可以很容易产生便于记忆和全局唯一的名称,用于服务命名。比如成功创建了一个节点,zookeeper保证其路径+名称是唯一的,客户端可以通过订阅其路径,获取服务列表。
  • 配置管理:保证zookeeper里存储的全局配置统一变更,而且zookeeper会利用其通知机制主动通知订阅相关配置的客户端。
  • 集群管理:也就是选举master,得益于zookeeper在整个分布式系统上可以做到强一致性,可以做到保证master唯一。
  • 队列管理:可以用于实现分布式队列,比如同步队列(通过watch所有成员,当到齐后通知客户端)和先进先出队列(数据带有编号的znode,客户端消费完后删除znode)。
  • 分布式锁
    • 排它锁:成功创建临时节点的客户端获得锁以执行操作,其他客户端只能watch锁情况,当锁释放后,其他客户端再去视图创建临时节点以获得锁后执行操作。重复以上流程
    • 共享锁:每个客户端都可以建立临时节点建立共享锁,以进行共享操作(比如读数据),只有当所有共享锁都被释放后才能有客户端加排它锁。

4.zookeeper的核心原理

 

如图,zookeeper由若干的server组成,其中有一台server是leader,其他的为flower。客户端可以连接一台server进行各种操作,当server的连接中断,客户端会连接到不同的server。

 

4.1 数据模型

 

ZooKeeper具有层级名称空间。唯一的区别是名称空间中的每个节点都可以有与其关联的数据以及子对象。这就像有一个文件系统,允许一个文件也是一个目录。路径通常表示为规范的,绝对的,斜线分隔的路径;

 

  • ZNodes
    • ZooKeeper树中的每个节点都称为 znodeZnodes维护一个stat结构,其中包含数据更改的版本号,acl更改。统计结构也有时间戳。版本号和时间戳允许ZooKeeper验证缓存并协调更新。每次znode的数据更改时,版本号都会增加。

 

    • 客户可以在znode上设置watches。对该znode的更改会触发watches,然后清除watches。当watches触发时,ZooKeeper会向客户端发送通知。

 

    • 储在名称空间中每个节点上的数据都是以原子方式读取和写入的。读取获取与znode关联的所有数据字节,写入将替换所有数据。每个节点都有一个访问控制列表(ACL),限制谁可以做什么。

 

    • ZooKeeper也有临时节点的概念。只要创建znode的会话处于活动状态,就会存在这些znode。当会话结束时,znode被删除。由于这种行为,短暂的znodes不允许有children。

 

    • 当创建一个znode时,你也可以请求ZooKeeper将一个单调递增的计数器附加到路径的末尾。该计数器对父节点znode是唯一的。计数器的格式为%010d – 即10个数字和0(零)填充(计数器以这种方式进行格式化以简化排序),即“<path> 0000000001”。注意:用于存储下一个序列号的计数器是由父节点维护的带符号整型(4字节),计数器在增加到2147483647之后会溢出(导致名称为“<path> -2147483647”)。

 

  • zookeeper的时间序列
    • Zxid
      • 每次接受对ZooKeeper状态的更改都会形成一份zxid(ZooKeeper Transaction Id)的形式的戳。这标识了ZooKeeper所有更改的总序列。每个更改都会有一个唯一的zxid,如果zxid1小于zxid2,那么zxid1发生在zxid2之前。
    • 版本号
      • 对节点的每次更改都会导致该节点的某个版本号增加。这三个版本号是version(对znode数据的更改次数),cversion(对znode子级的更改次数)以及aversion(对znode ACL的更改次数)。
    • Ticks
      • 当使用多服务器ZooKeeper时,服务器使用Tick定义事件的时间,例如状态上传,会话超时,对等体之间的连接超时等。Tick时间只通过最小会话超时间隔(2倍的滴答时间)间接暴露; 如果客户端请求的会话超时低于最小会话超时,则服务器将通知客户端会话超时实际上是最小会话超时。
    • 真实时间
      • 除了在znode创建和znode修改中将时间戳放入stat结构中之外,ZooKeeper根本不使用实时或时钟时间。

 

 

4.2 Session

 

ZooKeeper客户端建立与ZooKeeper服务的会话。一旦创建,句柄就会以CONNECTING状态开始,并且客户端库尝试连接到构成ZooKeeper服务的服务器之一,此时它将切换到CONNECTED状态。在正常操作过程中将处于这两种状态之一。如果发生不可恢复的错误,例如会话过期或身份验证失败,或者应用程序明确关闭了句柄,则句柄将移至CLOSED状态。

要创建客户端会话,应用程序代码必须提供一个连接字符串,其中包含逗号分隔的host:port对列表,每个对应于一个ZooKeeper服务器(例如“127.0.0.1:4545”或“127.0.0.1:3000,127.0.0.1 :3001,127.0.0.1:3002″ )。ZooKeeper客户端库将选择一个任意的服务器并尝试连接到它。如果此连接失败,或者客户端因任何原因与服务器断开连接,则客户端将自动尝试列表中的下一台服务器,直到(重新)建立连接。

 

4.3 Watches

 

ZooKeeper中的所有读取操作(getData(),getChildren()和exists())都可以选择将Watches设置为触发器。这里是ZooKeeper对的Watches定义:Watches事件是当Watches设置的数据发生变化时,一次性触发,发送给设置Watches的客户端。在手表的这个定义中要考虑三个要点:

  • 一次触发

数据发生变化时,一个Watches事件将发送给客户端。例如,如果客户端执行getData(“/ znode1”,true),并且稍后更改或删除/ znode1的数据,则客户端将获得/ znode1的监视事件。如果/ znode1再次发生变化,除非客户端进行了另一次设置新Watches的读取操作,否则不会发送监视事件。

  • 发送给客户端

这意味着一个事件正在前往客户端的途中,但在变更操作的成功返回代码到达发起更改的客户端之前,可能无法到达客户端。Watches异步发送给观察者。ZooKeeper提供了一个订购保证:客户端永远不会看到其设置了Watches的变化,直到它第一次看到Watches事件。网络延迟或其他因素可能导致不同的客户端在不同时间查看Watches并返回更新代码。关键是,不同客户端看到的一切都会有一致的顺序。

  • Watches设置的数据

这指的是节点可以改变的不同方式。它有助于将ZooKeeper视为维护两个手表列表:数据手表和儿童手表。getData()和exists()设置数据手表。getChildren()设置小孩手表。另外,根据返回的数据类型,可能会考虑设置手表。getData()和exists()返回有关节点数据的信息,而getChildren()返回一个子节点列表。因此,setData()将触发被设置的znode的数据监视(假设该设置成功)。一个成功的create()将触发一个数据监视,以便为正在创建的znode和一个父节点的子表监视。

手表在本地保存在客户端连接的ZooKeeper服务器上。这可以让手表重量轻,便于设置,维护和派送。当客户端连接到新的服务器时,手表将触发任何会话事件。与服务器断开连接时手表不会收到。当客户重新连接时,任何先前注册的手表将被重新注册并在需要时触发。一般而言,这一切都是透明的。有一种情况可能会遗漏一块手表:如果znode在断开连接时被创建并删除,则表示尚未创建的znode的存在将被忽略。

5.zookeeper的leader选举

在了解leader选举之前我们先了解几个zookeeper概念:

  • myid

每个 ZooKeeper 服务器的唯一标识符,通常从 1 开始

  • zxid

用于标识一次更新操作的Proposal ID。为了保证顺序性,该zkid必须单调递增。因此Zookeeper使用一个64位的数来表示,高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。低32位为该epoch内的序号,每次epoch变化,都将低32位的序号重置。这样保证了zkid的全局递增性。

  • 服务器状态
    • LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举
    • FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁
    • LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳
    • OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票
  • 选票数据结构

每个服务器在进行领导选举时,会发送如下关键信息:

logicClock 每个服务器会维护一个自增的整数,名为logicClock,它表示这是该服务器发起的第多少轮投票
state 当前服务器的状态
self_id 当前服务器的myid
self_zxid 当前服务器上所保存的数据的最大zxid
vote_id 被推举的服务器的myid
vote_zxid 被推举的服务器上所保存的数据的最大zxid

 

zookeeper的leader选举有两种算法:LeaderElection和FastLeaderElection(默认),这里只介绍FastLeaderElection。

  • FastLeaderElection

简单来说:每一个节点,相当于一个选民,他们都有自己的推荐人,最开始他们都推荐自己。谁更适合成为Leader有一个简单的规则,例如myid够大(配置)、持有的数据够新(zxid够大)。每个选民都告诉其他选民自己目前的推荐人是谁,类似于出去搞宣传拉拢其他选民。每一个选民发现有比自己更适合的人时就转而推荐这个更适合的人。最后,大部分人意见一致时,就可以结束选举。

投票流程

  • 自增选举轮次
    • Zookeeper规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的logicClock进行自增操作。
  • 初始化选票
    • 每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。例:服务器2投票给服务器3,服务器3投票给服务器1,则服务器1的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。
  • 发送初始化选票
    • 每个服务器最开始都是通过广播把票投给自己。
  • 接收外部投票
    • 服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。
  • 判断选举轮次
    •  收到外部投票后,首先会根据投票信息中所包含的logicClock来进行不同处理
      • 外部投票的logicClock大于自己的logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的logicClock更新为收到的logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。
      • 外部投票的logicClock小于自己的logicClock。当前服务器直接忽略该投票,继续处理下一个投票。
      • 外部投票的logickClock与自己的相等。当时进行选票PK。
  • 选票PK
    • 选票PK是基于(self_id, self_zxid)与(vote_id, vote_zxid)的对比
      • 外部投票的logicClock大于自己的logicClock,则将自己的logicClock及自己的选票的logicClock变更为收到的logicClock
      • 若logicClock一致,则对比二者的vote_zxid,若外部投票的vote_zxid比较大,则将自己的票中的vote_zxid与vote_myid更新为收到的票中的vote_zxid与vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖
      • 若二者vote_zxid一致,则比较二者的vote_myid,若外部投票的vote_myid比较大,则将自己的票中的vote_myid更新为收到的票中的vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱
  • 统计选票
    • 如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。
  • 更新服务器状态
    • 投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING

 

 

 

本文的评论功能被关闭了.