引言
Zookeeper最早诞生于雅虎研究院,后捐赠给Apche,于2010年11月正式成为Apache的顶级项目。很多人认为Zookeeper像HDFS,MapReduce一样是谷歌论文的产物,即是Chubby的开源实现,毕竟从API来看它们的差别就是acquire/release和open/close,且主要数据结构极其类似。其实不然,从paper中来看Zookeeper其实是一个无阻塞的分布式协调中心,这与Chubby的分布式锁的定义就完全不同。其次Zookeeper实际实现的是一个功能强大的同步原语,但它并不是一个特定的原语,因为实现功能更强大的原语的服务可用于实现功能更弱的原语,所以使得开发人员可以在客户端根据zk提供的接口实现满足自身需求的特化原语,这也是zk有如此广泛应用的一个重要前提。
这篇文章是对zookeeper论文的一个总结,确实通过这篇paper学到了很多以前未曾注意的东西,所以从中筛选几个重要的点详细说一说,巩固的同时也给不想看paper的读者提供一点便利。目前决定写两个方面,即一致性
与应用
。
一致性
我在以前的文章中说过,好的设计是业务驱动的,技术是用来服务业务的。而Zookeeper的一致性就深深的体现了这个观点,我们知道在一个分布式系统中我们需要在一致性和可用性之间做出抉择,但是通常我们都不能完全的抛弃另一方,所以就有了Base理论这样的观点,即在C与A之间保持平衡,C的最终选择就是最终一致性(Eventually consistent
),也就是这里成了分布式系统中业务驱动最为几点之一,因为一致性的设计取决于用户想要看到什么。而一致性的实现决定了是否可以从副本获取我们需要的数据,而“好”的一致性可以让系统的读负载线性提升,zk的设计目标就是针对与读多写少的场景,paper中给出了一个Fetching Service的实际的数据,每秒读写的比例在100:1到10:1之间徘徊。
所以zk对于一致性的设计为:
Linearizable writes
:所有更新ZooKeeper状态的请求都是可线性化的,并且遵循优先级。FIFO client order
:来自给定客户端的所有请求均按照客户端发送的顺序执行。
首先我们看看Chubby和zk读写操作的处理过程:
name | 读请求 | 写请求 |
---|---|---|
Chubby | 由master和client cache处理 | master处理(cache失效) |
Zookeeper | 在每个副本上本地处理读请求 [5]2.3 | leader处理 |
我们可以看到它们之间的区别就是读请求,Chubby请求mater(为了性能在客户端缓存),而zk则直接请求副本。因为Chubby的读写均基于master,所以实际是实现了一个线性一致性,这也是分布式系统中最强的一致性保证,即所有操作均为线性。而zk的读操作可以从副本出直接得到,着也引出了一个问题,即因为写操作中获得大多数的ack就记为写操作成功,此时一部分服务器还没有刚写入的数据,这也意味着读取可能会出现旧数据,这不是违反了FIFO client order吗?这也是zk一个设计的精妙之处,对于数据实时性要求不严格的地方可以使用这种方法,对于要求严格的地方我们可以使用一种特定的方法来达到从客户的角度来看的FIFO。
当然zk中从数据的角度
来看其实只保证读一致性(如果一个进程已经检测到对象的特殊值,那么后续的访问都不会返回任何历史值),只是使用了一些黑魔法达到了从客户的角度
看到的FIFO。
首先我们要时刻记住读取的数据在全局来看不一定是最新的,因为可能在sync以后的读操作以后又在其他Client写入了一条数据,当然这也满足zk的一致性,即对每一个客户来说的FIFO。具体一点:ClientA写入了一个值,然后立马读取,zk保证一定可以读取到ClientA刚刚写入的值;而如果ClientB写入了一个值,ClintA立马读取,zk并不保证可以看到写入。而zk给每一个写入后的状态一个唯一自增的zxid,并通过写操作的答复告知客户端,客户端之后的读操作都会携带这个zxid,所以读操作可以和上一个写操作的zxid作比较判断此次读取是否为旧数据,以此对于此次读操作而言的最新的数据,当然这就保证了FIFO Client order。但是我要想得到全局此时的最新数据呢?
也有办法,具体的操作就是在需要保证强一致性的读前加一条Sync(空的写操作),来得到目前最新的状态,这样我们就可以达到目标的一致性了(这应该就是所谓的数据角度一致性和用户角度一致性的区别了,此处就是典型的用户角度的一致性,实际数据并不一致,通过一些方法让Client认为一致)。当然使用watcher也可以保证读取到最新的数据(可能在通知是zxid是5,读到的是6,但一定是新于通知的那个时间点)。
还有一点想说,就是在6.824的zk这一课中,教授花了半个多小时着重描述了强一致性,以前对于强一致性的理解就是读写一致性,保证读写均为线性。但是课程中给出了一种根据日志来判断强一致性的方法,其实就是几个约束,即:
- 如果一个操作在另一个操作开始前结束,先结束的操作会先落地到这个历史记录中。
- 某个读请求看到了一个特定的写入值,读请求必然是在对应的最近的写请求之后进来的。
- 不允许不同的client看到不同的历史记录或者是同一时刻下所保存系统内的数据不同,所有的client经历的是同一个顺序的操作流。
- 强一致性不会提供过时的数据。
判断的方法就是在数据流之间画上有向边,如果操作流之间出现了环,即不满足强一致性的定义。具体可以去看课程中举的例子。
应用
因为zk提供的是一个容错的分布式协调中心,所以我们可以用zk提供的接口来干很多事情,以前我以为它最大的作用就是协助选主和分布式锁,原来有更多其他的用途:
Configuration Management
配置管理:实现一个全局的动态配置,把配置存在一个znode中,其他进程可以通过观测(watcher)来获取最新数据。Rendezvous
回合:其实我把它理解为选主,将主节点信息放在一个znode,供工作节点找到主节点。Group Membership
组关系:因为我们可以观察到一个会话,也就是一个znode的状态,所以如果所有的组成员都为一个znode的子节点,这样我们可以轻松的获取组成员列表,可以观察到成员的退出或加入。Simple Locks
简单锁:就是分布式锁,但是可能出现羊群效应。Simple Locks without Herd Effect
避免羊群效应的锁:通过在一个znode下创建临时节点,每一个节点watch上一个节点,这样可以避免大量锁的争用:Read/Write Locks
读写锁:其实和上一个实现差不多。Double Barrier
双重屏障:不知道我理解的对不对,我觉的论文中对这个的描述比较像count down latch。就是对一个znode设置一个阈值,到达以后大家一起开始计算(可能是在等待某些资源),同样所有的进程都执行完后才退出。
总结
zk的paper中花费大量的篇幅去描述它到底是一个什么样的东西,对于实现的部分描述的并不详细,比如ZAB协议,几乎可以说是一笔带过,但是已有的部分确实足以描述其具体的运行原理。zk是一个成功的系统,但并不代表着它的设计深奥到一般人看不懂,我们可以从中学到很多关于一致性和性能取舍的选择。可以说zk消除了构建分布式系统时很大一部分痛苦,但是针对于读写相同或者写大于读时zk就不再合适,也许此时实现一个分布式一致性协议库,在应用层进行调用是一个更好的选择,因为写请求多时大量的数据不仅要从Client传递到zk,还要从zk master传递到replication,且我们也不必从replication直接读取,此时实现一个底层的协议库不仅可以减少通信,还避免了zk对于维护client order要进行的一些内部操作(zxid,数据结构),所以还是要选择应用场景才是。
参考:
- [1]博文《Zookeeper VS Chubby》
- [2]博文《什么是ZooKeeper?》
- [3]博文《经典分布式论文阅读:Zookeeper》
- [4]博文《分布式协调神器ZooKeeper之整体概述》
- [5]论文《ZooKeeper: Wait-free coordination for Internet-scale systems》