Hadoop教程

ZooKeeper 服务

ZooKeeper是一个具有高可用性的髙性能协调服务。在本节中,我们将从三个方面 来了解这个服务:模型、操作和实现。

数据模型

ZooKeeper维护着一个树形层次结构,树中的节点被称为znode。znode可以用于 存储数据,并且有一个与之相关联的ACL。ZooKeeper被设计用来实现协调服务 (通常使用小数据文件),而不是用于大容量数据存储,因此一个znode能存储的数 据被限制在1MB以内。

ZooKeeper的数据访问具有原子性。客户端在读取一个znode的数据时,要么读到 所有的数据,要么读操作失败,不会只读到部分数据。同样,写操作将替换znode 存储的所有数据。ZooKeeper会保证写操作不成功就失败,不会出现部分写之类的 情况,也就是不会出现只保存客户端所写部分数据的情况。ZooKeeper不支持添加 操作。这些特征都是与HDFS所不同的。HDFS被设计用于大容量数据存储,支持 流式数据访问和添加操作。

znode通过路径被引用。像Unix中的文件系统路径一样,在ZooKeeper中路径被 表示成用斜杠分割的Unicode字符串。与Unix中的文件系统路径不同,ZooKeeper 中的路径必须是绝对路径,也就是说每条路径必须从一个斜杠字符开始。此外,所 有的路径表示必须是规范的,即每条路径只有唯一的一种表示方式,不支持路径解 析。例如,在Unix中,一个具有路径/a/b的文件也可以通过路径/a/./b化来表示,原 因在于“.”在Unix的路径中表示当前目录(“..”表示当前目录的上一级目录)。 在ZooKeeper中,“•”不具有这种特殊含义,这样表示的路径名是不合法的。

