一、zookeeper介绍
Q:什么是分布式协调技术
A:分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成”脏数据”的后果。
Q:什么是分布式系统
A:建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。内聚性是指每一个数据库分布节点高度自治,有本地的数据库管理系统。透明性是指每一个数据库分布节点对用户的应用来说都是透明的,看不出是本地还是远程。
在分布式数据库系统中,用户感觉不到数据是分布的,即用户不须知道关系是否分割、有无副本、数据存于哪个站点以及事务在哪个站点上执行等。
Q:分布式协调技术如何协调分布式系统对临界资源的访问
A:在分布式系统中,多个机器希望访问同一个临界资源,我们不希望他们同时进行访问,所以我们需要一个协调器让他们有序的来访问这个资源。
这个协调器就是我们经常提到的那个锁,也就是分布式锁。
比如说”进程-1”在使用该资源的时候,会先去获得锁,”进程1”获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,”进程1”用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。
这个分布式锁也就是我们分布式协调技术实现的核心内容。
Q:分布式锁实现的难点
A:之所以需要实现分布式锁,是因为和普通的同一机器上进程调度不同,分布式系统是基于网络的,所有在同一台机器上的假设都不存在:因为网络是不可靠的。
我们无法知道对其他机器上服务调用的成功或者失败的情况,你调用失败了不一定真正执行失败了,可能由于网络原因返回失败了。我们也无法控制调用的先后顺序,例如AB调用C,A先调用B后调用,但是C处理时不能保证A比B先处理。因为网络原因不能保证A比B先到达。
分布式协调技术框架ZooKeeper,他是分布式锁的实现者
ZooKeeper概述
ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等。
ZooKeeper数据模型
Znode
ZooKeeper拥有一个层次的命名空间,这个和标准的文件系统非常相似。
Znode结构是一种树形层次结构,每个节点可以拥有子节点。这里的节点我们称之为Znode
引用方式
Zonde通过路径引用。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串”/zookeeper”用以保存管理信息,比如关键配额信息。
Znode结构
ZooKeeper命名空间中的Znode,兼具文件和目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。图中的每个节点称为一个Znode。 每个Znode由3部分组成:
1. stat:此为状态信息, 描述该Znode的版本, 权限等信息
2. data:与该Znode关联的数据
3. children:该Znode下的子节点
ZooKeeper虽然可以关联一些数据,但并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。ZooKeeper的服务器和客户端都被设计为严格检查并限制每个Znode的数据大小至多1M,但常规使用中应该远小于此值。
数据访问
ZooKeeper中的每个节点存储的数据要被原子性的操作。也就是说读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。
节点类型
ZooKeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
1. 临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话(Session)结束,临时节点将被自动删除,当然可以也可以手动删除。虽然每个临时的Znode都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,ZooKeeper的临时节点不允许拥有子节点。
2. 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。
顺序节点
当创建Znode的时候,用户可以请求在ZooKeeper的路径结尾添加一个递增的计数。这个计数对于此节点的父节点来说是唯一的,它的格式为”%10d”(10位数字,没有数值的数位用0补充,例如”0000000001”)。当计数值大于2^32-1时,计数器将溢出。
观察
客户端可以在节点上设置watch,我们称之为监视器。当节点状态发生改变时(Znode的增、删、改)将会触发watch所对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次,这样可以减少网络流量。
ZooKeeper中的时间
ZooKeeper有多种记录时间的形式,其中包含以下几个主要属性:
1. Zxid
致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个Zxid格式的时间戳,并且这个时间戳全局有序。也就是说,也就是说,每个对节点的改变都将产生一个唯一的Zxid。如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者三个Zxid值,为别为:cZxid、mZxid、pZxid。
* cZxid: 是节点的创建时间所对应的Zxid格式时间戳。
* mZxid:是节点的修改时间所对应的Zxid格式时间戳。
* pZxid:是当前节点的子节点的最近一次创建/删除的时间戳。
实现中Zxid是一个64为的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个 新的epoch。第32位是个递增计数。
2. 版本号
对节点的每一个操作都将致使这个节点的版本号增加。每个节点维护着三个版本号,他们分别为:
1. version:节点数据版本号
2. cversion:子节点版本号
3. aversion:节点所拥有的ACL版本号
其他节点属性
ephemeralOwner:会话拥有者id,如果是临时节点则为会话的sessionID如果是永久节点则为0
dataLength:节点数长度
numChiledren:节点下的子节点数
ZooKeeper服务中的操作
create:创建子节点(必须有父节点)
delete:删除节点(必须没有子节点)
exists:测试节点是否存在并返回元数据
getACL/setACL:获取/设置节点的ACL
getChildren:获取节点下所有子节点列表
getData/setData:获取/设置节点数据
sync:使客户端的Znode视图与Zookeeper同步
限制
由于Znode操作均为原子操作,在更新其数据时必须附带上个版本的版本号,如果版本号不匹配则会更新失败。如果由于同时更新导致失败,客户端丢失了一次更新,由于其非阻塞的特性可以在不阻塞其他进程的情况下选择重试或者是其他操作(watch通知)
zookeeper虽然可以被看做一个文件系统但是由于其文件非常小而且是整体读写的,所以不需要打开关闭或者寻址操作。
Watch触发器
ZooKeeper可以为所有的读操作设置watch,这些读操作包括:exists()、getChildren()及getData()。watch事件是一次性的触发器,当watch的对象状态发生改变时,将会触发此对象上watch所对应的事件。watch事件将被异步地发送给客户端,并且ZooKeeper为watch机制提供了有序的一致性保证。理论上,客户端接收watch事件的时间要快于其看到watch对象状态变化的时间。
watch类型
ZooKeeper所管理的watch可以分为两类:
1. 数据watch(data watches):getData和exists负责设置数据watch
2. 孩子watch(child watches):getChildren负责设置孩子watch
我们可以通过操作返回的数据来设置不同的watch:
1. getData和exists:返回关于节点的数据信息
2. getChildren:返回孩子列表
所以
1. 一个成功的setData操作将触发Znode的数据watch
2. 一个成功的create操作将触发Znode的数据watch以及孩子watch
3. 一个成功的delete操作将触发Znode的数据watch以及孩子watch
watch的注册及触发
1. exists操作上的watch,在被监视的Znode创建、删除或数据更新时被触发。
2. getData操作上的watch,在被监视的Znode删除或数据更新时被触发。在被创建时不能被触发,因为只有Znode一定存在,getData操作才会成功。
3. getChildren操作上的watch,在被监视的Znode的子节点创建或删除,或是这个Znode自身被删除时被触发。可以通过查看watch事件类型来区分是Znode,还是他的子节点被删除:NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。
补充:
Watch由客户端所连接的ZooKeeper服务器在本地维护,因此watch可以非常容易地设置、管理和分派。当客户端连接到一个新的服务器时,任何的会话事件都将可能触发watch。另外,当从服务器断开连接的时候,watch将不会被接收。但是,当一个客户端重新建立连接的时候,任何先前注册过的watch都会被重新注册。
注意事项
Zookeeper的watch实际上要处理两类事件:
1. 连接状态事件(type=None, path=null)
这类事件不需要注册,也不需要我们连续触发,我们只要处理就行了。
2. 节点事件
节点的建立,删除,数据的修改。它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况。
上面2类事件都在Watch中处理,也就是重载的process(Event event)
节点事件的触发,通过函数exists,getData或getChildren来处理这类函数,有双重作用:
1. 注册触发事件
2. 函数本身的功能
函数的本身的功能又可以用异步的回调函数来实现,重载processResult()过程中处理函数本身的的功能。
使用ZooKeeper解决单点故障
通过对集群进行Master选举,来解决分布式系统中的单点故障。
单点故障:通常分布式系统采用主从模式,就是一个主控机连接多个处理节点。主节点负责分发任务,从节点负责处理任务,当我们的主节点发生故障时,那么整个系统就都瘫痪了,那么我们把这种故障叫作单点故障。
传统解决模式:传统方式是采用一个备用节点,这个备用节点定期给当前主节点发送ping包,主节点收到ping包以后向备用节点发送回复Ack,当备用节点收到回复的时候就会认为当前主节点还活着,让他继续提供服务。当主节点挂了,这时候备用节点收不到回复了,然后他就认为主节点挂了接替他成为主节点。
这种模式会有一种隐患,那就是主节点受到备用节点的控制,如果网络出现问题导致备用节点无法ping通主节点,即使主节点没有挂他也会将自己的master实例启动起来,这样就造成了双主节点,这样从节点一部分数据到某一个主节点上,另一部分到另一个主节点上,服务就乱了。
ZooKeeper解决方案:
设主节点机器为A,备用主节点机器为B,二者向zookeeper注册的节点为Node1和Node2。注册完以后进行选举,编号最小的节点将在选举中获胜获得锁成为主节点。
在A节点挂了的时候,它注册的节点将会被自动删除,ZooKeeper会自动感知节点的变化,然后再次发出选举,这时候B机器(Node2)替代”主节点-A”成为主节点。
当A恢复了的时候,会重新注册一个节点Node3,这时候ZooKeeper感知节点变化重新发起选举,B由于节点编号较小获得锁继续担任主节点,A节点成为备用节点。(也可以根据其他选举算法,但是只会存在一个主节点)
Q:故障转移到了ZooKeeper身上
当我们依赖于ZooKeeper对主节点进行控制时,虽然看似没问题,但是实际上,只是把故障转移到了ZooKeeper身上而已。如果ZooKeeper是单点的,如果zookeeper本身挂了那么我们在分布式系统中引入ZooKeeper也就失去了意义。这要求ZooKeeper在其实现的过程中要做一些可用性和恢复性的保证。
ZooKeeper运行模式
单点模式和复制模式,单点模式就是我们上面说的,可能会产生故障。
在生产环境中我们都使用的复制模式,ZooKeeper是通过复制来提高其可用性的。
ZooKeeper服务只要有半数以上还在运行都能保障服务可以继续(由于其选举算法导致的半数限制,只要半数以上必定最少有一台机器会保存最新的状态,那么这台机器就是我们的Leader)
ZK集群服务是多个机器提供Zookeeper服务,该集群有一个Leader,多个Follower客户端可以连接任意ZooKeeper服务节点来读写数据。
zookeeper通过以下两条基本保证来实现数据的一致性:
1. 全局串行化所有的写操作
2. 保证同一客户端的指令被FIFO执行(以及消息通知的FIFO)
所有的读请求由Zk Server 本地响应,所有的更新请求将转发给Leader,由Leader实施。
zookeeper为了保证可恢复性,包含了一个内存数据库,存放整个Data Tree,为了恢复,更新会被记录到磁盘,并且写在被应用到内存数据库之前,先被序列化到磁盘。
Q:保证ZooKeeper服务的高可用性就需要采用分布式模式,来冗余数据写多份,写多份带来一致性问题,一致性问题又会带来性能问题,那么就此陷入了无解的死循环。zookeeper是如何进行处理的
ZooKeeper在分区容错性和可用性上做了一定折中,这和CAP理论是吻合的。ZooKeeper从以下几点保证了数据的一致性:
1. 顺序一致性
来自任意特定客户端的更新都会按其发送顺序被提交。也就是说,如果一个客户端将Znode z的值更新为a,在之后的操作中,它又将z的值更新为b,则没有客户端能够在看到z的值是b之后再看到值a(如果没有其他对z的更新)。
2. 原子性
每个更新要么成功,要么失败。这意味着如果一个更新失败,则不会有客户端会看到这个更新的结果。
3. 单一系统映像
一个客户端无论连接到哪一台服务器,它看到的都是同样的系统视图。这意味着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统状态不会比在之前服务器上所看到的更老。当一台服务器出现故障,导致它的一个客户端需要尝试连接集合体中其他的服务器时,所有滞后于故障服务器的服务器都不会接受该连接请求,除非这些服务器赶上故障服务器。
4. 持久性
一个更新一旦成功,其结果就会持久存在并且不会被撤销。这表明更新不会受到服务器故障的影响。