Zookeeper简单介绍
2017-12-25
1 ZooKeeper是什么
1.1 分布式系统面临的问题
1.2 ZooKeeper概述
1.3 分布式锁服务,是如何实现的呢
2 Zookeeper典型应用场景
2.1 命名服务(Naming Service)
2.2 数据发布与订阅(配置中心)
2.3 集群管理与Master选举
2.4 分布式锁
2.5 分布式通知/协调
2.6 分布式队列
2.7 负载均衡
3 Zookeeper实现原理
3.1 ZooKeeper的4个目标
3.2 Paxos算法
3.3 ZooKeeper算法
4 ZooKeeper的API
参考
1 ZooKeeper是什么
1.1 分布式系统面临的问题
在分布式系统中,所有在同一台机器上的假设都不存在:因为网络是不可靠的。
在分布式环境中,由于网络的原因:
- 你对一个服务的调用失败了并不表示一定是失败的,可能是执行成功了,但是响应返回的时候失败了。
- 还有,A和B都去调用C服务,在时间上 A还先调用一些,B后调用,那么最后的结果是不是一定A的请求就先于B到达呢?
1.2 ZooKeeper概述
Google的三篇论文一直是分布式领域传阅的经典。根据MapReduce,于是我们有了Hadoop;根据GFS,于是我们有了HDFS;根据BigTable,于是我们有了HBase。而在这三篇论文里都提及Google的一个lock service---Chubby,于是我们有了Zookeeper。
伴随着Zookeeper有两篇论文:
- 一篇是Zab,就是介绍Zookeeper背后使用的一致性协议的(Zookeeper atomic broadcast protocol),
- 一篇就是介绍Zookeeper本身的。
Zookeeper:是一个分布式协调服务(a service for coordinating processes of distributed applications)。它提供了一项基本服务:分布式锁服务。
1.3 分布式锁服务,是如何实现的呢
- 它设计一种新的数据结构-Znode,并在在该数据结构的基础上定义了一些原语,也就是一些关于该数据结构的一些操作。我们暂且把它称作文件系统
- 由于ZooKeeper是工作在一个分布式的环境下,我们的服务是通过消息以网络的形式发送给分布式应用程序,所以还需要一个通知机制——Watcher机制。
上面这张图是理解ZooKeeper的关键,既展现了ZooKeeper内置的数据结构,又展现了典型应用场景。ZooKeeper内部实现了一个类似文件系统的树结构,每个节点被称为znode。znode可以看做是文件系统的中文件夹+文件。说它是文件夹,因为它下面可以放子节点;说它是文件,因为它本身存储了数据。
每个znode可以被客户端注册监听,当znode发生变化(存储的数据发生改变、被删除、子节点增加和删除时),ZooKeeper会通知客户端。
那么总结一下,ZooKeeper所提供的服务主要是通过:文件系统+watcher机制,来实现的。
2 Zookeeper典型应用场景
由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:
- 命名服务(Naming Service)
- 数据发布与订阅(配置中心)
- 集群管理与Master选举
- 分布式锁
- 分布式通知/协调
- 分布式队列
- 负载均衡
2.1 命名服务(Naming Service)
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,可以在如上图的NameService下创建一个znode,得到全局唯一的path,这个path就可以作为一个名称。
阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表:
- 服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。
- 服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目录下写入自己的URL地址。
注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
2.2 数据发布与订阅(配置中心)
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。如上图可以在configuration下创建znode
- 应用中用到的一些配置信息放到ZK上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。
- 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用。
- 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZK上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。
- 系统中有全局变量需要动态获取,只要将这些信息存放到指定的ZK节点上即可。
注意:在上面提到的应用场景中,有个默认前提是:数据量很小,但是数据更新可能会比较快的场景。
2.3 集群管理与Master选举
集群管理,主要是两点:一是机器的加入和退出,二是选举master。
集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:
- 集群中机器有变动的时候,牵连修改的东西比较多。
- 有一定的延时。
利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
- 客户端在节点 x 上注册一个Watcher,那么如果 x的子节点变化了,会通知该客户端。
- 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。
例如,监控系统在 /groupMembers节点上注册一个Watcher,以后每动态加机器,那么就往 /groupMembers下创建一个 EPHEMERAL类型的节点:/groupMembers/{hostname}. 这样,监控系统就能够实时知道机器的增减情况,至于后续处理就是监控系统的业务了。
Master选举则是zookeeper中最为经典的应用场景了。
在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建 /currentMaster 节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。
另外,这种场景演化一下,就是动态Master选举。这就要用到EPHEMERAL_SEQUENTIAL类型节点的特性了。
上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,就是允许所有请求都能够创建成功,但是得有个创建顺序,于是所有的请求最终在ZK上创建结果的一种可能情况是这样: /currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2 ,?/currentMaster/{sessionId}-3 ….. 每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点会马上小时,那么之后最小的那个机器就是Master了。
2.4 分布式锁
分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,所有客户端在它下面创建临时有序节点(CreateMode.EPHEMERAL_SEQUENTIAL),编号最小的获得锁,用完之后节点自动删除,下一个获得锁。
2.5 分布式通知/协调
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理
- 另一种心跳检测机制:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,大大减少系统耦合。
- 另一种系统调度模式:某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
- 另一种工作汇报模式:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合
2.6 分布式队列
队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。
第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。
第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。
2.7 负载均衡
这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ(kafka学习笔记:知识点整理)和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。这里以metaq为例如讲下:
生产者负载均衡:metaq发送消息的时候,生产者在发送消息的时候必须选择一台broker上的一个分区来发送消息,因此metaq在运行过程中,会把所有broker和对应的分区信息全部注册到ZK指定节点上,默认的策略是一个依次轮询的过程,生产者在通过ZK获取分区列表之后,会按照brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。
消费负载均衡:
在消费过程中,一个消费者会消费一个或多个分区中的消息,但是一个分区只会由一个消费者来消费。MetaQ的消费策略是:
- 每个分区针对同一个group只挂载一个消费者。
- 如果同一个group的消费者数目大于分区数目,则多出来的消费者将不参与消费。
- 如果同一个group的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务。
在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过 zookeeper watch消费者列表),然后重新进行负载均衡,保证所有的分区都有消费者进行消费。
3 Zookeeper实现原理
3.1 ZooKeeper的4个目标
ZooKeeper是基于如下4个目标来进行权衡和设计的
简单
分布式应用中的各个进程可以通过ZooKeeper的命名空间(Namespace)来进行协调,这个命名空间是共享的、具有层次结构的,更重要的是它的结构足够简单,像我们平时接触到的文件系统的目录结构一样容易理解。在ZooKeeper中每个命名空间(Namespace)被称为ZNode。
一个ZNode维护了一个状态结构,该结构包括:版本号、ACL变更、时间戳。每次ZNode数据发生变化,版本号都会递增,这样客户端的读请求可以基于版本号来检索状态相关数据。
冗余
ZooKeeper被设计为复制集群架构,每个节点的数据都可以在集群中复制传播,使集群中的每个节点数据同步一致,从而达到服务的可靠性和可用性。前面说到,ZooKeeper将数据放在内存中来提高性能,为了避免发生单点故障(SPOF),支持数据的复制来达到冗余存储,这是必不可少的。
有序
ZooKeeper使用时间戳来记录导致状态变更的事务性操作,也就是说,一组事务通过时间戳来保证有序性。基于这一特性。ZooKeeper可以实现更加高级的抽象操作,如同步等。
快速
ZooKeeper包括读写两种操作,基于ZooKeeper的分布式应用,如果是读多写少的应用场景(读写比例大约是10:1),那么读性能更能够体现出高效。
3.2 Paxos算法
假设集群每个节点具有一致的初始状态,那么我们只需要保证在集群启动后,每个节点都执行相同的操作序列,那么它们最后能得到一个一致的状态。
Paxos算法的作用是,保证每个节点都执行相同的操作序列。
Paxos算法通过投票来对写操作进行全局编号,同一时刻,只有一个写操作被批准;并发的写操作要去争取选票,只有获得半数选票的写操作才会被批准,因此任意时刻永远只会有一个写操作被批准。其他写操作竞争失败只好再发起一轮投票。在不断的投票过程中,所有写操作都被严格编号排序。假设一台机器接受了编号为100的写操作,之后又接受到编号为99的写操作,它马上能意识到自己数据不一致了,自动停止对外服务并和Leader同步状态。
Paxos算法白话版
有一个叫做Paxos的小岛,上面住了一批居民,岛上所有事情都由一群议员做决定。议员的总数确定,不能更改。岛上每次环境事务的变更都需要通过一个提议Proposal:
- 每个提议都有一个编号PID,该编号一直增长,不能倒退。
- 每个议员只会同意大于当前编号的提议,包括已生效的和未生效的。如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有人提过了。这里的当前编号是每个议员在自己记事本上面记录的编号,他不断更新这个编号。
- 每个提议都需要超过半数议员同意才能生效。
最开始的时候,议员的记事本上记录编号都为0。有一个议员发了一个提议:将电费设定为1元/度。他首先看了下记事本,恩,当前编号为0,那么我的这个提议的编号就是1,于是他给所有议员发消息:1号提议,设定电费1元/度。其他议员收到消息后查了一下记事本,哦,当前编号是0,这个提议可接受,于是他记录下这个提议并回复:我接受你的1号提议;同时他在记事本上记录:当前提议编号为1。发起提议的议员收到了超过半数的回复,立即给所有人发通知:1号提议生效!收到的议员会修改他的记事本,将1号提议由记录改为法令,当有人问他电费多少时,他会查看法令并告知对方:1元/度。
现在来看看同时有多个提议提出的情况。假设总共有三个议员S1-S3,S1和S2同时发起了一个提议:1号提议,设定电费。S1想设为1元/度,S2想设为2元/度。结果S3先收到了S1提议,于是他做了和前面同样的操作。紧接着他又收到了S2的提议,结果他一查记事本,咦,这个提议的编号小于等于我的当前编号1,于是他拒绝了这个提议:对不起,这个提议先前提过了。于是S2的提议被拒绝。假设S1发布的提议同时也被S1接受了,那么S1获得了超过半数的支持,于是S1正式发布了提议:1号提议生效。S2向S1或者S3打听并更新了1号法令的内容,然后他可以选择继续发起2号提议。
3.3 ZooKeeper算法
现在我们来一一对应Paxos算法与ZooKeeper的概念。下图是ZooKeeper的角色图,其中Observer暂时不管Zookeeper
- 议员——跟随者
- 居民——客户端
- 提议——znode状态改变(创建、删除、更改等)
- 提议编号PID——Zxid(ZooKeeper Transaction ID)
- 正式法令——所有znode及其数据
ZooKeeper实现的算法叫做FastPaxos,与Paxos算法的区别在于所有的提议必须通过Leader(这里类比为Paxos小岛的总统)提出,如果议员有自己的提议,必须发给总统由总统来提出。
于是算法分为两个问题:
- 一是怎么选出总统,这个后面再说;
- 二是这个ZooKeeper议会怎么运转。
Case 1
居民甲(Client)到某个议员(ZK Server)那里询问(Get)某条法令的情况(ZNode的数据),议员毫不犹豫地拿出他的记事本(local storage),查阅法令并告诉他结果,同时声明:我的数据不一定是最新的。你想要最新的数据?没问题,等着,等我找总统Sync一下再告诉你。
Case 2
居民乙(Client)到某个议员(ZK Server)那里要求政府归还他的一万元,议员让他在办公室等着,自己将问题反映给了总统,总统询问所有议员的意见,多数议员表示欠钱要还,于是总统发表声明,从国库中拿出一万元还债,国库总资产由100万变成99万。居民乙拿着钱回去了(Client函数返回)
Case 3
总统突然驾崩了,议员接二连三地发现联系不上总统,于是各自发表声明,推选新的总统,总统大选期间政府停业,拒绝居民的请求。
Leader选举算法
这里讲解ZooKeeper的默认算法FastLeaderElection算法。
- 首先,需要了解ZooKeeper的服务器有以下几种状态:LOOKING寻找leader状态,LEADING领导状态,FOLLOWING跟随者状态,OBSERVING观察者状态。
- 第二,选举过程需要进行多轮。每一轮选举中,机器会生成一张选票,然后群发给集群的其他机器。生成一张选票,那么机器保存的字段logicalClock/epoch会自增1。
- 第三,每个机器选择Leader的逻辑是:每台机器有一个zxid和id;选主机器会优先选择zxid最大的机器,当zxid相同时,选择id最大的机器。
FastLeaderElection. lookForLeader()算法逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 logicalclock++,表示是新一轮leader选举,它是一个内存值,服务器重启就会导致该值归0,所以如果服务器活得越久,这个值随着应该越大,每一轮选举会保持所有机器该值始终是其中相同的最大值。 2 推举自己作为leader,并将自己服务器上存储的最大zxid,自己的服务器id,自己的状态(looking)notify所有的服务器,告知大家我想当leader. 3 等待其他服务器的反馈消息,如果有消息回来,分为以下几个情况: 3.1 自己还在looking,该消息标记的服务器也在looking 消息的epoch<自己的logicalclock,表示这条消息是前面一轮的消息,于是回发一条消息告诉对方当前的机器的logicalclock和推举的leader和zxid 消息epoch>自己的logicalclock,表示对方已经开始新一轮选举了,更新logicalclock为epoch,清空接收到的所有服务器状态recvset.对比消息的zxid和本地的lastzxid,选取最大的作为leader,如果相同,则选取serverid最大的作为leader.然后sendNotifications()通知所有服务器我的选择。 消息epoch=自己的logicalclock,表示是同一轮选举,对比消息的zxid和本地的lastzxid,选取最大的作为leader,如果相同,则选取serverid最大的作为leader.如果返回的消息是最后选择,则sendNotifications()通知所有服务器我的选择,否则不理睬这条消息,不发送任何回应。 3.2 自己还在looking,该消息标记的服务器已经没有looking了 消息的epoch=自己的logicalclock,如果消息状态是leading,那么就认为他是leading,更改自己的状态返回。如果消息认为自己是leader,那么需要有1/2以上服务器认为自己是leader,就更改状态并返回。 消息的epoch<>自己的logicalclock,那么投票将加入到outofelection中,如果有1/2服务器以上的投票选择这条消息推荐的leader,那么更改自身的状态并返回。 3.3 自己没有looking,该消息标记的服务器还在looking 获得当前的leader信息,直接通知对方已经选择的leader. 3.4 自己没有looking,该消息标记的服务器没有looking 不做任何处理。 3.1 这种状态下: 如果收集到了所有服务器的投票, 如果此时收集的投票大于1/2服务器数,那么再等待一个时段,如果没有其他响应到来或者到来的响应没有新的选票产生。 此时看下此时选举出来的proposedLeader是否是自己,是则更改自己的状态为leading,否则更改为following,然后跳出选举阶段.如果不满足上面的两条条件,则继续等待消息。
4 ZooKeeper的API
Zookeeper的client是通过Zookeeper类提供的。Zookeeper的client api给我们提供以下这些API:
4.1 create
在给定的path上创建节点,这个path就像文件系统的路径,比如/myapp/data/1,
在创建节点的时候还可以指定节点的类型:
- 是永久节点,
- 永久顺序节点,
- 临时节点,
- 临时顺序节点。
这个节点类型是非常强大的:
- 永久节点:一经创建就永久保留了,就像我们在文件系统上创建一个普通文件,这个文件的生命周期跟创建它的应用没有任何关系。
- 临时节点:,当创建这个临时节点的应用与zookeeper之间的会话过期之后就会被zookeeper自动删除了。这个特性是实现很多功能的关键。比如我们做集群感知,我们的应用启动的时候将自己的ip地址作为临时节点创建在某个节点下面。当我们的应用因为某些原因,比如网络断掉或者宕机,它与zookeeper的会话就会过期了,过期后这个临时节点就删除了。这样我们就可以通过这个特性来感知到我们的服务的集群有哪些机器是活者的。
- 顺序节点:一般,如果我们在指定的path上创建节点,如果这个节点已经被创建了,则会抛出一个NodeExistsException的异常。如果我们在指定的路径上创建顺序节点,则Zookeeper会自动的在我们给定的path上加上一个顺序编号。这个特性就是实现分布式锁的关键。假设我们有几个节点共享一个资源,我们这几个节点都想争用这个资源,那我们就都向某个路径创建临时顺序节点。然后顺序最小的那个就获得锁,然后如果某个节点释放了锁,那顺序第二小的那个就获得锁,以此类推,这样一个分布式的公平锁就实现了。
除此之外,每个节点上还可以保存一些数据。
4.2 delete
删除给定节点。删除节点的时候还可以给定一个version,只有路径和version都匹配的时候节点才会被删除。有了这个version在分布式环境种我们就可以用乐观锁的方式来确保一致性。比如我们先读取一下节点,获得了节点的version,然后删除,如果删除成功了则说明在这之间没有人操作过这个节点,否则就是并发冲突了。
4.3 exists
这个节点会返回一个Stat对象,如果给定的path不存在的话则返回null。这个方法有一个关键参数,可以提供一个Watcher对象。Wathcer是Zookeeper强大功能的源泉。Watcher就是一个事件处理器,一个回调。比如这个exists方法,调用后,如果别人对这个path上的节点进行操作,比如创建,删除或设置数据,这个Wather都会接收到对应的通知。
4.4 setData/getData
设置或获取节点的数据,getData也可以设置Watcher
4.5 getChildren
获取子节点,可以设置Watcher
4.6 sync
zookeeper是一个集群,创建节点的时候只要半数以上的节点确认就认为是创建成功了,但是如果读取的时候正好读取到一个落后的节点上,那就有可能读取到旧的数据,这个时候可以执行一个sync操作,这个操作可以确保读取到最新的数据。
参考
[1] 随笔分类 - Zookeeper
[2] ZooKeeper学习笔记
[4] zookeeper体系架构