zookeeper02-ZooKeeper架构和术语

1、ZooKeeper架构

  • 应用程序通过客户端库调用ZooKeeper。客户端库负责与ZooKeeper服务器的交互
  • 图2-5展示了客户端与服务器端之间的关系。每个客户端导入客户端库之后,就可以与任意一个ZooKeeper节点通信。

  • ZooKeeper服务器有两种运行模式:独立模式((standalone)和仲裁模式(quorum)。
    • 独立模式:只有一个服务器,ZooKeeper状态不会被复制。
    • 仲裁模式:有一组ZooKeeper服务器,我们称之为ZooKeeper集群,它们之间可以进行状态的复制,并一起服务客户端请求。

1.1、ZooKeeper独立模式

  • 独立模式:只有一台服务器,并且不会进行状态复制。

1.2、ZooKeeper仲裁模式

  • 在仲裁模式下,ZooKeeper可以在集群中的所有服务器上复制数据树。但如果让客户端等待每个服务器完成数据保存后再继续,那么延迟问题将是不可接受的。
  • 在公共管理领域,法定人数是指进行一项投票所需的投票人的最少数量。而在ZooKeeper中,它是为了使ZooKeeper能够正常工作而必须运行的且可用的服务器的最小数量。这个数量也是在告知客户端数据已被安全存储之前,必须存储客户端数据的服务器的最小数量
    • 例如,我们一共有5个ZooKeeper服务器,但法定人数为3个,只要任意3个服务器保存了数据,客户端就可以继续,而其他两个服务器最终也将捕获到数据,并保存数据。
  • 选择适当大小的法定人数很重要。法定人数的数量是保证zookeeper集群发生延迟或崩溃,zookeeper服务主动确认的任何更新请求都将持续存在,直到另一个请求更改它。
  • 为了明白这到底是什么意思,让我们先来通过一个例子来看看,如果法定人数太小,将会出现什么问题。假设有5个服务器并设置法定人数为2,如果有两台服务器s1和s2确认它们已经复制了一个创建/z znode的请求,并回复客户端,但是在服务器s1和s2将新的znode复制到其他服务器之前,这两台服务器与其他服务器、客户端发生了长时间的分区隔离,此时ZooKeeper服务的状态仍然正常,因为法定人数为2,而现在还有3个服务器,但这3个服务器将无法发现新的/z znode。因此,对创建节点/z的请求是非持久化的。(如果s1和s2仅是与另外3台服务器发生了网络分区,但与客户端通信正常,此时的ZooKeeper服务就是发生了脑裂
  • 通过使用多数方案,可以避免上面的问题,法定人数要大于所有服务器数量的一半,也就是我们能够容忍小于1/2数量的服务器崩溃
    • 例如,如果我们有5个服务器,我们可以容忍2台服务器崩溃。
  • 服务器的数量并不一定是奇数,但偶数实际上会使系统更加脆弱。(建议使用奇数个服务器)

2、ZooKeeper的术语

  • 很多用于协作的原语常常在很多应用之间共享。因此,设计一个用于协作的服务的方法往往是提供原语列表,暴露出每个原语的实例化调用方法,并直接操作这些实例。比如,我们可以说分布式锁组成了一个重要的原语,暴露出创建(create)、获取(acquire)和释放(release)锁的三个调用方法。
  • ZooKeeper另辟蹊径。ZooKeeper并不直接暴露原语,相反,它暴露了一个类似于文件系统的API(由一些方法组成),使应用程序能够实现它们自己的原语。我们通常使用方法(recipes)来表示这些原语的实现。应用程序调用这些方法来操作和维护ZooKeeper上的多个小型的数据节点,这些节点被称为znode,并采用类似于文件系统的树状层级结构进行管理
  • 图2-1描述了一个znode树的结构,根znode(节点)包含4个子znode(节点),其中三个子znode拥有下一级子znode,叶子znode存储了数据信息。

  • znode如果没有数据常常表达了重要的信息。比如,在主-从模式的例子中,如果/master znode没有数据,表示主-从应用还没有选举出主节点。
  • 而图2-1中涉及的一些其他znode,在主-从模式的配置中非常有用:
    • /workers:其下每个子znode保存了主从系统中一个可用从节点。如图2-1所示,有一个从节点(foot.com:2181)。如果一个从节点变得不可用,它对应znode应该从/workers中移除。
    • /tasks:其下每个子znode保存了所有已经创建并等待从节点执行的任务的信息。主-从应用的客户端在/tasks下添加一个子znode,用来表示一个新任务,并等待执行。
    • /assign:其下每个子znode保存了分配到某个从节点的任务信息,当主节点分配给从节点一个任务,就会在/assign下增加一个znode。

2.1、API概述

  • znode可能包含数据也可能不包含。如果znode包含数据,该数据将以字节数组的形式存储。字节数组的确切格式是特定于每个应用程序,并且ZooKeeper不直接提供解析它的支持。我们可以使用如Protocol Buffers、Thrift、Avro或MessagePack等序列化(Serialization)包来处理保存于znode中的数据格式,不过有些时候,使用UTF-8或ASCI编码的字符串就可以了。
  • ZooKeeper API提供如下操作:
    • create /path data:创建一个名为/path并包含数据的znode(节点)。
    • delete /path:删除/path znode。
    • exists /path:检查/path是否存在。
    • setData /path data:设置/path的数据为data。
    • getData /path:返回/path中的数据。
    • getChildren /path:返回/path下的子znode列表。
  • 需要注意的是,ZooKeeper不允许对znode中保存的数据进行部分读写。当设置或读取znode中的数据时,znode中的内容将被完全替换或读取。
  • ZooKeeper客户端要连接到ZooKeeper服务,是通过API调用来建立会话(session)。

2.2、不同类型的znode

  • 创建znode时,还需要指定该znode的类型(mode),类型决定了znode的行为方式。
  • znode一共有4种类型:持久的(persistent)、临时的(ephemeral)、持久有序的(persistent-sequential)和临时有序的(ephemeralsequential)。

2.2.1、持久节点和临时节点

  • znode可以是持久的(persistent),也可以是临时的(ephemeral)。
    • 持久的znode,只能通过调用delete来进行删除。
    • 临时的znode,当创建该znode的客户端崩溃或关闭了与ZooKeeper的连接时,这个znode就会被删除。
  • 持久znode是非常有用的,可以通过持久znode为应用保存一些数据,即这个znode的创建者不再属于应用系统时,这个znode的数据也可以保存下来而不丢失。例如,在主-从模式中,即使分配任务的主节点已经崩溃了,也需要保存从节点的任务分配情况。
  • 临时znode传递关于应用程序的某些方面的信息,这些信息只在其创建者的会话有效时才存在。例如,在主-从模式中,当一个节点创建一个名为/master的临时znode时,该znode的存在意味着系统现在有一个主节点了,且该主节点处于正常运行中。如果主节点崩溃后,该znode仍然存在,那么系统将无法监测到主节点已经崩溃。因此这个znode需要和主节点一起消失,这样就可以阻止系统继续运行。从节点也可以使用临时的znode,如果一个从节点失效,它的会话将会过期,并且它在/workers中的子znode将自动消失。
  • 一个临时znode,在以下两种情况下将会被删除:
    • 当创建该znode的客户端的会话因超时或主动关闭而中止时。
    • 当某个客户端(不一定是创建者)主动删除该znode时。
  • 因为临时的znode在其创建者的会话过期时被删除,所以我们现在不允许临时znode拥有子znode

2.2.2、有序节点

  • znode还可以设置为有序的(sequential) 。一个有序znode被分配一个唯一的单调递增的整数。当创建有序znode时,一个序号会被追加到路径之后。例如,如果一个客户端创建了一个有序znode,其路径为/tasks/task-,那么Zookeeper将会为其分配一个数字序号并追加到其路径之后,例如1,最后该znode为/tasks/task-1。
  • 通过这种简单的方式,为有序znode提供唯一的名称,同时也可以直观地查看有序znode的创建顺序。

2.3、监视与通知

  • 因为ZooKeeper通常是作为远程服务被访问,所以客户端每次访问znode时,客户端都需要获得znode中的内容,这样的代价就非常大:这会导致更高的延迟和更多的操作。以图2-2为例,第二次调用getChildren /tasks,返回了相同的值(一个空的集合),其实这是没有必要的。

  • 为了取代客户端轮询,我们选择了基于通知(notification)的机制:客户端向ZooKeeper注册监视znode变化的监视点(watch),znode有任何变化就会触发监视点,然后通知客户端监视点是一个单次触发的操作,即一个监视点只会触发一个通知。为了接收多个通知,客户端必须在每次收到通知后重新设置一个监视点。如图2-3所示,当节点/tasks发生变化时,客户端会收到一个通知,并从ZooKeeper读取一个新值。

  • 因为监视点是单次触发操作,所以在客户端接收到关于znode的通知和设置新监视之间,znode可能会发生新的更改(但不要担心,您不会错过对状态的更改)。
  • 让我们看一个简单的例子,看看它是如何工作的。假设以下事件按此顺序发生:
    • (1)客户端C1设置监视点来监控/tasks数据的变化。
    • (2)客户端C2连接后,向/tasks中添加了一个新的任务。
    • (3)客户端C1接收到/tasks有变化的通知。
    • (4)客户端C1设置新的监视点,在设置完成前,第三个客户端C3连接后,向/tasks中添加了一个新的任务。
      • 客户端C1最终设置了新的监视点,但是C3所做的更改没有触发任何通知。为了观察C3所做的变更,实际上在C1设置新的监视点前需要读取/tasks节点的状态,通过在设置监视点前读取ZooKeeper的状态,C1就不会错过任何变更。
  • 通知机制的一个重要保障是,在对同一个znode进行任何其他更改之前,将先通知客户端
    • 例如,如果客户端对一个znode设置了监视点,并且该znode发生了两个连续更新。在第一次更新后,第二次更新前,客户端将收到通知,然后读取znode中的数据。(即在第一次变更后,会先通知客户端,然后再做第二次变更
    • 我们要满足的关键点是保证客户端观察到更新顺序。尽管ZooKeeper状态的变化最终会传播到其他给定的客户端(不论快慢),但我们保证客户端可以根据全局顺序观察到ZooKeeper状态的变化
  • ZooKeeper可以定义不同类型的监视点,不同类型的监视点有不同的通知。
    • 客户端可以设置多种监视点,如监视znode的数据变化、监视znode的子znode的变化、监视znode的创建或删除。
    • 为了设置监视点,可以使用API中的任何方法来读取Zookeeper的状态,在使用这些API方法时,传入一个watcher对象或使用默认的watcher对象。

2.4、znode的版本号

  • 每一个znode都有一个版本号,该版本号随着每次数据更改而递增
  • API中的两个操作是有条件地执行:setData和delete
    • 这两个调用都以znode的版本号作为参数,只有当客户端传递的版本与服务器上的版本号一致时,调用才会成功。
    • 当多个ZooKeeper客户端对同一个znode进行操作时,版本的使用就会显得尤为重要。
      • 例如,假设客户端C1对/config写入了一些配置信息,如果另一个客户端C2同时更新这个znode,此时C1的版本号已经过期,C1调用setData一定不会成功。
  • C1使用的版本不匹配,操作失败,如图2-4所示。

2.5、会话

  • 会话的概念对于ZooKeeper的运行是非常重要。
  • 在对ZooKeeper执行任何请求之前,客户端必须与Zookeeper服务器建立会话。客户端提交给ZooKeeper的所有操作都会关联到一个会话。当会话因某种原因结束时,在该会话期间创建的临时节点将消失
  • 当客户端使用特定的语言绑定ZooKeeper服务器创建一个句柄时,它会与ZooKeeper服务器建立一个会话。客户端最初连接到集群中的任意一台服务器,并且只连接到一个服务器。它使用TCP连接与ZooKeeper服务器通信,但如果客户端有一段时间没有收到来自当前ZooKeeper服务器的消息,会话可能会移动到另一个ZooKeeper服务器上。将会话移动到不同的服务器是由ZooKeeper客户端库透明地处理的
  • 会话提供顺序保证,这意味着会话中的请求是按照FIFO(先进先出)顺序执行的。通常,客户端只有一个打开的会话,所以它的所有请求都是按照FIFO顺序执行的如果客户端有多个并发会话,那么FIFO顺序在多个会话之间未必能够保持。如果同一客户端有多个会话,就算这些会话分别在不同的时间进行请求,也不一定保持FIFO顺序。在这种情况下,它是如何发生的:
    • (1)客户端建立一个会话,并通过两个连续的异步调用来创建/tasks和/workers。
    • (2)第一个会话到期。
    • (3)客户端建立另一个会话,并通过异步调用来创建/assign。
    • 在这个调用序列中,可能只创建了/tasks和/assign,这是因为第一个会话保持了FIFO顺序,但在跨会话时就违反了FIFO顺序。

2.6、会话的状态和周期

2.6.1、会话状态转换

  • 会话的生命周期是指会话从创建到结束之间的时间,无论它是正常关闭还是由于超时而过期。为了讨论会话中发生的事情,我们需要考虑会话可能处于的状态以及可能改变其状态的事件。
  • 会话的主要状态:CONNECTING、CONNECTED、CLOSED和NOT_CONNECTED。状态的转换依赖于发生在客户端与服务之间的各种事件(图2-6)。

  • 当要开始一个ZooKeeper会话时,会话从NOT_CONNECTED状态开始,并过渡到CONNECTING(图2-6中的箭头1)。正常情况下,与ZooKeeper服务器连接成功,会话转换到CONNECTED状态(箭头2)。当客户端与ZooKeeper服务器断开连接或无法收到服务器的响应时,会话就会转换到CONNECTED状态(箭头3)并尝试寻找其他ZooKeeper服务器。如果它能够找到另一个服务器或重新连接到原来的服务器,一旦服务器确认会话仍然有效,会话就会转换到CONNECTED状态。否则,服务器将声明会话过期,会话转换到CLOSED状态(箭头4)。应用程序也可以显式关闭会话(箭头4和5)。
  • 注意,发生网络分区时等待CONNECTING
    • 当客户端因超时断开与服务器连接时,客户端仍处于“CONNECTING”状态。如果因网络分区问题导致客户端与Zookeeper集群隔离而发生连接断开时,它将一直保持这种状态,直到显式地关闭这个会话,或者分区恢复后,客户端从ZooKeeper服务器获悉会话已经过期。
    • 发生这种行为是因为ZooKeeper集群负责声明会话是否过期,而不是客户端。直到客户端获悉一个ZooKeeper会话已经过期,客户端才能声明该会话已经过期。然而,客户端可以选择关闭会话。

2.6.2、会话重连

  • 创建会话时需要设置一个重要的参数是会话超时时间,即ZooKeeper服务允许会话超时之前存在的时间。
    • 如果经过时间T之后,ZooKeeper服务接收不到这个会话的任何消息,ZooKeeper服务就会声明会话过期。
    • 在客户端,如果它在1/3 T时没有收到来自服务器的任何消息,它就向服务器发送一个心跳消息。在2/3 T处,客户端开始寻找一个不同的服务器,而此时它还有1/3 T的时间去寻找。
  • 注意,客户端会尝试连接到哪一个ZooKeeper服务器?
    • 在仲裁模式下,客户端有多个服务器可以连接,而在独立模式下,它必须尝试重新连接到可用的单个服务器。在仲裁模式中,应用程序需要传递可用的服务器列表给客户端,客户端选择一个进行连接。
  • 当尝试连接到一个不同的服务器时,重要的是这个服务器的ZooKeeper状态至少要和客户端最后连接的服务器的ZooKeeper状态一致客户端不能连接到这样的服务器:客户端已经发现的更新而服务器却未发现。
  • ZooKeeper通过在服务中排序更新来决定状态是否最新。ZooKeeper中的每一个状态的变化都与其他所有已经执行的更新完全一致。在ZooKeeper中,根据更新的顺序分配事务标识符。因此,如果一个客户端已经更新到了i(事务标识符),那么这个客户端就不能连接到只更新到i'(i'<i)的服务器上。
  • 图2-7描述了在重连情况下事务标识符(zkid)的使用。当客户端因超时与S1断开连接后,客户端开始尝试连接S2,但延迟于客户端所知的变化。S3对这个变化的情况与客户端保持一致,所以S3可以安全连接。

2.7、实现一个原语:通过ZooKeeper实现锁

  • 关于ZooKeeper的功能,一个简单的例子就是通过锁来实现临界区域。我们知道锁有很多形式(如:读/写锁、全局锁),通过ZooKeeper来实现锁也有多种方式。这里讨论一个简单的方式来说明应用中如何使用ZooKeeper,我们不再考虑其他形式的锁。
  • 假设有一个应用程序由n个进程组成,这些进程尝试获取一个锁。再次强调,ZooKeeper并未直接暴露原语,因此我们使用ZooKeeper的接口来管理znode,并以此来实现锁。为了获得一个锁,进程p尝试创建一个名为/lock的znode,如果进程p成功创建了znode,就表示它获得了锁并可以继续执行其临界区域的代码。不过一个潜在的问题是进程p可能崩溃,导致这个锁永远无法释放。在这种情况下,没有任何其他进程可以再次获得这个锁,整个系统可能因死锁而崩溃。为了避免这种情况,我们不得不在创建这个节点时指定/lock为临时znode。
  • 其他进程将因这个znode存在而创建/lock失败。每个监听/lock变化的进程,在检测到/lock已被删除时将尝试创建znode来获得锁。例如,当进程pʹ收到/lock被删除的通知时,它将尝试创建/lock试图获取锁,如果其他进程已经创建了,就继续监听znode。
#                                                                                                                     #
posted @ 2021-12-18 23:00  麦恒  阅读(41)  评论(0编辑  收藏  举报