经典分布式论文阅读:Zookeeper

4,616 阅读9分钟

本文是ZooKeeper论文的阅读笔记,ZooKeeper用于协调分布式系统中的进程,为分布式系统提供消息群发、共享寄存器、分布式锁这些中心化的服务。

分布式系统中需要的协调服务包括:配置、组成员关系、领导选举和锁服务。ZooKeeper并没有直接提供这些服务,因为更强的原语可以用来实现较弱的原语,ZooKeeper提供了API供开发者实现自己的原语。ZooKeeper的API操作类似文件系统的层级结构上的免等待数据对象,同时保证所有操作的客户端先进先出串行写入。ZooKpeer使用管道架构实现高吞吐和低延迟,更新操作采用Zab保证线性,读取操作在服务器本地进行,不需要确定顺序。观察机制在数据更新之后通知客户端,使得客户端能够快速获取最新数据。

ZooKeeper服务

ZooKeeper以库的形式向客户端提供API,库也负责客户端到ZooKeeper服务器的连接。ZooKeeper中的数据节点称为znode,以树型命名空间组织。客户端连接服务器后建立会话,通过会话句柄发送请求。

服务总览

ZooKeeper给客户端提供了数据对象的抽象(znode)。

znode有两种类型:

  • 常规:数据对象正常创建和删除。
  • 临时:创建对象的会话终止之后,对象会被删除。

如果在创建文件的时候设置SEQUENTIAL标志,那么会在文件名后增加一个自动增加的计数器。ZooKeeper实现了观测(watch)机制,能够在数据对象更新后通知客户端,观测只会触发一次。

数据模型:ZooKeeper中的数据模型是只支持全量读写的文件系统,znode保存应用程序的抽象概念,用来存储配置、元数据等信息。

会话:客户端连接ZooKeeper后建立会话,会话用来标识客户端。

客户端API

  • create(path, data, flags):创建一个路径为path的znode,将data[]保存到其中,返回新znode的名称,flags用来设置znode类型:普通或者临时,以及设置SEQUENTIAL标志。
  • delete(path, version):如果版本匹配,删除path对应的znode。
  • exists(path, watch):如果path对应的znode存在,那么返回真,否则返回假。watch标志让客户端观测这个znode。
  • getData(path, watch):返回znode对应的数据和元数据,watch功能类似。
  • setData(path, data, version):如果版本匹配,将data[]写入到path对应的znode中。
  • getChildren(path, watch):返回znode的子节点集合。
  • sync(path):等待目前所有未决的更新,path没什么用。

以上全部的方法提供了阻塞版本和非阻塞版本,如果传入版本号为-1,那么不进行版本检查。

ZooKeeper保证

ZooKeeper有两项基本的顺序保证

  • 线性写入:所有改变ZooKeeper状态的更新都是串行的;
  • 客户端先进先出:所有来自客户端的请求按照先进先出顺序执行。

可以举个例子演示这两个保证如何保障系统运行。假设一个系统选举主节点管理其他节点,主节点随后需要更新一些配置,然后通知其他节点,要求:

  • 主节点在修改配置过程,不希望其他节点访问正在被修改的配置
  • 主节点在更新完成前崩溃,不希望其他节点访问这些破碎的配置

可以设置一个readyznode解决,主节点可以在配置前删除,完成后重新建立。当其他节点看到ready不存在时就不读取配置。

但是还会存在问题:如果其他节点看到ready后读取配置,但是主节点随即删除开始修改配置,那么其他节点将得到过时的配置。这个问题可以采用观测机制来解决,ready删除后会及时通知其他节点。

ZooKeeper两个耐久性保证:

  • 如果大部分服务器都活跃,那么服务就是可用的
  • 如果ZooKeeper成功响应了一个修改请求,只要大部分的节点都可以最终恢复,那么修改就可以在无数次故障中保持持久。

原语例子

  • 配置管理:只需要将配置保存在一个znode中,各个进程可以通过观测来获取配置更新通知。
  • 会合:很多分布式系统包含主节点和工作节点,但是节点的调度由调度器决定,可以将主节点信息放在一个znode,供工作节点找到主节点。
  • 组成员关系:组成员进程上线之后可以在组对应的znode之下创建对应的临时子znode,成员进程退出之后临时znode也被删除,因此可以通过组znode的子znode获取组成员状态。
  • 简单锁:锁可以创建一个对应的znode实现。如果创建成功,那么获取锁。如果已经存在,那么需要等待锁被释放(znode被删除)后才能获取锁(创建znode)。
  • 无羊群效应的简单锁:简单锁会出现大量进程竞争的情况,可以将锁请求排序后,按次序分配锁。
