集群角色
在ZooKeeper 中没有沿用传统的Master/Slave 概念, 而是引入了Leader 、Followe和observer 三个角色。ZooKeeper 集群中的所有机器通过一个Leader 选举过程来选定一台被称为“ Leader” 的机器, Leader 服务器为客户端提供读和写服务。除Leader 外, 其他机器包括Follower 和Observer 。Follower 和Observer 都能够提供读服务, 唯一的区别在于, Observer 机器不参与Leader 选举过程, 也不参与写操作的“ 过半写成功” 策略, 因此Observer 可以在不影响写性能的情况下提升集群的读性能。
会话
Session 是指客户端会话, 我们首先来了解一下客户端连接。在ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个TCP 长连接。ZooKeeper对外的服务端口默认是22181 , 客户端启动的时候, 首先会与服务器建立一个TCP 连接,从第一次连接建立开始, 客户端会话的生命周期也开始了, 通过这个连接, 客户端能够通过心跳检测与服务器保持有效的会话, 也能够向ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch 事件通知。Session 的SessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时, 只要在SessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器, 那么之前创建的会话仍然有效。
数据节点(Znode )
在ZooKeeper 中, ZNode 可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZN0de 被创建了, 除非主动进行ZNode 的移除操作, 否则这个ZNode 将一直保存在ZooKeeper 上。而临时节点就不一样了, 它的生命周期和客户端会话绑定,一旦客户端会话失效, 那么这个客户端创建的所有临时节点都会被移除。另外, ZooKeeper 还允许用户为每个节点添加一个特殊的属性: SEQUENTIAL。一旦节点被标记上这个属性, 那么在这个节点被创建的时候, ZooKeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。
版本
ZooKeeper 的每个ZNodc 上都会存储数据, 对应于每个ZNode ,ZooKeeper 都会为其维护一个叫作Stat 的数据结构, Stat 中记录了这个ZNode 的三个数据版本, 分别是version ( 当前ZNode 的版本) 、cversion ( 当前ZNode 子节点的版本)和aversion ( 当前zNode 的ACL 版本) 。
Watcher
watcher( 事件监听器),是ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些watcher , 井且在一些特定事件触发的时候, ZooKeeper 服务端会将事件通知到感兴趣的客户端上去, 该机制是ZooKeeper 实现分布式协调服务的重要特性。
ACL
ZooKeeper 采用ACL ( Access Control Lists ) 策略来进行权限控制, 类似于UNIX 文件系统的权限控制。Z00Keepe「定义了如下5 种权限。
. CREATE : 创建子节点的权限。
. READ : 获取节点数据和子节点列表的权限。
. WR.TE : 更新节点数据的权限。
. DELETE : 删除子节点的权限。
. ADMIN : 设置节点ACL 的权限。
其中尤其需要注意的是, CREATE 和DELETE 这两种权限都是针对子节点的权限控制。
ZAB 协议
ZAB 协议是为分布式协调服务。ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZAB 协议的开发设计人员在协议设计之初并没有要求其具有很好的扩展性, 最初只是为雅虎公司内部那些高吞吐量、低延迟、健壮、简单的分布式系统场景设计的。
在zooKeeper 的官方文档中也指出, ZAB 协议并不像Paxos 算法那样, 是一种通用的分布式一致性算法, 它是一种特别为ZooKeeper 设计的崩溃可恢复的原子消息广播算法。在ZooKeeper 中, 主要依赖ZAB 协议来实现分布式数据一致性, 基于该协议, ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。具体的,ZooKeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求, 并采用ZAB 的原子广播协议, 将服务器数据的状态变更以事务Proposal 的形式广播到所有的副本进程上去。ZAB 协议的这个主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更, 因此能够很好地处理客户端大量的并发请求。
另一方面, 考虑到在分布式环境中, 顺序执行的一些状态变更其前后会存在一定的依赖关系, 有些状态变更必须依赖于比它早生成的那些状态变更, 例如变更C 需要依赖变更A 和变更B 。这样的依赖关系也对ZAB 协议提出了一个要求: ZAB 协议必须能够保证一个全局的变更序列被顺序应用, 也就是说, ZAB 协议需要保证如果一个状态变更已经被处理了, 那么所有其依赖的状态变更都应该已经被提前处理掉了。最后, 考虑到主进程在任何时候都有可能出现崩溃退出或重启现象, 因此, ZAB 协议还需要做到在当前主进程出现上述异常情况的时候, 依旧能够正常工作。
ZAB 协议包括两种基本的模式, 分别是崩溃恢复和消息广播。当整个服务框架在启动过程中, 或是当Leader 服务器出现网络中断、崩溃退出与重启等异常情况时, ZAB 协议就会进人恢复模式并选举产生新的Leader 服务器。当选举产生了新的Leader 服务器, 同时集群中已经有过半的机器与该Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,所谓的状态同步是指数据同步, 用来保证集群中存在过半的机器能够和Leader 服务器的数据状态保持一致。当集群中已经有过半的Follower 服务器完成了和Leader 服务器的状态同步, 那么整个服务框架就可以进入消息广播模式了。当一台同样遵守ZAB 协议的服务器启动后加人到集群中时, 如果此时集群中已经存在一个Leader 服务器在负责进行消息广播, 那么新加入的服务器就会自觉地进人数据恢复模式: 找到Leader 所在的服务器, 并与其进行数据同步, 然后一起参与到消息广播流程中去。
正如上文介绍中所说的, ZooKeeper 设计成只允许唯一的一个Leader 服务器来进行事务请求的处理。Leader 服务器在接收到客户端的事务请求后, 会生成对应的事务提案并发起一轮广播协议; 而如果集群中的其他机器接收到客户端的事务请求, 那么这些非Leader 服务器会首先将这个事务请求转发给Leader 服务器。
当Leader 服务器出现崩溃退出或机器重启, 亦或是集群中已经不存在过半的服务器与该Leader 服务器保持正常通信时, 那么在重新开始新一轮的原子广播事务操作之前, 所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态, 于是整个ZAB 流程就会从消息广播模式进入到崩溃恢复模式。一个机器要成为新的Leader , 必须获得过半进程的支持, 同时由于每个进程都有可能会崩溃, 因此, 在zAB 协议运行过程中, 前后会出现多个Leader , 并且每个进程也有可能会多次成为Leader 。进人崩溃恢复模式后, 只要集群中存在过半的服务器能够彼此进行正常通信, 那么就可以产生一个新的Leader 并再次进入消息广播模式。举个例子来说,一个由3 台机器组成的ZAB 服务, 通常由1个Leader 、2 个Follower 服务器组成。某一个时刻, 假如其中一个Folfower 服务器挂了, 整个ZAB 集群是不会中断服务的, 这是因为Leader 服务器依然能够获得过半机器( 包括Leader 自己) 的支持。
消息广播
在消息、广播过程中, Leader 服务器会为每一个Folfower 服务器都各自分配一个单独的队列, 然后将需要广播的事务Pr叩osal 依次放入这些队列中去, 并且根据FIFO策略进行消息发送。每一个Follower 服务器在接收到这个事务ProPosal 之后, 都会首先将其以事务日志的形式写入到本地磁盘中去, 并且在成功写入后反馈给Leader 服务器一个Ack 响应。当Leader 服务器接收到超过半数Folfower 的Aok 响应后, 就会广播一个Commit 消息给所有的Follower 服务器以通知其进行事务提交, 同时Leader 自身也会完成对事务的提交, 而每一个Folfower 服务器在接收到Commit 消息后, 也会完成对事务的提交。
数据同步
所有正常运行的服务器, 要么成为Leader , 要么成为Follower 并和Leader 保持同步。Leador 服务器需要确保所有的Follower 服务器能够接收到每一条事务ProPosal , 并且能够正确地将所有已经提交了的事务ProPosal 应用到内存数据库中去。具体的, Leader 服务器会为每一个Follower 服务器都准备一个队列, 并将那些没有被各Follower 服务器同步的事务以Proposal 消息的形式逐个发送给Follower 服务器, 并在每一个Proposal 消息后面紧接着再发送一个Commit 消息, 以表示该事务已经被提交。等到Follower 服务器将所有其尚未同步的事务ProPosal都从Leader 服务器上同步过来并成功应用到本地数据库中后, Leader 服务器就会将该Follower 服务器加入到真正的可用Follower 列表中,并开始之后的其他流程。
Zookeeper并不保证读取的是最新数据
如果一个ZooKeeper集群有10000台节点,当进行写入的时候,如果已经有6K个节点写入成功,zk就认为本次写请求成功。但是这时候如果一个客户端读取的刚好是另外4K个节点的数据,那么读取到的就是旧的过期数据。在ZooKeeper的官方文档中对此有解释,地址在:ZooKeeper Programmer's Guide
ZooKeeper一致性的保证:
ZooKeeper是一种高性能,可扩展的服务,虽然读取速度比写入快,但是读取和写入操作都设计的极为快速,这样做的原因是在读取的情况下,ZooKeeper可能会提供较旧的数据,但这是为了ZooKeeper的一致性保证:
-
顺序一致性:来自客户端的更新将按照发送的顺序被写入到zk
-
原子性:更新操作要么成功要么失败,没有中间状态
-
单系统快照:客户端将看到服务的相同视图,而不管它连接到的服务器。
-
可靠性:一旦应用更新,数据将被持久化,直到数据被再次更新,对于该保证有两个推论:
1、如果客户端得到了成功的返回码,说明写入成功,数据被持久化,如果出现了通信错误,超时等一些故障,客户端将不知道更新是否已应用。我们采取措施尽量减少失败,但唯一的保证是只有成功的返回码。 (这在Paxos中称为单调性条件。)
2、如果客户端已经读取到了数据或者写入成功了数据,都不会因为zk的失败而导致回滚;及时性:在一段时间后,客户端将看到最新的系统更新,在此期间客户端将看到这种变更。
有时开发人员错误地假定ZooKeeper实际上没有做出另一个保证:跨客户端的强一致性ZooKeeper并不保证在每个实例中,两个不同的客户端将具有相同的ZooKeeper数据的视图。由于诸如网络延迟的因素,一个客户端可以在另一客户端被通知该改变之前执行更新,考虑两个客户端A和B的场景。如果客户端A将znode / a的值从0设置为1,则告诉客户端B读取/ a,则客户端B可以读取旧值0,这取决于它连接到的服务器。如果客户端A和客户端B读取相同的值很重要,则客户端B应该在执行读取之前从ZooKeeper API方法调用sync()方法。因此,ZooKeeper本身不保证所有服务器上同步发生变化,但ZooKeeper原语可用于构建更高级的函数,提供有用的客户端同步。
ZooKeeper的sync方法的解释:异步的实现当前进程与leader之间的指定path的数据同步;