ZooKeeper提供高可用、高性能的协调服务。本文讨论其提供的服务类型、模型、操作、实现。

数据模型

ZooKeeper通过znode管理数据,众多的znode构成类似于Linux目录的层级结构。每个znode中包含数据及ACL。因为ZooKeeper的目录是提供协调服务而非数据存储,因此对于每个znode中可以存储的数据有大小限制,最大是1M。对znode中数据的读、写、更新是原子的。在一个写入操作中不会存在部分数据写入成功的情况,要不就全成功,要不就全失败。读操作同样如此。另外ZooKeeper不支持追加操作。

ZooKeeper通过路径引用znode,路径必需是绝对路径,也就是一定要以'/'开头,并且只有一种表达方式,不像Linux一样支持通配符、'.'、'..'等。

临时性znode

有两种类型的znode:临时性znode与持久性znode,znode的类型在创建时决定,并且不被允许在后来修改。当创建临时性znode的客户端失效时,ZooKeeper自动删除其创建的临时性znode。相反,持久性znode只有在明确删除时才会被删除,并且删除者不一定是创建者。一般临时性znode没有孩子节点,甚至是临时性znode。

虽然临时性znode与特定的客户端连接绑定,但它对其它所有客户端也是可见的,当然取决于ACL的设置。

临时性znode主要用于构建成员列表等应用。

***

znode可以在全局范围内管理自动增加的***。在创建znode时设置sequential标志,***将会成为znode名称的一部分,每次操作自增一次。如,创建名称为/a/b-的***类型的znode,则ZooKeeper可能会创建/a/b-3名称的znode并将其路径返回,其中的数字3是ZooKeeper自动创建的。当其它的客户端创建同类型的/a/b-时,则ZooKeeper可能会返回/a/b-4。***类型的znode可以用来在全局范围对资源、事件等编号、计数,例如共享锁等。

监视器

znode可以被观察、监视。客户端首先要对某个znode注册需要被观察、监视的事件。当znode的状态发生变化时,客户端将会被通知。例如,客户端在一个znode上调用exists操作,同时在其上加一个观察者。此时,如果znode不存在,则exists操作返回假。如果在一段时间以后第二个客户端创建了此znode,则当前客户端注册的观察者将会被触发,并且当前客户端将接收到相关信息,可以使用的观察者类型有很多。

操作

操作znode的九个基本操作:

操作 描述
create 创建znode
delete 删除znode
exists 测试znode是否存在并返回元数据
getACL、setACL 设置、返回znode的ACL
getChildren 返回znode子结点列表
getData、setData 设置、返回znode中存储的数据
sync 同步

ZooKeeper中的更新操作,如delete、setData等,在执行时需要指定znode的版本号,znode的版本号是通过前边的exists取得的。如果版本号不一致,则更新失败。原因是在取得znode的版本号以后,对它进行更新之前,已经有其它的客户端对它进行了更新,导致版本号不一致。此时,可根据具体情况决定是否重新更新或者执行其它操作。ZooKeeper可以看成是一个简化版本的文件系统,对于open、close、seek等操作这里没有提供。

关于sync操作:虽然在zookeeper中写操作中原子的。但是对于读操作,写操作可能存在延时。既写操作已经成功,但有可能读操作仍然会得到原始的数据。sync的作用是读操作强制同步,在返回结果之前,确定结果是否已经有更新,如果有,则应用更新。

多重更新

ZooKeeper支持的另一种操作是multi。它有点类似于对事务的支持,将一系列对不同znode的写操作组合在一起执行,所有操作要么全部生效、要么全部无效,避免部分成功部分失败而引起的不一致性。如当完成一个操作需要修改两个znode中的数据时,就可以使用multi。

APIS

ZooKeeper提供了多种语言实现的客户端,如Java、C、Perl、Python等。对于相同的接口函数,往往有两种实现,同步与异步。同步版本的接口在调用时客户端需等待,结果保存在接口的返回值中。异步接口被调用时函数立刻返回,当异步调用结束时,结果会传递给调用异步函数时指定的回调函数。如,下例中的同步函数、异步函数及其回调函数:

// 同步版本
public Stat exists(String path, Watcher watcher) throws KeeperException,
        InterruptedException

