之前我们在搭建hadoop分布式环境的时候用到过Zookeeper注册hadoop服务。那么到底Zookeeper在分布式环境中发挥了什么作用呢,这次我们就来讨论这个问题。
在分布式系统中通常都会有多台机器构成一个集群来对外提供服务,对外来说有几台机器在提供服务它并不关心,那么对内而言组成集群的机器如何互相协调保持一致这是个问题。Zookeeper将提供这样的一个功能–分布式协调技术。
1. Zookerrper概述
ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等。
ZooKeeper性能上的特点决定了它能够用在大型的、分布式的系统当中。从可靠性方面来说,它并不会因为一个节点的错误而崩溃。除此之外,它严格的序列访问控制意味着复杂的控制原语可以应用在客户端上。ZooKeeper在一致性、可用性、容错性的保证,也是ZooKeeper的成功之处,它获得的一切成功都与它采用的协议——Zab协议是密不可分的。
ZooKeeper在实现这些服务时,首先它设计一种新的数据结构——Znode,然后在该数据结构的基础上定义了一些原语,也就是一些关于该数据结构的一些操作。有了这些数据结构和原语还不够,因为我们的ZooKeeper是工作在一个分布式的环境下,我们的服务是通过消息以网络的形式发送给我们的分布式应用程序,所以还需要一个通知机制——Watcher机制。那么总结一下,ZooKeeper所提供的服务主要是通过:数据结构+原语+watcher机制,三个部分来实现的。那么我就从这三个方面,给大家介绍一下ZooKeeper。
2. Zookeeper的数据模型Znode
Znode用来存储节点信息,它的数据模型和我们平常的文件管理系统目录树非常相似。不同之处在于:
1)路径引用
Znode通过路径引用来管理子节点。路径必须是绝对的,所以路径都是以斜杠字符来开头。其次,与我们的文件系统一样,路径必须的是惟一的。
2)Znode数据结构
Znode不单单是维护着节点信息,同时自身也保存着一些关联信息。这些信息可以分为3个部分:
- stat:状态信息,描述Znode的版本以及权限信息;
- data; 与该Znode关联的数据信息;
- children:该Znode下的子节点信息;
其中stat中的信息与我们平常的操作息息相关,stat中又包含如下字段:
- czxid: 引起这个znode创建的zxid
- mzxid: znode最后更新的zxid
- ctime: znode被创建的毫秒数(从1970年开始)
- mtime: znode最后修改的毫秒数(从1970年开始)
- version: znode数据变化号
- cversion: znode子节点变化号
- aversion: znode访问控制列表的变化号
- ephemeralOwner: 如果是临时节点这个是znode拥有者的session id。如果不是临时节点则是0
- dataLength: znode的数据长度
- numChildren: znode子节点数量
后面我们再分析这些字段的意义。
data部分并不是存放一些非常大的数据。Zookeeper设计并不是作为常规的数据仓库或者数据库的,他作为分布式协调任务调度器,通常是会保存一些必要的配置文件,状态,以及路由信息,这些信息的存储通常都不会太大,Zookeeeper的服务端和客户端都被设计为严格检查每个Znode的数据大小最大为1M。
3)数据访问
Zookeeper中每个节点存储的数据被要求为原子操作。每一个节点都有自己的ACL(访问控制列表),这个列表规定了用户的权限。
4)节点类型
zk中的节点分为两种类型:临时节点和永久节点。节点的类型在创建的时候就被确定并且不能更改。
- 临时节点:节点的生命周期依赖于创建他们的会话。一旦会话(session)结束,临时节点就会被自动删除。一般临时节点虽然是某一个客户端发起会话创建的,但是他们对所有的客户端都是可见的。另外,zk中规定临时节点不可以拥有子节点。
- 永久节点:该节点的生命周期不依赖于会话,只有客户端发起删除命令才会被删除。
5)观察
客户端可以在节点上设置watch,即监视器。当节点发生改变的时候出发watch所对应的操作,zk将会向客户端发送一条且仅发送一条通知。因为watch只能被出发一次,这样就减少了网络的流量消耗。
2.1 zk中的时间
zk中记录时间并不是一个简单的时间戳,包含如下属性:
Zxid
zk中节点状态改变的每一个操作都会被记录一个Zxid格式的时间戳,这个时间戳全局有序。即每个对节点的改变都会记录一个全局唯一的时间戳,如果Zxid1的值小于Zxid2,那么Zxid1所发生的时间必然在Zxid2之前。事实上zk的每一个节点都维护者三个Zxid:分别为:cZxid,mZxid,pZxid:
cZxid: 是节点的创建时间所对应的Zxid格式时间戳。
mZxid: 是节点的修改时间所对应的Zxid格式时间戳。
pZxid: 最新修改的Zxid,是不是与mZxid重复了。
Zxid是一个64位的数字,高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,他都会有一个新的epoch。低32位是个递增计数器。
Version 版本号
对节点的每一个操作都会使这个节点的版本号递增。每一个节点分别维护着三个版本号:
- version:节点数据版本号
- cversion:子节点版本号
- aversion:节点所拥有的ACL版本号
3. zk中的基本操作
create : 创建Znode(如果是zi节点,父节点必须存在)
delete : 删除Znode(Znode没有子节点)
exists : 测试Znode是否存在,存在则获取他的元数据信息
getACL/setACL : 为Znode获取/设置ACL信息
getChildren :获取Znode所有子节点列表
getData/setData :获取/设置Znode的相关数据
sync : 是客户端的Znode师徒与zk同步
操作 | 说明 |
---|---|
create | 创建Znode(如果是zi节点,父节点必须存在) |
delete | 删除Znode(Znode没有子节点) |
exists | 测试Znode是否存在,存在则获取他的元数据信息 |
getACL/setACL | 为Znode获取/设置ACL信息 |
getChildren | 获取Znode所有子节点列表 |
getData/setData | 获取/设置Znode的相关数据 |
sync | 是客户端的Znode师徒与zk同步 |
4. watch触发器
ZooKeeper可以为所有的读操作设置watch,这些读操作包括:exists()、getChildren()及getData()。watch事件是一次性的触发器。
watch类型可以分为两类:
- 数据watch(data watches):getData和exists负责设置数据watch
- 孩子watch(child watches):getChildren负责设置孩子watch
5. zk的应用场景
1)分布式锁
共享锁在同一个进程中是很容易实现,但是在跨进程或者是不同的server中实现起来却不是那么容易。zk实现这个功能却是很容易。
在实现中,获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。
具体步骤如下:
加锁: ZooKeeper 将按照如下方式实现加锁的操作:
ZooKeeper调用create()方法来创建一个路径格式为“locknode/lock- ”的节点,此节点类型为sequence (连续)和 ephemeral (临时)。也就是说,创建的节点为临时节点,并且所有的节点连续编号,即“lock-i ”的格式。
在创建的锁节点上调用getChildren()方法,来获取锁目录下的最小编号节点,并且不设置 watch 。
步骤 2 中获取的节点恰好是步骤1中客户端创建的节点,那么此客户端获得此种类型的锁,然后退出操作。
客户端在锁目录上调用exists()方法,并且设置 watch 来监视锁目录下比自己小一个的连续临时节点的状态。
如果监视节点状态发生变化,则跳转到第2步,继续进行后续的操作,直到退出锁竞争。
2)配置管理(数据发布与订阅)
在分布式系统里,我们会把一个服务应用分别部署到n台服务器上,这些服务器的配置文件是相同的,如果配置文件的配置选项发生变化,那么我们就得一个个去改这些配置文件,如果我们需要改的服务器比较少,这些操作还不是太麻烦,如果我们分布式的服务器特别多,那么更改配置选项就是一件麻烦而且危险的事情。这时我们可以将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
3)集群管理
Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
4)队列管理
Zookeeper 可以处理两种类型的队列:
- 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
- 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
A、同步队列 用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。
B、FIFO队列:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。