共计 7073 个字符,预计需要花费 18 分钟才能阅读完成。
ZooKeeper 是 Hadoop Ecosystem 中非常重要的组件,它的主要功能是为分布式系统提供一致性协调 (Coordination) 服务,与之对应的 Google 的类似服务叫 Chubby。今天这篇文章分为三个部分来介绍 ZooKeeper,第一部分介绍 ZooKeeper 的基本原理,第二部分介绍 ZooKeeper 提供的 Client API 的使用,第三部分介绍一些 ZooKeeper 典型的应用场景。
ZooKeeper 基本原理
1. 数据模型
如上图所示,ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每个 ZNode 都可以通过其路径唯一标识,比如上图中第三层的第一个 ZNode, 它的路径是 /app1/c1。在每个 ZNode 上可存储少量数据(默认是 1M, 可以通过配置修改, 通常不建议在 ZNode 上存储大量的数据),这个特性非常有用,在后面的典型应用场景中会介绍到。另外,每个 ZNode 上还存储了其 Acl 信息,这里需要注意,虽说 ZNode 的树形结构跟 Unix 文件系统很类似,但是其 Acl 与 Unix 文件系统是完全不同的,每个 ZNode 的 Acl 的独立的,子结点不会继承父结点的,关于 ZooKeeper 中的 Acl 可以参考之前写过的一篇文章《Zookeeper 中的 ACL 简述》。
2. 重要概念
2.1 ZNode
前文已介绍了 ZNode, ZNode 根据其本身的特性,可以分为下面两类:
Regular ZNode: 常规型 ZNode, 用户需要显式的创建、删除
Ephemeral ZNode: 临时型 ZNode, 用户创建它之后,可以显式的删除,也可以在创建它的 Session 结束后,由 ZooKeeper Server 自动删除
ZNode 还有一个 Sequential 的特性,如果创建的时候指定的话,该 ZNode 的名字后面会自动 Append 一个不断增加的 SequenceNo。
2.2 Session
Client 与 ZooKeeper 之间的通信,需要创建一个 Session,这个 Session 会有一个超时时间。因为 ZooKeeper 集群会把 Client 的 Session 信息持久化,所以在 Session 没超时之前,Client 与 ZooKeeper Server 的连接可以在各个 ZooKeeper Server 之间透明地移动。
在实际的应用中,如果 Client 与 Server 之间的通信足够频繁,Session 的维护就不需要其它额外的消息了。否则,ZooKeeper Client 会每 t /3 ms 发一次心跳给 Server,如果 Client 2t/3 ms 没收到来自 Server 的心跳回应,就会换到一个新的 ZooKeeper Server 上。这里 t 是用户配置的 Session 的超时时间。
2.3 Watcher
ZooKeeper 支持一种 Watch 操作,Client 可以在某个 ZNode 上设置一个 Watcher,来 Watch 该 ZNode 上的变化。如果该 ZNode 上有相应的变化,就会触发这个 Watcher,把相应的事件通知给设置 Watcher 的 Client。需要注意的是,ZooKeeper 中的 Watcher 是一次性的,即触发一次就会被取消,如果想继续 Watch 的话,需要客户端重新设置 Watcher。这个跟 epoll 里的 oneshot 模式有点类似。
3. ZooKeeper 特性
3.1 读、写 (更新) 模式
在 ZooKeeper 集群中,读可以从任意一个 ZooKeeper Server 读,这一点是保证 ZooKeeper 比较好的读性能的关键;写的请求会先 Forwarder 到 Leader,然后由 Leader 来通过 ZooKeeper 中的原子广播协议,将请求广播给所有的 Follower,Leader 收到一半以上的写成功的 Ack 后,就认为该写成功了,就会将该写进行持久化,并告诉客户端写成功了。
3.2 WAL 和 Snapshot
和大多数分布式系统一样,ZooKeeper 也有 WAL(Write-Ahead-Log),对于每一个更新操作,ZooKeeper 都会先写 WAL, 然后再对内存中的数据做更新,然后向 Client 通知更新结果。另外,ZooKeeper 还会定期将内存中的目录树进行 Snapshot,落地到磁盘上,这个跟 HDFS 中的 FSImage 是比较类似的。这么做的主要目的,一当然是数据的持久化,二是加快重启之后的恢复速度,如果全部通过 Replay WAL 的形式恢复的话,会比较慢。
3.3 FIFO
对于每一个 ZooKeeper 客户端而言,所有的操作都是遵循 FIFO 顺序的,这一特性是由下面两个基本特性来保证的:一是 ZooKeeper Client 与 Server 之间的网络通信是基于 TCP,TCP 保证了 Client/Server 之间传输包的顺序;二是 ZooKeeper Server 执行客户端请求也是严格按照 FIFO 顺序的。
3.4 Linearizability
在 ZooKeeper 中,所有的更新操作都有严格的偏序关系,更新操作都是串行执行的,这一点是保证 ZooKeeper 功能正确性的关键。
ZooKeeper Client API
ZooKeeper Client Library 提供了丰富直观的 API 供用户程序使用,下面是一些常用的 API:
create(path, data, flags): 创建一个 ZNode, path 是其路径,data 是要存储在该 ZNode 上的数据,flags 常用的有: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL
delete(path, version): 删除一个 ZNode,可以通过 version 删除指定的版本, 如果 version 是 - 1 的话,表示删除所有的版本
exists(path, watch): 判断指定 ZNode 是否存在,并设置是否 Watch 这个 ZNode。这里如果要设置 Watcher 的话,Watcher 是在创建 ZooKeeper 实例时指定的,如果要设置特定的 Watcher 的话,可以调用另一个重载版本的 exists(path, watcher)。以下几个带 watch 参数的 API 也都类似
getData(path, watch): 读取指定 ZNode 上的数据,并设置是否 watch 这个 ZNode
setData(path, watch): 更新指定 ZNode 的数据,并设置是否 Watch 这个 ZNode
getChildren(path, watch): 获取指定 ZNode 的所有子 ZNode 的名字,并设置是否 Watch 这个 ZNode
sync(path): 把所有在 sync 之前的更新操作都进行同步,达到每个请求都在半数以上的 ZooKeeper Server 上生效。path 参数目前没有用
setAcl(path, acl): 设置指定 ZNode 的 Acl 信息
getAcl(path): 获取指定 ZNode 的 Acl 信息
ZooKeeper 典型应用场景
1. 名字服务(NameService)
分布式应用中,通常需要一套完备的命令机制,既能产生唯一的标识,又方便人识别和记忆。我们知道,每个 ZNode 都可以由其路径唯一标识,路径本身也比较简洁直观,另外 ZNode 上还可以存储少量数据,这些都是实现统一的 NameService 的基础。下面以在 HDFS 中实现 NameService 为例,来说明实现 NameService 的基本布骤:
目标:通过简单的名字来访问指定的 HDFS 机群
定义命名规则:这里要做到简洁易记忆。下面是一种可选的方案:[serviceScheme://][zkCluster]-[clusterName],比如 hdfs://lgprc-example/ 表示基于 lgprc ZooKeeper 集群的用来做 example 的 HDFS 集群
配置 DNS 映射: 将 zkCluster 的标识 lgprc 通过 DNS 解析到对应的 ZooKeeper 集群的地址
创建 ZNode: 在对应的 ZooKeeper 上创建 /NameService/hdfs/lgprc-example 结点,将 HDFS 的配置文件存储于该结点下
用户程序要访问 hdfs://lgprc-example/ 的 HDFS 集群,首先通过 DNS 找到 lgprc 的 ZooKeeper 机群的地址,然后在 ZooKeeper 的 /NameService/hdfs/lgprc-example 结点中读取到 HDFS 的配置,进而根据得到的配置,得到 HDFS 的实际访问入口
2. 配置管理(Configuration Management)
在分布式系统中,常会遇到这样的场景: 某个 Job 的很多个实例在运行,它们在运行时大多数配置项是相同的,如果想要统一改某个配置,一个个实例去改,是比较低效,也是比较容易出错的方式。通过 ZooKeeper 可以很好的解决这样的问题,下面的基本的步骤:
将公共的配置内容放到 ZooKeeper 中某个 ZNode 上,比如 /service/common-conf
所有的实例在启动时都会传入 ZooKeeper 集群的入口地址,并且在运行过程中 Watch /service/common-conf 这个 ZNode
如果集群管理员修改了了 common-conf,所有的实例都会被通知到,根据收到的通知更新自己的配置,并继续 Watch /service/common-conf
3. 组员管理(Group Membership)
在典型的 Master-Slave 结构的分布式系统中,Master 需要作为“总管”来管理所有的 Slave, 当有 Slave 加入,或者有 Slave 宕机,Master 都需要感知到这个事情,然后作出对应的调整,以便不影响整个集群对外提供服务。以 HBase 为例,HMaster 管理了所有的 RegionServer,当有新的 RegionServer 加入的时候,HMaster 需要分配一些 Region 到该 RegionServer 上去,让其提供服务;当有 RegionServer 宕机时,HMaster 需要将该 RegionServer 之前服务的 Region 都重新分配到当前正在提供服务的其它 RegionServer 上,以便不影响客户端的正常访问。下面是这种场景下使用 ZooKeeper 的基本步骤:
Master 在 ZooKeeper 上创建 /service/slaves 结点,并设置对该结点的 Watcher
每个 Slave 在启动成功后,创建唯一标识自己的临时性 (Ephemeral) 结点 /service/slaves/${slave_id},并将自己地址 (ip/port) 等相关信息写入该结点
Master 收到有新子结点加入的通知后,做相应的处理
如果有 Slave 宕机,由于它所对应的结点是临时性结点,在它的 Session 超时后,ZooKeeper 会自动删除该结点
Master 收到有子结点消失的通知,做相应的处理
4. 简单互斥锁(Simple Lock)
我们知识,在传统的应用程序中,线程、进程的同步,都可以通过操作系统提供的机制来完成。但是在分布式系统中,多个进程之间的同步,操作系统层面就无能为力了。这时候就需要像 ZooKeeper 这样的分布式的协调 (Coordination) 服务来协助完成同步,下面是用 ZooKeeper 实现简单的互斥锁的步骤,这个可以和线程间同步的 mutex 做类比来理解:
多个进程尝试去在指定的目录下去创建一个临时性 (Ephemeral) 结点 /locks/my_lock
ZooKeeper 能保证,只会有一个进程成功创建该结点,创建结点成功的进程就是抢到锁的进程,假设该进程为 A
其它进程都对 /locks/my_lock 进行 Watch
当 A 进程不再需要锁,可以显式删除 /locks/my_lock 释放锁;或者是 A 进程宕机后 Session 超时,ZooKeeper 系统自动删除 /locks/my_lock 结点释放锁。此时,其它进程就会收到 ZooKeeper 的通知,并尝试去创建 /locks/my_lock 抢锁,如此循环反复
5. 互斥锁(Simple Lock without Herd Effect)
上一节的例子中有一个问题,每次抢锁都会有大量的进程去竞争,会造成羊群效应(Herd Effect),为了解决这个问题,我们可以通过下面的步骤来改进上述过程:
每个进程都在 ZooKeeper 上创建一个临时的顺序结点(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的为当前的持锁者 (${seq} 是 ZooKeeper 生成的 Sequenctial Number)
其它进程都对只 watch 比它次小的进程对应的结点,比如 2 watch 1, 3 watch 2, 以此类推
当前持锁者释放锁后,比它次大的进程就会收到 ZooKeeper 的通知,它成为新的持锁者,如此循环反复
这里需要补充一点,通常在分布式系统中用 ZooKeeper 来做 Leader Election(选主)就是通过上面的机制来实现的,这里的持锁者就是当前的“主”。
6. 读写锁(Read/Write Lock)
我们知道,读写锁跟互斥锁相比不同的地方是,它分成了读和写两种模式,多个读可以并发执行,但写和读、写都互斥,不能同时执行行。利用 ZooKeeper,在上面的基础上,稍做修改也可以实现传统的读写锁的语义,下面是基本的步骤:
每个进程都在 ZooKeeper 上创建一个临时的顺序结点(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的一个或多个结点为当前的持锁者,多个是因为多个读可以并发
需要写锁的进程,Watch 比它次小的进程对应的结点
需要读锁的进程,Watch 比它小的最后一个写进程对应的结点
当前结点释放锁后,所有 Watch 该结点的进程都会被通知到,他们成为新的持锁者,如此循环反复
7. 屏障(Barrier)
在分布式系统中,屏障是这样一种语义: 客户端需要等待多个进程完成各自的任务,然后才能继续往前进行下一步。下用是用 ZooKeeper 来实现屏障的基本步骤:
Client 在 ZooKeeper 上创建屏障结点 /barrier/my_barrier,并启动执行各个任务的进程
Client 通过 exist()来 Watch /barrier/my_barrier 结点
每个任务进程在完成任务后,去检查是否达到指定的条件,如果没达到就啥也不做,如果达到了就把 /barrier/my_barrier 结点删除
Client 收到 /barrier/my_barrier 被删除的通知,屏障消失,继续下一步任务
8. 双屏障(Double Barrier)
双屏障是这样一种语义: 它可以用来同步一个任务的开始和结束,当有足够多的进程进入屏障后,才开始执行任务;当所有的进程都执行完各自的任务后,屏障才撤销。下面是用 ZooKeeper 来实现双屏障的基本步骤:
进入屏障:
Client Watch /barrier/ready 结点, 通过判断该结点是否存在来决定是否启动任务
每个任务进程进入屏障时创建一个临时结点 /barrier/process/${process_id},然后检查进入屏障的结点数是否达到指定的值,如果达到了指定的值,就创建一个 /barrier/ready 结点,否则继续等待
Client 收到 /barrier/ready 创建的通知,就启动任务执行过程
离开屏障:
Client Watch /barrier/process,如果其没有子结点,就可以认为任务执行结束,可以离开屏障
每个任务进程执行任务结束后,都需要删除自己对应的结点 /barrier/process/${process_id}
ZooKeeper 学习总结 http://www.linuxidc.com/Linux/2016-07/133179.htm
Ubuntu 14.04 安装分布式存储 Sheepdog+ZooKeeper http://www.linuxidc.com/Linux/2014-12/110352.htm
CentOS 6 安装 sheepdog 虚拟机分布式储存 http://www.linuxidc.com/Linux/2013-08/89109.htm
ZooKeeper 集群配置 http://www.linuxidc.com/Linux/2013-06/86348.htm
使用 ZooKeeper 实现分布式共享锁 http://www.linuxidc.com/Linux/2013-06/85550.htm
分布式服务框架 ZooKeeper — 管理分布式环境中的数据 http://www.linuxidc.com/Linux/2013-06/85549.htm
ZooKeeper 集群环境搭建实践 http://www.linuxidc.com/Linux/2013-04/83562.htm
ZooKeeper 服务器集群环境配置实测 http://www.linuxidc.com/Linux/2013-04/83559.htm
ZooKeeper 集群安装 http://www.linuxidc.com/Linux/2012-10/72906.htm
Zookeeper3.4.6 的安装 http://www.linuxidc.com/Linux/2015-05/117697.htm
本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-11/136704.htm