// 异步版本
public void exists(String path, Watcher, StatCallBack cb, Object ctx)

// 异步版本的回调函数
public void processResult(int rc, String path, Object ctx, Stat stat);

同步版本的函数相对简单。当使用事件驱动的异步模型进行程序设计时,异步版本的函数能够增加系统的吞吐量。

监视触发器

一般通过exists、getChildren、getData等写操作设置监视器。通过create、delete、setData等写操作触发监视器。ACL操作不参与监视。当一个监视器被触发,会生成一个事件,生成事件的类型取决于监视器的类型与触发它的操作:

  • exists设置的监视器会被znode的创建、删除、其持有的数据被更新操作触发。
  • getData设置的监视器会被删除、数据更新触发。getData比exists少了一个创建事件,因为执行getData的znode一定是先被创建完成的。
  • getChildren设置的监视器会被子znode的创建、删除以及自身的删除所触发。NodeDeleted指示发生了删除行为,NodeChildrenChanged指示发生删除行为的是子znode,否则是父znode。

当事件发生时,会传递znode的路径以便区别发生事件的znode是谁。但是有一点需要注意,例如如果收到NodeChildrenChanged的事件,处理函数只知道发生事件的具体znode及发生了什么事,至于是那个子节点被新创建或者删除,则需要另外调用诸如getChildren之类的函数重新获取信息,但在此期间,znode的children可能已经又发生了变化,此时取到的子节点与事件发生之时可能已经不同。当znode中的数据发生变更时,执行getData也是相同的情况,在具体的应用开发时需要注意。

ACLs

在创建znode时,通过指定ACL列表指出谁可以对这个znode执行何种操作。ACLs的实现依赖于用户身份的认证机制,既客户端如何向ZooKeeper服务证明自己是谁。ZooKeeper支持如下三种客户端身份认证机制:

  • digest:客户端通过用户名与密码认证自己的身份。
  • sasl:客户端通过Kerberos进行身份认证。
  • ip:客户端通过IP地址标识自己的身份。

客户端在与ZooKeeper服务器建立会话后可以进行身份验证,也并非一定需要,取决于所访问的znode,如果znode的ACLs明确规定访问此znode之前需要先进行身份认证,才会需要身份认证,如果znode的ACLs规定不需要身份认证则不需要。

下面代码展示如何在digest模式下添加用户:

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

在为znode指定ACLs时,需要先创建,而ACLs的创建,需要与身份认证相关联,如下代码:

new ACL(Perms.READ, new Id("ip", "10.0.0.1"));

此ACL表示当客户端IP是10.0.0.1时,允许其读znode。

Perms.READ是所有可设置权限中的一种,全部可用的权限及其对应操作如下表:

ACL permission 允许操作
CREATE create(创建子znode)
READ getChildren、getData
WRITE setData
DELETE delete(删除子znode)
ADMIN setACL

前文中使用的OPEN_ACL_UNSAFE为ZooKeeper预定义的ACL,定义在ZooDefs.Ids类中。另外ZooKeeper支持身份认证插件化,能够集成第三方认证方案。

可用性

ZooKeeper支持两种部署方案:单机模式与replicated模式。前者只需要一台服务器,主要用于测试,不保证高可用性。后者将ZooKeeper以多实例备份的形式部署在一个由多台服务器组成的集群中。如集群中有五台服务器,则即使其中的两台失效,ZooKeeper仍然能正常提供服务,因为失效的服务器数没有过半。

ZooKeeper实现此功能的概念非常简单,当对一个znode执行变更时,ZooKeeper保证此变更被应用于超过半数的服务器,例如五台中的三台,当有不超过半数的服务器失效时如其中的两台,并且两台恰好是被更新的两台,此时仍然有一台服务器的状态是最新并且可用的,而其它没有被更新的服务器,也就是状态落后于正常服务器的节点最终也会被正确更新。

