一、 Paxos
基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、垮、重启,消息可能会延迟、丢失、重复, Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。因此从20世纪80年代起对于一致性算法的研究就没有停止过。
Paxos这里有一则广为流传的趣事:
Lamport大牛在他Paxos的第一个版本早在1990年就提交给ACM的评审委员会了, 但是当时没有人理解他的算法,主编回执他的稿子建议他用数学而不是神话描述他的算法,他们才会考虑接受这篇论文。Lamport大牛很生气:”为什么搞理论的这群人一点幽默感也没有呢?” 他拒绝修改而且撤了这篇文章。
1996年微软的Butler Lampson在WDAG96上提出了重新审视这篇文章,因为他读懂了。
1997年MIT的Nancy Lynch在WDAG97上根据原文重新改写了这篇文章 叫做”Revisiting the Paxos Algorithm” 在本文中她们”代替”Lamport用数学形式化的定义并证明了Paxos。
于是在1998年的ACM TOCS上 这篇迟到了9年的paper终于被接受了
后来2001年,Lamport大牛也作出了让步,他用简单的语言而不是神话故事,重述了原文,但是通篇还是没有数学符号。 L大牛甚为固执, 据他自己说,他检查过了自己的语言并没有歧义,不需要数学来描述。
实际上对于一般的开发人员,我们并不需要了解Paxos所有细节及如何实现,只需要知道Paxos是一个分布式选举算法就够了。
二、 ZooKeeper
ZooKeeper是一个构建在Paxos算法上的高可用的分布式数据管理与系统协调框架,提供了一系列原语集,更上层的应用可以用它来实现同步,配置管理,名称服务,Master选举,分布式锁,分布式队列等。
1. zookeeper提供如下服务保证:
l 顺序一致性——client的updates请求都会根据它发出的顺序被顺序的处理;
l 原子性——一个update操作要么成功要么失败,没有其他可能的结果;
l 一致性的镜像——client不论连接到哪个server,展示给它都是同一个视图;
l 可靠性——一旦一个update被应用就被持久化了,除非另一个update请求更新了当前值
l 及时性——客户端所看到的系统在一个时间范围内是最新的
2. zookeeper的设计目标
l 简单——ZooKeeper允许分布式进程之间通过一个共享的层级命名空间来相互协作,这个命名空间以类似于文件系统的方式组织起来。命名空间中的数据单元被称为znode,它相当于文件或者目录。与典型文件系统不同的是,文件系统用于持久化存储,而ZooKeeper的数据是保持在内存中的,这意味着ZooKeeper能达到很高的吞吐量和低延迟。
l ZooKeeper的实现十分注重高性能、高可用性,和严格的顺序访问。高性能使得ZooKeeper能用于大规模分布式系统,高可用性能避免单点故障,严格的顺序化意味着复杂的同步操作可以在客户端实现。
l 集群化——ZooKeeper本身也是有集群化的。如下图:
客户端与单个服务端相连,它维持一个TCP连接,在其上发送请求,获得响应,获得监控事件和发送心跳检测。如果到服务端的TCP连接断了,客户端会连接另一个服务端。组成ZooKeeper服务的每个服务端都知道其它服务端的存在,它们维护一个服务端状态的内存镜像,连同事务日志和快照保存在持久化存储中,只要大部分服务端可用,ZooKeeper服务就可用。
l 顺序化——ZooKeeper为每个更新操作都标记一个号码以反映事务的顺序。后来的操作使用这个顺序来实现高度的抽象,比如同步原语。
l 快速——这个特性在以读操作为主的工作中尤为明显。ZooKeeper应用程序运行于数以千计的机器中,当读操作与写操作的比例为10:1时,ZooKeeper能获得最佳性能。
三、 数据模型和层级命名空间
ZooKeeper提供的命名空间极像一个标准的文件系统,一个名字是一个以/分隔的路径元素的序列,ZooKeeper命名空间的每个节点通过路径来标识。
1. 节点和临时节点
与标准文件系统不同,ZooKeeper命名空间中的所有节点都可以有数据和子节点,这就像文件系统中允许一个文件同时是一个目录。(ZooKeeper被设计用来存储管理服务的数据:状态信息、配置、位置信息等,所以每个节点存储的数据通常很小,几字节到几K字节不等。)我们使用znode这个术语来表示ZooKeeper的数据节点。
zonde维持一个stat结构,它包含数据变化的版本号、ACL变化和时间戳,以允许cache校验和协调化的更新。每当znode的数据变化时,版本号将增加。一个客户端收到数据时,它也会收到数据的版本号。
保存在每个znode中的数据都是自动读写的。读操作获取znode的所有数据,写操作替换掉znode的所有数据。每个节点有一个访问控制表(ACL)来限制谁能做哪些操作。
ZooKeeper也有临时节点的概念,这些znode只存在于创建znode的会话中。当会话结束,这些节点也就被删除了。
2. 条件化更新和监控
ZooKeeper支持监控的概念。客户端可以在zonode上设置一个观察员。这个观察员会在znode发生变化时触发和移除。当观察员被触发时,客户端会接收到一个说明znode发生变化的包。如果客户端和服务端的连接断了,客户端会收到一个本地的通知。简单的API
四、 Zookeeper API
ZooKeeper的一个设计目标是易于编程。所以,它只支持如下操作:
在命名空间的某个位置创建一个节点
l delete
删除一个节点
l exists
测试某位置的节点是否存在
l get data
从一个节点读取数据
l set data
往一个节点写数据
l get children
获取一个节点的子节点列表
l sync
等待数据被传播以同步数据
五、 zookeeper实现
l 领导者(Leader) : 领导者不接受client的请求,负责进行投票的发起和决议,最终更新状态。
l 跟随者(Follower): Follower用于接收客户请求并返回客户结果。参与Leader发起的投票。
l 观察者(observer): Oberserver可以接收客户端连接,将写请求转发给leader节点。但是Observer不参加投票过程,只是同步leader的状态。Observer为系统扩展提供了一种方法。
l 学习者 ( Learner ) : 和leader进行状态同步的server统称Learner,上述Follower和Observer都是Learner。
2. 下图从较高层次说明了ZooKeeper的构成:
集群数据库是存在于内存中的数据库,保存命名空间的所有数据。更新操作会被记录到硬盘中以便恢复,写操作先被序列化到硬盘中,再应用到内存数据库中。通常Zookeeper由2n+1台servers组成,只要有n+1台(大多数)server可用,整个系统保持可用。
对于follower收到的client的请求,对于读操作,由follower的本地内存数据库直接给client返回结果;对于会改变系统状态的写操作,则交由Leader进行提议投票,超过半数通过后返回结果给client:
Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当leader被选举出来,且大多数server的完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态。
广播模式需要保证proposal被按顺序处理,因此zk采用了递增的事务id号(zxid)来保证。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64为的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch。低32位是个递增计数。 当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的server都恢复到一个正确的状态。
3. 选举和同步过程:
zk的实现中用了基于paxos算法(主要是fastpaxos)的实现。具体如下:
1) 每个Server启动以后都询问其它的Server它要投票给谁。
2) 对于其他server的询问,server每次根据自己的状态都回复自己推荐的leader的id和上一次处理事务的zxid(系统启动时每个server都会推荐自己)
3) 收到所有Server回复以后,就计算出zxid最大的哪个Server,并将这个Server相关信息设置成下一次要投票的Server。
4) 计算这过程中获得票数最多的的sever为获胜者,如果获胜者的票数超过半数,则改server被选为leader。否则,继续这个过程,直到leader被选举出来。
此外恢复模式下,如果是重新刚从崩溃状态恢复的或者刚启动的的server还会从磁盘快照中恢复数据和会话信息。(zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复) 选完leader以后,zookeeper就进入状态同步过程。
状态同步过程:
1) leader就会开始等待server连接
2) Follower连接leader,将最大的zxid发送给leader
3) Leader根据follower的zxid确定同步点
4) 完成同步后通知follower 已经成为uptodate状态
5) Follower收到uptodate消息后,又可以重新接受client的请求进行服务了
六、 性能数据
1. 节点大小
配置文件中的jute.maxbuffer属性表示节点可存的数据大小,default=0xfffff=1MB,如果超出该值则会引起ZooKeeper系统不稳定,甚至崩溃,直接重启无效,所以禁止创建数据超过1MB的节点;鉴于超过10k的节点数据对性能的较大影响,建议数据大小尽可能控制在10k之内。
2. 创建销毁读取性能(引自KM)
3. 综合读写性能曲线
七、 应用场景
1. 命名服务(Naming Service)
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的znode,所有应用从中读取,避免写死。
2. 分布式通知/协调
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理
3. 分布式锁
这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
l 所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
l 控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
4. 分布式队列
简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。
第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去/taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。