一、概念
Zookeeper是一个开源的分布式协调服务,其设计目标是将那些复杂的且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一些列简单的接口提供给用户使用。其是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/发布、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。其可以保证如下分布式一致性特性。
① 顺序一致性
从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中去。
② 原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,即整个集群要么都成功应用了某个事务,要么都没有应用。
③ 单一视图
无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。
④ 可靠性
一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会一直被保留,除非有另一个事务对其进行了变更。
⑤ 实时性
Zookeeper保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
Zookeeper致力于提供一个高性能、高可用、且具有严格的顺序访问控制能力(主要是写操作的严格顺序性)的分布式协调服务,其具有如下的设计目标。
① 简单的数据模型
Zookeeper使得分布式程序能够通过一个共享的树形结构的名字空间来进行相互协调,
即Zookeeper服务器内存中的数据模型由一系列被称为ZNode的数据节点组成,
Zookeeper将全量的数据存储在内存中,以此来提高服务器吞吐、减少延迟的目的。
② 可构建集群
一个Zookeeper集群通常由一组机器构成,组成Zookeeper集群的而每台机器都会在内存中维护当前服务器状态,并且每台机器之间都相互通信。
③ 顺序访问
对于来自客户端的每个更新请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。
④ 高性能
Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。
1、节点
在ZooKeeper中,节点也称为znode。由于对于程序员来说,对zk的操作主要是对znode的操作,znode采用类似文件系统的层次树状结构进行管理,如下图实例:
Paste_Image.png
/workers节点作为父节点,其下每个znode子节点保存了系统中一个可用从节点信息。
/tasks节点作为父节点,其下每个znode子节点保存了所有已经创建并等待从节点执行的任务的信息。
/assign节点作为父节点,其下每个znode子节点保存了分配到某个从节点的一个任务信息,
当主节点为某个从节点分配了一个任务,就会在/assign下增加一个子节点。
创建一个节点可以选择4种类型:持久的(persistent)、临时的(ephemeral)、持久有序的(persistent_sequential)和临时有序的(ephemeral_sequential)。
持久节点与临时节点:
节点的类型在创建时就被确定下来,并且不能改变。
持久节点的存活时间不依赖于客户端会话,只有客户端在显式执行删除节点操作时,节点才消失。
临时节点的存活时间依赖于客户端会话,当会话结束,临时节点将会被自动删除(当然也可以手动删除临时节点)。利用临时节点的这一特性,我们可以使用临时节点来进行集群管理,包括发现服务的上下线等。
ZooKeeper规定,临时节点不能拥有子节点。
//创建了一个持久节点/module1,且其数据为”module1”。
create /module1 module1
//创建了一个临时节点 /module1/app1,数据为”app1”。
create -e /module1/app1 app1
//关闭会话,然后输入命令
get /module1/app1
Node does not exist: /module1/app1
有序节点:
一个有序znode节点被分配唯一个单调递增的整数。当创建有序节点时,一个序号会被追加到路径之后,如/tasks/task0000000001。有序znode通过提供了创建具有唯一名称的znode的简单方式。同时也通过这种方式可以直观地查看znode的创建顺序。
//使用命令create加上-s参数,可以创建顺序节点
create -s /tasks/task data
//输出
Created /tasks/task0000000001
//创建了一个持久顺序节点 /tasks/task0000000001。如果再执行此命令,则会生成节点 /tasks/task0000000002。
节点的属性:
get /module1/app2
//输出
app2
cZxid = 0x20000000e
ctime = Thu Jun 30 20:41:55 HKT 2016
mZxid = 0x20000000e
mtime = Thu Jun 30 20:41:55 HKT 2016
pZxid = 0x20000000e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
版本号:
对于每个znode来说,均存在三个版本号:
•dataVersion
数据版本号,每次对节点进行set操作,dataVersion的值都会增加1(即使设置的是相同的数据)。
•cversion
子节点的版本号。当znode的子节点有变化时,cversion 的值就会增加1。
•aclVersion
ACL(Access Control List,访问控制)的版本号。
事务ID:
对于zk来说,每次的变化都会产生一个唯一的事务id,zxid(ZooKeeper Transaction Id)。通过zxid,可以确定更新操作的先后顺序。例如,如果zxid1小于zxid2,说明zxid1操作先于zxid2发生。
需要指出的是,zxid对于整个zk都是唯一的,即使操作的是不同的znode。
•cZxid
Znode创建的事务id。
•mZxid
Znode被修改的事务id,即每次对znode的修改都会更新mZxid。
2、监视通知机制
监视与通知机制,避免了客户端获取数据做轮询。
Paste_Image.png
•通知机制是单次触发的操作,为了收到多个通知,需要每次收到通知之后设置一个新的监视点。
•客户端可以设置多种监视点:监控znode数据变化、监控znode子节点变化、监控znode创建或者删除。调用读取zookeeper的API时,传入一个watcher对象或者默认的watcher。
3、版本机制
Paste_Image.png每一个znode都有一个版本号,它随着每次数据变化而自增。两个API操作可以有条件地执行:setData和delete。这两个调用以版本号作为传入参数,只有当传入参数的版本号与服务器上的版本号一致时调用才会成功。
4、会话
Zookeeper的连接与会话就是客户端通过实例化Zookeeper对象来实现客户端与服务端创建并保持TCP连接的过程。
会话状态:
在Zookeeper客户端与服务端成功完成连接创建后,就创建了一个会话,Zookeeper会话在整个运行期间的生命周期中,会在不同的会话状态中之间进行切换,这些状态可以分为CONNECTING、CONNECTED、RECONNECTING、RECONNECTED、CLOSE等。
一旦客户端开始创建Zookeeper对象,那么客户端状态就会变成CONNECTING状态,同时客户端开始尝试连接服务端,连接成功后,客户端状态变为CONNECTED,通常情况下,由于断网或其他原因,客户端与服务端之间会出现断开情况,一旦碰到这种情况,Zookeeper客户端会自动进行重连服务,同时客户端状态再次变成CONNCTING,直到重新连上服务端后,状态又变为CONNECTED,在通常情况下,客户端的状态总是介于CONNECTING和CONNECTED之间。但是,如果出现诸如会话超时、权限检查或是客户端主动退出程序等情况,客户端的状态就会直接变更为CLOSE状态。
Session是Zookeeper中的会话实体,代表了一个客户端会话,其包含了如下四个属性:
• sessionID。会话ID,唯一标识一个会话,每次客户端创建新的会话时,Zookeeper都会为其分配一个全局唯一的sessionID。
•TimeOut。会话超时时间,客户端在构造Zookeeper实例时,会配置sessionTimeout参数用于指定会话的超时时间,Zookeeper客户端向服务端发送这个超时时间后,服务端会根据自己的超时时间限制最终确定会话的超时时间。
•TickTime。下次会话超时时间点,为了便于Zookeeper对会话实行"分桶策略"管理,同时为了高效低耗地实现会话的超时检查与清理,Zookeeper会为每个会话标记一个下次会话超时时间点,其值大致等于当前时间加上TimeOut。
•isClosing。标记一个会话是否已经被关闭,当服务端检测到会话已经超时失效时,会将该会话的isClosing标记为"已关闭",这样就能确保不再处理来自该会话的请求了。
5、事务ID
事务id在会话重连中作用:
Paste_Image.png
在集群模式下,客户端有多个服务器可以连接,当尝试连接到一个不同的服务器时,这个服务器的状态要与最后连接的服务器的状态要保持一致。Zk正是使用zxid来标识这个状态,图中描述了客户端在重连情况下zxid的作用。当客户端因超时与S1断开连接后,客户端开始尝试连接S2,但S2延迟于客户端所识别的状态。然而,S3的状态与客户端所识别的状态一致,所以客户端可以安全连接上S3。
6、leader follower
集群模式下,zookeeper有以下几种角色:
Paste_Image.png
在仲裁模式下,具有一组ZooKeeper服务器,我们称为ZooKeeper集合(ZooKeeper ensemble),它们之间可以进行状态的复制。但如果让一个客户端等待每个服务器完成数据保存后再继续,延迟问题将无法接受。为此,只需要将数据保存到保证zookeeper有效运行的最小数量的服务器上(zookeeper仲裁,遵循多数原则,即 总数/2+1 ,偶数数量服务器更脆弱,即使出现“脑裂”也不影响)。就可以通知客户端数据已经安全保存,其他服务器复制已保存数据的服务器状态。
7、ZAB协议
Zookeeper依赖ZAB协议来实现分布式数据的一致性,基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间的数据的一致性,即其使用一个单一的诸进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程中,ZAB协议的主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。
ZAB协议的核心是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理方式,即:所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将一个客户端事务请求转化成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
ZAB包括两种基本的模式:消息广播、数据同步和崩溃恢复。
消息广播
1. 客户端发起一个写操作请求
2. Leader服务器将客户端的request请求转化为事物proposql提案,同时为每个proposal分配一个全局唯一的ID,即ZXID。
3. leader服务器与每个follower之间都有一个队列,leader将消息发送到该队列
4. follower机器从队列中取出消息处理完(写入本地事物日志中)毕后,向leader服务器发送ACK确认。
5. leader服务器收到半数以上的follower的ACK后,即认为可以发送commit
6. leader向所有的follower服务器发送commit消息。
崩溃恢复
zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是leader服务器接受写请求,即使是follower服务器接受到客户端的请求,也会转发到leader服务器进行处理。
如果leader服务器发生崩溃,则zab协议要求zookeeper集群进行崩溃恢复和leader服务器选举。
ZAB协议崩溃恢复要求满足如下2个要求:
确保已经被leader提交的proposal必须最终被所有的follower服务器提交。
确保丢弃已经被leader出的但是没有被提交的proposal。
根据上述要求,新选举出来的leader不能包含未提交的proposal,即新选举的leader必须都是已经提交了的proposal的follower服务器节点。同时,新选举的leader节点中含有最高的ZXID。这样做的好处就是可以避免了leader服务器检查proposal的提交和丢弃工作。
leader服务器发生崩溃时分为如下场景:
1. leader在提出proposal时未提交之前崩溃,则经过崩溃恢复之后,
新选举的leader一定不能是刚才的leader。因为这个leader存在未提交的proposal。
2 leader在发送commit消息之后,崩溃。
即消息已经发送到队列中。经过崩溃恢复之后,参与选举的follower服务器
(刚才崩溃的leader有可能已经恢复运行,也属于follower节点范畴
)中有的节点已经是消费了队列中所有的commit消息。
即该follower节点将会被选举为最新的leader。剩下动作就是数据同步过程。
数据同步
在zookeeper集群中新的leader选举成功之后,leader会将自身的提交的最大proposal的事物ZXID发送给其他的follower节点。follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。
ZXID是一个64位的数字,其中32位可以看做是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal时,都会对该计数器进行加1操作,而高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader时,就会从这个Leader上取出其本地日志中最大事务Proposal的ZXID,并解析出epoch值,然后加1,之后以该编号作为新的epoch,低32位则置为0来开始生成新的ZXID,ZAB协议通过epoch号来区分Leader周期变化的策略,能够有效地避免不同的Leader服务器错误地使用不同的ZXID编号提出不一样的事务Proposal的异常情况。当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,因为当前集群中一定包含了一个Quorum(过半)集合,该集合中的机器一定包含了更高epoch的事务的Proposal,因此这台机器的事务Proposal并非最高,也就无法成为Leader。
网友评论