一、分布式协调技术
分布式协调技术主要解决分布式环境中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成“脏数据”的后果。这时,可能有人会说,写一个调度方法就可以了。说这句话的人可能对分布树还不是很了解。如果说这些进程全部都跑在一台机上的话,那确实就好办了,问题是他是在一个分布式的环境下,那什么是分布式呢?
在图中有三台机器,这三台机器通过网络将其连接起来,构成一个系统来为用户提供服务,我们把这样的一种系统,称之为分布式系统。
我们继续来分析,分布式系统中,如何对进程进行调度,假设在第一台机器上挂载了一个资源,然后三个物理进程都要竞争这个资源,但我们又不希望它们同时访问,这个时候我们就需要一个协调器,来让它们有序的访问这个资源,这个协调器就是我们经常所说的那个锁,比如,进程1在使用该资源的时候,会先去获得锁,进程1获得锁以后会对资源独占,进程1完成对资源的使用之后,就会释放资源,这时,其他进程就能够获取到该资源,通过这个锁机制,就能保证三个进程有序的访问临界资源,这种锁,叫做分布式锁。
二、分布式锁的实现
2.1 面临的问题
看了上图中的分布式环境之后,有人可能会觉得这不是很难,无非是原来在同一机器上的进程调度语句,通过网络实现在分布式环境中,是的,表面上可以这么说,但是问题就在网络这,所有在同一台机器上的假设都不存在,因为网络是不可靠的,比如,在同一台机器上的一个服务的调用如果成功,那就是成功,如果失败,那就是失败。但是在分布式环境下,由于网络的不可靠,你对一个服务调度失败了,那不一定就是失败,有可能成功了,但相应返回的时候失败了。还有,A和B都去调C服务,在时间上,A还先调用一些,B后调用,但是最后到达C的结果不一定是A先于B。还有在分布式系统中为了提高可靠性,往往会部署多套服务,但是如何在多套服务中达到一致性,这是分布式实现的一个大难题。
所以,分布式协调远比同一台机器上多个进程的调度要难的多,而且如果为每一个分布式应用都开发一个独立的协调程序,一方面,协调程序反复编写浪费,难以重用,另一方面,系统的开销比较大,会影响系统原有的性能。所以,急需一种高可用,高可靠的协调机制来用以协调分布式应用。
2.2 分布式锁的实现者
目前,在分布式协调技术就是Google的Chubby还有Apache的Zookeeper,他们都是分布式锁的实现者,但是zookeeper是开源的。
三、Zookeeper概述
zookeeper是一种为分布式应用所设计的,高可用,高性能且一致的开源协调服务。它提供了一项基本服务:分布式锁服务,由于其开源的特性,后来我们的开发者在分布式锁的基础上,摸索出了其他的使用方法:配置维护,组服务,分布式消息队列,分布式通知/协调等。
注意,zookeeper性能上的特点,决定了它能够用在大型的分布式系统当中。从可靠性来说,它并不会因为一个节点的错误而崩溃。除此之外,它严格的序列访问控制意味着复杂的控制源语可以应用在客户端上,zookeeper在一致性,可用性,容错性的保证,也是zookeeper的成功之处,它获得一切成功都与它采用的协议--Zab协议是密不可分的。
zookeeper在实现这些服务时,首先设计一种新的数据结构---Znode,然后在该数据结构的基础上定义了一些原语,也就是关于数据结构的一些操作。有了这些数据结构和原语还不够,因为我们的zookeeper是工作在一个分布式的环境下,我们的服务是通过消息以网络的形式发送给我们的分布式应用程序,所以还需要一个通知机制---Watcher机制。
总结:zookeeper所提供的服务是通过:数据结构+原语+watcher机制 三个部分来实现。
四、Zookeeper数据模型
4.1 ZooKeeper数据模型Znode
zookeeper树中的每个节点称之为-Znode。和文件系统的目录树一样,Zookeeper树中的每个节点可以拥有子节点。也有不同之处:
(1)引用方式
Znode是通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此它们必须由斜杠字符开头。除此之外,它们必须是唯一的,也就是说每一个路径只有一个表示,因此,路径不能改变。在Zookeeper中,路径由Unicode字符串组成,并且有一些限制,字符串“zookeeper”用以保存管理信息,比如关键配额信息。
(2)Znode结构
ZooKeeper命名空间中的Znode,兼具文件和目录两种特点。既像文件一样护着数据,源信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分,图中的每一个节点称之为Znode。每个Znode由3部分组成:
- stat:此为状态信息,描述该Znode的版本,权限等信息。
- data:与Znode关联的数据。
- children:该Znode下的子节点。
Zookeeper虽然可以关联一些数据,但并没有设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息,状态信息,汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。Zookeeper的服务器和客户端都被设计为严格检查并限制每个Znode数据大小之多1M,但常规使用应远小于此值。
(3)数据访问
Zookeeper中的每个节点存储的数据要被原子性操作。也就是读操作将获取与节点相关的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。
(4)节点类型
Zookeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
- 临时节点:该节点的声明周期依赖于它们的会话。一旦会话结束,临时节点被自动删除,当然也可以手动删除。虽然每个Znode都会绑定到一个客户端会话,但它们对所有的客户端还是可见的,另外Zookeeper的临时节点不允许拥有子节点。
- 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,才能被删除。
(5)顺序节点
当创建Znode的时候,用户可以在Zookeeper的路径的结尾添加一个递增的计数。这个计数对于此节点的父节点来说是唯一的,它的格式为"%10d"(10位数字,没有数值的数位用0补充,例如"0000000001")。当计数值大于232-1时,计数器将溢出。
(6) 观察
客户端可以在节点上设置watch,我们称之为监听器,当节点状态发生改变时(Znode的增删改)将会触发watch所对应的操作,当watch被触发时,Zookeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次,这样可以减少网络流量。
4.2 Zookeeper中的时间
zookeeper有多种记录时间的形式,其中包含以下几个主要属性:
(1)zxid
致使Zookeeper节点状态改变的每一个操作都将使节点接收到一个Zxid格式的时间戳,并且这个时间戳全局有序。也就是说,每个对节点的改变都将产生一个唯一的Zxid。如果Zxid1的值小于Zxid2的值,那么zxid1所对应的事件发生在Zxid2所对应的事件之前。实际上,Zookeeper的每个节点维护着三个Zxid的值,分别为:cZxid、mZxid、pZxid
- cZxid:是节点创建时间所对应的Zxid格式时间戳。
- mZxid:是节点的修改时间所对应的Zxid格式时间戳。
实际上,Zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch。低32位是个递增计数。
(2)版本号
对节点的每一个操作都将致使这个节点的版本号增加,每个节点维护着三个版本号,它们分别为:
- version:节点数据版本号
- cversion:子节点版本号
- aversion:节点所拥有的ACL版本号
4.3 Zookeeper节点属性
一个节点拥有其表示状态的许多重要属性
五、Zookeeper服务中断操作
zookeeper有9个基本操作
更新zookeeper操作是有限制的,delete或setData必须明确要更新的Znode版本号,我们可以调用exists找到。如果版本号不匹配,更新将会失败。
更新zookeeper操作是非阻塞式的,因此客户端如果失去了一个更新(由于另一个进程在同时更新这个Znode),他可以在不阻塞其他进程执行的情况下,选择重新尝试,或进行其他操作。
尽管Zookeeper可以被看作是一个文件系统,但是处于便利,摒弃了一些操作系统的操作原语。因为文件系统非常的小并且易于读写,所以不需要打开,关闭,或是其它操作。
六、触发器
(1)watch概述
Zookeeper可以为所有的读操作设置watch,这些读操作包括:exists(),getChildren(),及getData()。watch事件是一次性的触发器,当watch的对象状态发生改变时,将会触发此对象上watch所对应的事件,watch事件将被异步的发送给客户端,并且Zookeeper为watch机制提供了有序的一致性保证。理论上,客户端接收watch事件的时间要快于看到watch对象状态变化的时间。
(2)watch类型
Zookeeper所管理的watch可以分为两类:
- 数据watch(data watches):getData和exists负责设置数据watch
- 孩子watch(child watches):getChildren负责设置孩子watch
我们可以通过操作返回的数据来设置不同的watch:
- getData和exists:返回关于节点的数据信息。
- getChildren:返回孩子列表。
因此,
- 一个成功的setData操作将触发Znode的数据watch
- 一个成功的create操作将触发Znode的数据watch以及孩子的watch
- 一个成功的delete操作将触发Znode的数据watch以及孩子的watch
(3)watch注册与触发
- exists操作上的watch,在被监视的Znode创建,删除或数据更新时被触发。
- getData操作上的watch,在被监视的Znode被删除或数据更新时被触发。
- getChildren操作上的watch,在被监视的Znode的子节点创建或删除或是其自身被删除时触发。可以通过查看watch类型来区分是Znode,还是他的子节点被删除;NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。
Watch由客户端所连接的Zookeeper服务器在本地维护,因此watch可以非常容易的设置,管理和分配。当客户端连接到一个新的服务器时,任何的会话事件都可能会触发watch。当服务器断开连接时,watch将不会被接收,当客户端重新建立连接时,任何先前注册过的watch都会被重新注册。
(4)需要注意的几点
Zookeeper的watch实际上需要处理两类事件:
- 连接状态事件
这类事件不需要我们注册,也不需要我们连续触发,我们只要处理就行了 - 节点事件
节点的建立,删除,数据的修改。它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况。
上面的两类事件都在watch中处理,也就是重载的process(Event event)
节点事件的触发,通过exists,getData或getChildren来处理这类事件由双重作用:
- 注册触发事件。
- 函数本身的功能。
函数的本身功能又可以用异步的回掉函数来实现,重载processResult()过程中处理函数本身的的功能。
七、Zookeeper应用举例
一、分布式锁的应用场景
在分布式锁服务中,有一种最典型的应用场景,就是通过对集群进行master选举,什么是单点故障?通常分布式系统采用主从模式,就是一个主机连接多个处理节点。主节点负责分发任务,从节点负责处理任务,当我们的主节点发生故障时,那么整个系统就瘫痪了,我们把这个故障叫做单点故障。
二、传统解决方案
传统方式是采用一个备用节点,这个备用节点定期给当前主节点发送ping包,主节点收到ping包以后,向备用节点回复ack,当备用节点收到回复时,就会认为当前主节点还活着,让他继续提供服务。
当主节点挂了,这时候备用节点就收不到回复了,然后他就认为主节点接替他成为主节点。
但是这种方式就是有一个隐患,就是网络问题,来看下网络问题会造成什么后果。
也就是说,主节点并没有挂,只是在回复的时候发生网络故障,这样我们的备用节点同样收不到回复,就会认为主节点挂了,然后备用节点将他的master启动起来,这样,分布式就有两个主节点,--双master,这样从节点就会将他所做的事情一部分汇报给主节点,一部分汇报给从节点,这样服务就全乱了。为了防止这种情况,我们引入zookeeper,它虽然不能避免网络故障,但是他能保证每时每刻只有一个master。
三、Zookeeper解决方案
(1)Master启动
引入了zookeeper以后,我们启动了两个主节点,A和B,他们启动以后,都想zookeeper去注册一个节点。我们假设主节点A锁注册节点是master-0001,主节点B锁的注册节点是master-0002,注册完以后进行选举,编号最小的节点将会在选举中获胜获得锁成为主节点,也就是主节点A会获得锁成为主节点,主节点B会成为备用节点,通过这种方式就完成了对master进程的调度。
(2)Master故障
如果主节点A挂了,这时候他所注册的节点将会自动删除,Zookeeper会自动感知节点的变化,然后再次发出选举,这时候主节点B将会在选举中获胜,代替A成为主节点。
(3)Master恢复
如果主节点恢复 ,他会再次向zookeeper注册为一个节点,这时候它注册的节点将会是master0003,zookeeper会感知节点的变化再次发出选举,这时候B会在选举中获胜担任主节点。A担任备用节点。