ZooKeeper内部使用Zab协议实现此功能。首先集群中的节点分成一个leader节点,及其它的followers节点。当更新时,首先作用于leader节点,当leader节点完成更新的持久化后,它会向它的followers节点发送更新请求,当followers中超过半数的节点已经持久化更新后,更新操作成功,当然整个操作过程被设计成原子的。如果leader节点失效,则在余下的节点中通过某种算法选出新的leader节点并重复以上过程。为了提高响应速度,ZooKeeper首先将变更应用到内存中,返回数据时首先查内存,持久化可以在稍后进行。

一致性保证

在ZooKeeper集群中,follower的状态可能落后于leader几个更新,并且不同的follower落后的更新数可能还不相同,当然这种落后指的是节点已经对变更编号,但还没有应用于内在,当然也没有持久化这种情况。在ZooKeeper中,每个变更操作都会在全局范围内被分配一个唯一的id,称之为zxid。当某个变更的zxid为z1,小于另一个变更的zxid z2,则表示z1的变更发生在z2之前,zxid的目的是保证在全局范围内变更发生顺序的一致性。ZooKeeper通过如下几个设计保证一致性:

  • 变更序列一致:ZooKeeper保证所有变更,在集群内所有的节点上按统一的顺序发生。
  • 变更原子性:所有的变更操作都是原子的,要么全部成功,要么全部失败,不存在中间状态。
  • 单image:如果客户端连到的节点失效并且需要重新选择节点连接时,ZooKeeper保证新节点的状态一定不会比原来的节点旧。
  • 持久化:变更成功,对于集群而言,意味着已经更新到硬盘上,并且节点的个数超过一半。也就是说即使有一半的节点失效,集群仍能正确工作。
  • 时间线:对于follower滞后于leader是时间限制,不能太多,如果某个节点超过限制则强制其先暂停工作,然后将状态更新到符合要求为止。

我个人理解,对于某些节点,变更只是被编号并记录在案,其生效需要先将变更应用于内存,最后再将变更应用于硬盘。如果某个变更已经在集群中超过半数的节点上完成更新并返回,此时某个客户端连接的节点恰好还没有将变更应用于内存,此时就会发生不一致问题。解决方案中的一种是后者在读取数据之前,先调用sync函数,强迫连接的节点跟上leader的状态。如果此类操作太多一定会影响到性能。在实际应用中可能需要在性能与一致性之间作出选择。

会话

首先需要为ZooKeeper客户端配置一组可用ZooKeeper服务器,在启动时,客户端试着与一组服务器中的一个建立连接,如果失败则选取其它的服务器重试,直到成功。如果所有的服务器都不能建立连接则会失败。

当与服务器建立连接后,服务器会创建一个会话。如果在规定的时间内服务器没有收到来自客户端的请求,则会话超时,服务器将会删除会话。客户端在空闲时通过PING与保活,所以会话失效的情况并不常见,除非发生网络问题。当然客户端发送保活请求的时间间隔要小于服务器端会话超时的时间隔。

当客户端因为某种原因失去与服务器的联系时,由ZooKeeper的客户端自动尝试连接其它可用的服务器,并且所连接服务器的状态一定不会落后与原来的服务器。在此期间,客户端会收到连接失效及重新连接的事件,当然在旧连接失效时不会产生此类通知,而是在新连接建立后才会产生此类事件。在连接重新建立时如果客户端发起操作则会失败。

状态

客户端创建的ZooKeeper对象是有状态的,它在其生命周期内会在几个状态之间迁移。可通过调用getStatez()方法返回当前状态。ZooKeeper对象在某个时间点只能处于一种状态,可能的状态及变化路线如下:

ZooKeeper服务模型、操作、实现

可以看到,CONNECTING与CONNECTED之间可以相互转换,主要原因是客户端在失去与服务器的连接后会自动尝试其它服务器所引起。而CONNECTED到CLISED之间有两条线,其中左边表示客户端主动关闭,右边表示服务端会话超时。对于每一种状态变迁,用户都可以对连接的相关事件注册监视器,当事件发生时,客户端会收到通知并进行处理。

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-09-22
  • 2022-12-23
  • 2022-12-23
  • 2021-09-21
  • 2022-12-23
猜你喜欢
  • 2022-03-02
  • 2022-12-23
  • 2021-09-09
  • 2022-12-23
  • 2021-10-13
  • 2021-11-16
  • 2021-09-15
相关资源
相似解决方案