Lock
1   n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL)
2   C = getChildren(l, false)
3   if n is lowest znode in C, exit
4   p = znode in C ordered just before n
5   if exists(p, true) wait for watch event
6   goto 2
Unlock
1   delete(n)
  • 读写锁:写锁和普通锁类似,和其他的锁互斥。
Write Lock
1   n = create(l + “/write-”, EPHEMERAL|SEQUENTIAL)
2   C = getChildren(l, false)
3   if n is lowest znode in C, exit
4   p = znode in C ordered just before n
5   if exists(p, true) wait for event
6   goto 2

读锁之间可以互相兼容,和写锁互斥。

Read Lock
1   n = create(l + “/read-”, EPHEMERAL|SEQUENTIAL)
2   C = getChildren(l, false)
3   if no write znodes lower than n in C, exit
4   p = write znode in C ordered just before n
5   if exists(p, true) wait for event
6   goto 3
  • 双栅栏:双栅栏用来保证多个客户端的计算同时开始和同时结束。客户端开始计算之前添加znode到栅栏对应的znode之下,结束计算之后删除znode。客户端需要等待栅栏znode的子znode数量到达一定阈值后才能开始计算,客户端可以等待一个特殊的ready的znode的创建,当数量到达阈值后创建。客户端退出的时候需要等待子znode全部被删除,同样可以通过删除ready删除。

ZooKeeper应用

  • 解析服务:在雅虎的爬虫系统的解析服务中,主节点需要告知解析节点系统配置,解析节点需要报告自己的状态。因此,解析服务使用ZooKeeper管理配置领导选举。下图是系统读写操作情况,可以发现读取操作占大头。

  • Katta:Katta是一个分布式索引,主节点将分片分配给从节点并追踪进度,主要使用ZooKeeper进行组成员关系管理领导选举配置管理
  • 雅虎消息中介:雅虎消息中介负责无数话题下的消息的发布和接收,这些话题分布在多个服务器上,每个服务器采用主从备份。系统的znode结构如下图所示,类似于shutdownmigration_prohibited是系统的配置信息,nodes保存了属于组成员的服务器信息,而topics保存了负责具体话题对应的主服务器已经从服务器,另外在主节点奔溃后需要领导选举

ZooKeeper实现

ZooKeeper的组件如下图所示,ZooKeeper的数据副本保存在每一个服务器上,写操作需要通过一致性协议提交到数据库,而读取请求可以直接访问服务器本地数据库获得。ZooKeeper在应用修改到数据库之前会写入到磁盘,故障后采用快照加日志的方式进行故障。根据一致协议,写入请求会转发到领导(leader)节点。

请求处理器

请求处理器收到写入请求之后,会将其转换为幂等的事务,根据请求内容计算出新的数据、版本号和时间戳,等待应用到数据库中。

原子广播

ZooKeeper使用Zab作为原子广播协议,使用简单的多数认同达成一致性。Zab保证广播发送和接受的顺序是一致的,领导节点广播之前需要确保已经收到了前一个领导的广播。

多副本数据库

当服务器故障后,使用周期性的快照和快照之后的日志恢复。创建快照的时候并不需要锁定,因为事务都是幂等的,因此再次应用已经应用的修改没有影响。

客户端-服务器交互

当服务器执行一个写入操作后,会通知观测的客户端并清除观测,每个服务器只负责通知自己连接的客户端。每个读取请求对应着一个zxid,对应服务器上看到的最后一个写入事务的ID。因为读取是在服务器本地进行,可能在读取之前的一些写入没有同步到客户端连接的服务器,ZooKeeper提供了sync操作,保证sync之后的读取操作都能够获得发生在sync之前的写入结果。客户端会从服务器获取最新zxidzxid另外一个作用就是保证客户端在切换服务器后,新服务器看到视图不能比客户端之前看到的视图落后,也就是服务器zxid不能早于客户端的zxid。如果检测客户端故障,会话是有超时时间的,客户端在没有活动期间也要发送心跳避免超时。

参考文献

  1. Hunt, Patrick, et al. "ZooKeeper: Wait-free Coordination for Internet-scale Systems." USENIX annual technical conference. Vol. 8. No. 9. 2010.