在ZooKeeper中,路径由Unicode字符串构成,并且有一些限制(见ZooKeeper的 '参考文档)。字符串“zookeeper”是一个保留词,不能将它用作一个路径组件。需 要特别指出的是,ZooKeeper 使用/zooAeeper子树来保存管理信息,比如关于配额 的信息。

注意,ZooKeeper的路径与URI不同,前者在Java API中通过 java.lang.String来使用,而后者是通过Hadoop Path类(或java.net.URI)来使用。

znode有一些性质非常适合用于构建分布式应用,我们将在接下来的几个小节中进 行讨论。

短暂znode

znode有两种类型:短暂的和持久的。znode的类型在创建时确定并且之后不能再 修改。在创建短暂znode的客户端会话结束时,ZooKeeper会将该短暂znode删 除。相比之下,持久znode不依赖于客户端会话,只有当客户端(不一定是创建它 的那个客户端)明确要删除该持久znode时才会被删除。短暂znode不可以有子节 点,即使是短暂子节点。

虽然每个短暂znode都会被绑定到一个客户端会话,但它们对所有的客户端还是可 见的(当然,还是要符合其ACL的定义)。

对于那些需要知道特定时刻有哪些分布式资源可用的应用来说,使用短暂znode是 一种理想的选择。本章前面的例子就使用短暂znode来实现一个组成员管理服务, 让任何进程都知道在特定的时刻有哪些组成员可用。

顺序号

顺序(sequential)znode是指名称中包含ZooKeeper指定顺序号的znode。如果在创 建znode时设置了顺序标识,那么该znode名称之后便会附加一个值,这个值是由 一个单调递增的计数器(由父节点维护)所添加的。

观察

znode以某种方式发生变化时,“观察”(watch)机制可以让客户端得到通知。可以 针对ZooKeeper服务的操作来设置观察,该服务的其他操作可以触发观察。例如, 客户端可以对一个znode调用exists操作,同时在它上面设定一个观察。如果这 个znode不存在,则客户端所调用的exists操作将会返回false。如果一段时间 之后,另外一个客户端创建了这个znode,则这个观察会被触发,通知前一个客户 端这个znode被创建。在下一小节中,将完整介绍哪些操作会触发其他操作。

操作

如表14-1所示,ZooKeeper中有9 种基本操作。

表14-1.ZooKeeper服务的操作

操作 描述
create 创建一个znode(必须要有父节点)
delete 删除一个znode(该znode不能有任何子节点)
exists 测试一个znode是否存在并且查询它的元数据
getACL, setACL 获取/设置一个znode的ACL
getChildren 获取一个znode的子节点列表
getData, setData 获取/设置一个znode所保存的数据
sync 将客户端的znode视图与ZooKeeper同步

ZooKeeper中的更新操作是有条件的。在使用delete或setData操作时必须提供 被更新znode的版本号(可以通过exists操作获得)。如果版本号不匹配,则更新 操作会失败。更新操作是非阻塞操作,因此一个更新失败的客户端(由于其他进程 同时在更新同一个znode)可以决定是否重试,或执行其他操作,并且它这样做不会 阻塞其他进程的执行。

虽然ZooKeeper可以被看作是一个文件系统,但出于简单性的需要,有一些文件系 统基本操作被它摒弃了。由于ZooKeeper中的文件较小并且总是整体被读写,因此 没有必要提供打开、关闭或查找操作。

我们已经看过同步执行的化¥3 API。下面是Java API操作的签名,它返回一个封装有znode元数据的Stat对象(如果znode不存在,则为null):

public Stat exists(String path, Watcher watcher) throws KeeperException, InterruptedException

在ZooKeeper类中同样可以找到异步执行的签名,如下所示:

public void exists(String path. Watcher watcher, StatCallback cb, Object ctx)

因为所有异步操作的结果都是通过回调来传送的,因此在Java API中异步方法的 返回类型都是void。调用者传递一个回调的实现,从ZooKeeper接收到响应时, 其方法被调用。在这种情况下,回调的是StatCallback接口,它有以下方法:

public void processResult(int rc, String path,Object ctx, Stat stat);

观察触发器

操作exists、getChildren和getData上可以设置观察,这些观察可以被写操作create、delete和setData触发。ACL相关的操作不参与任何观察。当一个观察被触发时会产生一个观察事件,这个观察和触发它的操作共同决定了观察事件的类型。

  • 当所观察的znode被创建、删除或其数据被更新时,设置在exists操作上的 观察将被触发。
  • 当所观察的znode被删除或其数据被更新时,设置在getData操作上的观察 将被触发。创建znode不会触发getData操作上的观察,因为getData操作 成功执行的前提是znode必须已经存在。
  • 当所观察的znode的一个子节点被创建或删除时,或所观察的znode自己被删 除时,设置在getChildren操作上的观察将会被触发。你可以通过观察事件 的类型来判断被删除的是znode还是其子节点:NodeDelete代表znode被删 除,NodeChildrenChanged代表一个子节点被删除。
  • ACL

    每个znode被创建时都会带有一个ACL列表,用于决定谁可以对它执行何种操作。ACL依赖于ZooKeeper的客户端身份验证机制。ZooKeeper提供了下面几种身份验 证模式。

    digest

    通过用户名和密码来识别客户端。

    host

    通过客户端的主机名(hostname)来识别客户端。

    ip

    通过客户端的IP地址来识别客户端。

    在建立一个ZooKeeper会话之后,客户端可以对自己进行身份验证。虽然znode的 ACL列表会要求所有的客户端是经过验证的,但ZooKeeper的身份验证过程却是 可选的。在这种情况下,客户端必须通过自己对自己进行验证来支持对^0如的访 问。这里有一个使用digest模式(用户名和密码)进行身份验证的例子:

    zk.addAuthInfo("digest", "tom:secret".getBytes());

    每个ACL都是身份验证模式、符合该模式的一个身份和一组权限的组合。例如, 如果我们打算给域example.com下的客户端对某个znode的读权限,可以使用 host模式、example.com的ID和READ权限在该znode上设置一个ACL。在 Java语言中,我们可以使用如下方式来创建这个ACL对象:

    new ACL(Perms.READ, new Id("host", "example.com"));

    表14-3列出了一个完整的权限集合。注意,exists操作并不受ACL权限的限 制,因此任何客户端都可以调用exists来检索一个znode的状态或查询一个 znode是否存在。

    表 14-3. ACL权限

    ACL权限 允许的操作
    CREATE create(子节点)
    READ getChildnen
    getData
    WRITE setData
    DELETE delete(子节点)
    ADMIN setACL

    在类ZooDefs.Ids中有一些预定义的ACL, OPEN_ACL_UNSAFE是其中之一,它 将所有的权限(不包括ADMIN权限)授予每个人。

    此外,ZooKeeper还支持可插入的身份验证机制,如果需要的话,它可以集成第三 方的身份验证系统。


    实现

    ZooKeeper服务有两种不同的运行模式。一种是“独立模式”(standalone mode), 即只有一个ZooKeeper服务器。这种模式较为简单,比较适合于测试环境(甚至可 以在单元测试中采用),但是不能保证高可用性和恢复性。在生产环境中的ZooKeeper通常以“复制模式”运行于一个计算机集群上,这个计 算机集群被称为一个“集合体”(ensemble)。ZooKeeper通过复制来实现髙可用 性,只要集合体中半数以上的机器处于可用状态,它就能够提供服务。例如,在一 个有5个节点的集合体中,任意2台机器出现故障,都可以保证服务继续,因为剩 下的3台机器超过了半数。注意,6个节点的集合体也只能够容忍2台机器出现故 障,因为如果3台机器出现故障,剩下的3台机器没有超过集合体的半数。出于这 个原因,一个集合体通常包含奇数台机器。

    从概念上来说,ZooKeeper是非常简单的:它所做的就是确保对znode树的每一个 修改都会被复制到集合体中超过半数的机器上。如果少于半数的机器出现故障,则 最少有一台机器会保存最新的状态。其余的副本最终也会更新到这个状态。

    然而,这个简单想法的实现却不简单。ZooKeeper使用了Zab协议,该协议包括两 个可以无限重复的阶段。

    阶段1:领导者选举

    集合体中的所有机器通过一个选择过程来选出一台被称为“领导者”(leader) 的机器,其他的机器被称为“跟随者”(follower)。一旦半数以上(或指定数量) 的跟随者已经将其状态与领导者同步,则表明这个阶段已经完成。

    阶段2:原子广播

    所有的写请求都被转发给领导者,再由领导者将更新广播给跟随者。当半数以 上的跟随者已经将修改持久化之后,领导者才会提交这个更新,然后客户端才 会收到一个更新成功的响应。这个用来达成共识的协议被设计成具有原子性, 因此每个修改要么成功要么失败。这类似于数据库中的两阶段提交协议。

    如果领导者出现故障,其余的机器会选出另外一个领导者,并和新的领导者一起继 续提供服务。随后,如果之前的领导者恢复正常,便成为一个跟随者。领导者选举 的过程是非常快的,根据一个已发表的结果来看,只需要大约200毫秒,因此在 领导者选举的过程中不会出现性能的明显降低。

    在更新内存中的znode树之前,集合体中的所有机器都会先将更新写入磁盘。任 何一台机器都可以为读请求提供服务,并且由于读请求只涉及内存检索,因此非常快。


    一致性

    理解ZooKeeper的实现基础有助于理解其服务所提供的一致性保证。对集合体中机 器所使用的术语“领导者”和“跟随着”是恰当的,它们表明了一点,即一个跟随 者可能滞后于领导者几个更新。这也表明了一个事实,在一个修改被提交之前,只 需要集合体中半数以上而非全部机器已经将其持久化。对于ZooKeeper来说,理想 的情况就是将客户端都连接到与领导者状态一致的服务器上。每个客户端都可能被 连接到领导者,但客户端对此无法控制,甚至它自己也无法知道是否连接到领导者。参见图14-2。

    图14-2.跟隨者响应读请求,领导者提交写请求

    在ZooKeeper中设计中,以下几点考虑保证了数据的一致性。

    顺序一致性:
    来自任意特定客户端的更新都会按其发送顺序被提交。也就是说,如果一个客 户端将znode z的值更新为a,在之后的操作中,它又将z的值更新为b,则没 有客户端能够在看到z的值是b之后再看到值a(如果没有其他对z的更新)。

    原子性:
    每个更新要么成功,要么失败。这意味着如果一个更新失败,则不会有客户端 会看到这个更新的结果。

    单一系统映像:一个客户端无论连接到哪一台服务器,它看到的都是同样的系统视图。这意味 着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统 状态不会比在之前服务器上所看到的更老。当一台服务器出现故障,导致它的 一个客户端需要尝试连接集合体中其他的服务器时,所有滞后于故障服务器的 服务器都不会接受该连接请求,除非这些服务器赶上故障服务器。

    持久性:一个更新一旦成功,其结果就会持久存在并且不会被撒销。这表明更新不会受 到服务器故障的影响。

    及时性:
    任何客户端所看到的系统视图的滞后都是有限的,不会超过几十秒。这意味着 与其允许一个客户端看到非常陈旧的数据,还不如将服务器关闭,强迫该客户 端连接到一个状态较新的服务器。

    出于性能的原因,所有的读操作都是从ZooKeeper服务器的内存获得数据,它们不 参与写操作的全局排序。如果客户端之间通过ZooKeeper之外的机制进行通信,则 这个性质可能会导致客户端所看到的ZooKeeper状态是不一致的。

    例如,客户端A将znode z的值从a更新为a’,接着A告诉B去读z的值,而B 读到的值是a而不是a’。这与ZooKeeper的一致性保证是完全兼容的(这种情况称 为“跨客户端视图的同时一致性”)。为了避免这种情况发生,B应该在读z的值 之前对z调用sync操作。sync操作会强制B所连接的ZooKeeper服务器“赶 上”领导者,这样当B读z的值时,所读到的将会是A所更新的(或后来更新的)。


    会话

    毎个ZooKeeper客户端的配置中都包括集合体中服务器的列表。在启动时,客户端 会尝试连接到列表中的一台服务器。如果连接失败,它会尝试连接另一台服务器, 以此类推,直到成功与一台服务器建立连接或因为所有ZooKeeper服务器都不可用 而失败。

    一旦客户端与一台ZooKeeper服务器建立连接,这台服务器就会为该客户端创建一 个新的会话。毎个会话都会有一个超时的时间设置,这个设置由创建会话的应用来 设定。如果服务器在超时时间段内没有收到任何请求,则相应的会话会过期。一旦一个会话已经过期,就无法重新打开,并且任何与该会话相关联的短暂抑0如都会 丢失。会话通常长期存在,而且会话过期是一种比较罕见的事件,但对应用来说, 如何处理会话过期仍是非常重要的。

    只要一个会话空闲超过一定时间,都可以通过客户端发送ping请求(也称为心跳)保 持会话不过期。(ping请求由ZooKeeper的客户端库自动发送,因此在你的代码中 不需要考虑如何维护会话。)这个时间长度的设置应当足够低,以便能够检测出服 务器故障(由读超时体现),并且能够在会话超时的时间段内重新连接到另外一台服 务器。

    ZooKeeper客户端可以自动地进行故障切换,切换至另一台ZooKeeper服务器,并 且,关键的一点是,在另一台服务器接替故障服务器之后,所有的会话(和相关的 短暂znode)仍然是有效的。

    在故障切换过程中,应用程序将收到断开连接和连接至服务的通知。当客户端断开 连接时,观察通知将无法发送;但是当客户端成功恢复连接后,这些延迟的通知会 被发送。当然,在客户端重新连接至另一台服务器的过程中,如果应用程序试图执 行一个操作,这个操作将会失败。这充分体现了在真实的ZooKeeper应用中处理连 接丢失异常的重要性。

    时间

    在ZooKeeper中有几个时间参数。“滴答”(tick time)参数定义了 ZooKeeper中的 基本时间周期,并被集合体中的服务器用来定义交互时间表。其他设置都是根据滴 答参数来定义的,或至少受它限制。例如,会话超时(session timeout)参数的值不可 以小于2个滴答并且不可以大于20个滴答。如果你试图将会话超时参数设置在这 个范围之外,它将会被自动修改到这个范围之内。

    通常将滴答参数设置为2秒(2000毫秒),对应于允许的会话超时范围是4到40 秒。在选择会话超时设置时有几点需要考虑。

    较短的会话超时设置会较快地检测到机器故障。在组成员管理的例子中,会话超时 的时间就是用来将故障机器从组中删除的时间。但避免将会话超时时间设得太低, 因为繁忙的网络会导致数据包传输延迟,从而可能会无意中导致会话过期。在这种 情况下,机器可能会出现“振动”(flap)现象:在很短的时间内反复离开而后重 新加入组。

    对于那些创建较复杂暂时状态的应用程序来说,由于重建的代价较大,因此比较适 合设置较长的会话超时。在某些情况下,可以对应用程序进行设计,使它能够在会 话超时之前重启,从而避免出现会话过期的情况(这适合于对应用进行维护或升 级)。服务器会为每个会话分配一个唯一的ID和密码,如果在建立连接的过程中将 它们传递给ZooKeeper,可以用于恢复一个会话(只要该会话没有过期)。将会话ID 和密码保存在稳定存储器中后,可以将一个应用程序正常关闭,然后在重启应用之 前凭借所保存的会话ID和密码来恢复会话环境。

    你可以将这个特征看成是一种用来帮助避免会话过期的优化技术。但不能因此忽略 对会话过期异常的处理,因为机器的意外故障也会导致会话过期,或即使应用程序 是正常关闭,也有可能没有在它的会话过期之前完成重启——无论什么原因。

    一般的规则是,ZooKeeper集合体中的服务器越多,会话超时的设置应越大。连接 超时、读超时和ping周期都被定义为集合体中服务器数量的函数,因此集合体中 服务器数量越多,这些参数的值反而越小。如果频繁遇到连接丢失的情况,应考虑 增大超时的€置。可以使用JMX来监控ZooKeeper的度量指标,例如请求延迟 统计。


    状态

    ZooKeeper对象在其生命周期中会经历几种不同的状态(参见图14-3)。你可以在任 何时刻通过getState()方法来查询对象的状态:

    public States getState()

    States被定义成代表ZooKeeper对象不同状态的枚举类型值(不管是什么枚举 值,一个ZooKeeper的实例在一个时刻只能处于一种状态)。在试图与ZooKeeper服务建立连接的过程中,一个新建的ZooKeeper实例处于CONNECTING状态。一 旦建立连接,它就会进入CONNECTING状态。

    图14-3.ZooKeeper状态转换图

    通过注册观察对象,使用了 ZooKeeper对象的客户端可以收到状态转换通知。在 进入CONNECTED状态时,观察对象会收到一个WatchedEvent通知,其中 KeeperState 的值是 SyncConnected。

    ZooKeeper实例可以断开,然后重新连接到ZooKeeper服务,此时它的状态就在 CONNECTED和CONNECTING之间转换。如果它断开连接,观察会收到一个 Disconnected事件。注意,这些状态转换都是由ZooKeeper实例自己发起的, 如果连接丢失,它会自动尝试重新连接。

    如果close()方法被调用或出现会话超时(观察事件的KeeperState值为Expired) 时,ZooKeeper实例会转换到第三个状态CLOSED。一旦处于CLOSED状态, ZooKeeper对象不再被认为是活跃的(可以对States使用isAlive()方法来测 试),并且不能再用。为了重新连接到ZooKeeper服务,客户端必须创建一个新的 ZooKeeper 实例。

    关注微信获取最新动态