简介
Zookeeper为分布式应用 提供了高效且可靠的分布式协调服务,提供了诸如统一命名服务、发布订阅、负载均衡、配置管理和分布式锁等分布式的基础服务。
设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
可以保证以下特性:
- 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中
- 原子性:要么集群中所有机器都成功应用了某一个事务,要么都没有应用。
- 单一视图:无论客户端连接的是哪一个Zookeeper服务器,其看到的服务端数据模型都是一致性的
- 可靠性:一旦服务端成功应用了某个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来。
- 实时性:Zookeeper能保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
基本概念/原理
集群角色
通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式。而在Zookeeper中,引入了Leader、Follower、Observer(可以没有)三种角色。Zookeeper集群中的所有机器通过一个Leader选举过程选定一台被称为“Leader”的机器,Leader服务器为客户端提供读和写服务。Follower和Observer都能提供读服务,唯一的区别是,Observer机器不参与Leader选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能。
Leader是整个Zookeeper集群工作机制的核心,主要工作有以下两个:
1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性
2)集群内部各服务器的调度者
Follower是Zookeeper集群状态的跟随者,其主要工作有以下三个:
1)处理客户端非事务请求,转发事务请求给Leader
2)参与事务请求Proposal的投票
3)参与Leader选举
Observer和Follower很像,唯一的区别是不参与任何形式的投票,只提供非事务服务通常用在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
事务
在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,我们称之为事务操作。对于每一个事务请求,Zookeeper都会为其分配一个全局唯一的事务iD,用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从ZXID中可以识别出Zookeeper处理这些更新操作请求的全局顺序。
会话
Session是指客户端会话。一个客户端连接是指客户端和服务器之间的一个TCP长连接。从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够提供心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接收响应,同时还能通过该连接接收来自服务器的Watch事件通知。Session的sessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或者客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台机器,那么之前创建的会话仍然有效。
数据节点 ZNode
Zookeeper将所有数据存储在内存中,数据模型是一棵树,由/进行分割的路径,就是一个ZNode(比如/foo/path1)。每个ZNode都会保存自己的数据内容,同时保存一系列属性信息。
ZNode可以分为持久节点和临时节点。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点的生命周期与客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。另外,Zookeeper还允许用户为每个节点添加一个SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。
数据节点分为持久节点、临时节点和顺序节点,可以生成四种组合:持久、持久顺序、临时、临时顺序。
版本
对应每个ZNode,Zookeeper都会为其维护一个叫做Stat的数据结构,Stat中记录了这个ZNode 的三个数据版本,分别是version(当前ZNode版本)、cversion(当前ZNode子节点的版本)和aversion(当前ZNode的ACL版本)。还有czxid(创建时的事务ID),mzxid(最后一个被更新时的事务ID)等。
version为0表示自创建该节点后,被更新过0次。注意即使变更并没有使得数据内容的值发生变化,version的值仍然会变更。
版本的作用可以用乐观锁原理来解释,更新数据的时候ZK使用乐观锁来保证原子性。
Watcher
Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务器会将事件通知到感兴趣的客户端上去。
Watcher机制包括客户端线程、客户端WatchManager和ZK服务器三部分。客户端在向ZK服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
KeeperState和EventType两个枚举分别代表了通知状态和事件类型。
注意,客户端无法直接从该事件中获取到对应数据节点的原始数据内容以及变更后的新数据内容,而是需要客户端再次主动去重新获取数据。
Watcher特性:
1)一次性:无论是服务器还是客户端,一旦一个Watcher被触发,ZK都会将其从相应的存储中移除。因此开发人员必须要进行反复注册,这样的设计有效地减轻了服务器的压力
2)客户端串行执行:Watcher的回调是一个串行同步的过程,这为我们保证了顺序。
3)轻量:WatchedEvent是ZK整个Watcher通知机制的最小通知单元,只包含通知状态、事件类型和节点路径。
ACL
Zookeeper使用ACL(访问控制列表)策略来进行权限控制,避免因误操作而带来的数据随意变更导致的分布式系统异常。
Zookeeper定义了5种权限:
CREATE:创建节点
READ:读取节点数据和子节点列表
WRITE:更新节点数据的权限
DELETE:删除子节点
ADMIN:设置节点ACL
ZAB协议
ZAB:Zookeeper Atomic Broadcast 原子消息广播协议
核心
1) Zookeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。ZAB协议的这个主备模型架构保证了同一时刻集群中只能有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。
2) 在分布式环境中,顺序执行的一些状态变更其前后存在一定的依赖关系,有些状态变更必须依赖于比它早生成的那些状态变更。ZAB协议必须能够保证一个全局的变更序列被顺序地应用。
3)所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的服务器则称为Follower。Leader负责将一个客户端事务请求转换成一个Proposal,并将该Proposal分发给集群中所有的Follower服务器。之后Leader需要等待所有Follower的反馈,一旦超过半数的Follower进行了正确的反馈后,那么Leader就会再次向所有的Follower分发Commit消息,要求其将前一个Proposal进行提交。
内容介绍
ZAB协议包括两种基本的模式,分别是崩溃恢复和消息广播。当整个服务框架在启动过程中,或是当Leader出现网络中断、崩溃退出等异常情况时,ZAB协议就会进入恢复模式,并选举产生新的Leader。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步(数据同步)之后,ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower完成了与Leader的状态同步,那么整个服务器框架就可以进入消息广播模式。当一台同样遵循ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在Leader在负责进行消息广播,那么新加入的服务器就会自觉进入数据恢复模式,找到Leader,并与其进行数据同步,然后一起参与到消息广播流程中去。
Leader在接收到客户端的事务请求后,会生成对应的Proposal并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader。
当Leader崩溃或机器重启,或者集群中已经不存在过半的服务器与该Leader保持正常通信时,那么在重新开始新一轮的原子广播操作之前,所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。
消息广播
消息广播使用了一个原子广播协议,类似于2PC。
针对客户端的事务请求,Leader会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。
但是与2PC还是略有不同,移除了中断事务逻辑(rollback),所有的Follower要么正常反馈Leader提出的Proposal,要么就抛弃Leader;并且可以在过半的Follower已经反馈ack时就开始提交事务Proposal了,而不需要等待所有Follower。
这样简化后的2PC是无法处理Leader崩溃而带来的数据不一致问题的,因此在ZAB协议中添加了崩溃恢复模式来解决这个问题。
另外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易地保证消息广播过程中消息接收与发送的顺序性。
在整个消息广播过程中,Leader会为每个事务请求分配一个全局递增的唯一ID,称为事务ID(ZXID)。由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进行排序与处理。
在消息广播中,Leader会为每个Follower都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入到这些队列中,并且按照FIFO策略进行消息发送。每个Follower在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中,并且在成功写入后反馈给Leader一个ack。当Leader接收到过半数的Follower的ack响应后,就会广播一个Commit消息给所有的Follower以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每一个Follower在接收到Commit请求后,也会完成事务的提交。
崩溃恢复
ZAB协议需要一个快速可靠的Leader选举算法,不仅需要让Leader自己知道自己被选举为Leader,并且还需要让集群中的所有其他机器也能够快速感知到选举产生的Leader服务器。
1)ZAB协议需要保证已经在Leader上提交的事务最终被所有服务器都提交
假设一个事务在Leader上被提交了,并且已经得到过半Follower的ack,但是在它将commit消息发送给所有Follower之前,Leader挂了。
2)ZAB协议需要确保丢弃那些只在Leader上被提出的事务。
假设Leader提出了一个事务Proposal之后马上崩溃退出,从而导致集群中的其他服务器都没有收到这个事务Proposal。于是,当Leader恢复后再次加入集群中的时候,ZAB协议需要确保丢弃该事务。
基于以上两个特殊情况,需要保证Leader选举算法:能够确保提交已经被Leader提交的事务Proposal,同时丢弃已经被跳过的事务Proposal。
如果让Leader选择算法能够保证新选举出来的Leader拥有集群中所有机器最高编号(ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交Proposal,并且可以省去Leader服务器检查Proposal的提交和丢弃工作的这一步操作了。
Leader选举
术语:SID 服务器ID
ZXID 事务ID,唯一标识一次服务器状态的变更
Vote 投票 当集群中的机器发现自己无法检测到Leader时,就会开始尝试进行投票
Quorum 过半机器数 如果总机器数为n,那么Quorum为n/2+1
进入Leader选举
当Zookeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
1)服务器 初始化
2)服务器运行时无法和Leader保持连接
而当一台机器进入Leader选举时,当前集群也可能处于以下两种状态:
1)集群中本来存在Leader
2)集群中不存在Leader
1)中通常是集群中的某一台机器启动比较晚,在它启动之前,集群已经可以正常工作,即已经存在了一台Leader服务器。针对这种情况,当该机器试图去选举Leader时,会被告知当前服务器的Leader,对于该机器来说,仅仅需要和Leader机器建立起连接,并进行状态同步即可。
第一次投票
通常有两种情况会导致集群中不存在Leader,一种情况是在整个服务器刚刚初始化情况时,此时尚未产生Leader;另一种情况是Leader宕机。
此时,集群中的所有机器都处于一种试图选举出一个Leader的状态,这种状态称为Looking。当一台机器处于Looking状态时,那么它就会向集群中的所有其他机器发送消息,这个消息称为投票。
投票可以表示为(SID,ZXID),SID是所推举的服务器唯一ID,ZXID是事务ID。
在第一次投票时,由于还无法检测到集群中其他机器的状态信息,因此每台机器都是将自己作为被推举的对象来进行投票。
第二次投票
集群中的每台机器发出自己的投票后,也会收到来自集群中其他机器的投票。每台机器都会根据一定的规则,来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票。
vote_sid:接收到的投票中所推举的Leader的SID
vote_zxid:接收到的投票中所推举的Leader的ZXID
self_sid:当前服务器自己的SID
self_zxid:当前服务器自己的ZXID
对于每次收到的投票的处理,都是一个对(vote_sid,vote_zxid)和(self_sid,self_zxid)的对比的过程。
规则1:如果vote_zxid大于self_zxid,那么认可当前收到的投票,并再次将该投票发送出去
规则2:如果vote_zxid等于self_zxid,则就对比两者的SID。如果vote_sid大于self_sid,那么就认可当前接收到的投票,并再次将该投票发送出去
规则3:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么同样坚持自己的投票,不做变更。
确定Leader
经过第二次投票后,集群中的每台机器都会再次受到其他机器的投票,然后开始统计投票。如果一台机器收到超过半数的相同的投票,那么整个投票对应的SID机器即为Leader。
小结
哪台机器上的数据越新,那么越有可能成为Leader。数据越新,ZXID就越大,也就越能保证数据的恢复。如果几台服务器有着相同的ZXID,那么SID较大的服务器成为Leader。
数据同步
完成Leader选举之后,在接收客户端事务请求前,Leader会确认事务日志中的所有Proposal是否已经被集群中过半的机器提交了,即是否完成数据同步。
正常情况下:Leader会为每一个Follower都准备一个队列,并将那些没有被各Follow同步的事务以Proposal消息的形式逐个发送给Follower,并在每一个Proposal消息后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower将所有尚未同步的事务Proposal都从Leader上同步过来并成功应用到本地数据库中,Leader就会将该Follower加入到真正的可用Follower列表中。
如何处理需要被丢弃的Proposal的?
ZXID是一个64位的数字,低32位是一个单调递增的计数器,针对客户端的每一个事务请求,Leader在产生一个新事务Proposal的时候,都会对该计数器进行加一操作;高32位则代表了Leader周期epoch编号,每当选举产生一个新的Leader,就会从这个Leader上取出其本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出epoch,然后再对其进行加一操作,之后就会以此编号作为新的epoch,并将低32位置0来开始新的ZXID。ZAB协议中这一通过epoch编号来区分Leader周期变化的策略,能够避免不同的Leader错误地使用相同的ZXID编号提出不一样的事务Proposal的异常情况,这对于识别Leader崩溃恢复前后生成的Proposal非常有帮助。
当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,因为当前集群中一定包含一个Quorum集合(存在过半的处于UP状态的进程的进程子集),该集合中的机器一定包含了更高epoch的事务Proposal,因此这台机器的事务Proposal一定不是最高,也就无法成为Leader了。当这台机器加入到集群后,以Follower的角色连接上Leader后,leader会根据自己服务器上最后被提交的Proposal来与Follower的Proposal进行对比,对比的结果当然是Leader要求Follower进行一个回退操作——回退到一个已经被集群中过半机器提交的最新的事务Proposal。
与Paxos算法的联系与区别
ZAB协议并不是一个Paxos算法的典型实现。
联系:
- 两者都存在一个类似Leader的角色,由其负责多个Follower的运行
- Leader都会等待过半Follower做出正确反馈后,才会将一个Proposal提交
- 每个Proposal中都包含了一个表示当前Leader周期的值,ZAB中称为epoch,Paxos中称为ballot。
区别:
Paxos算法中,一个新Leader会进行两个阶段的工作。第一阶段被称为读阶段,Leader与所有其他机器进行通信来收集上一个Leader的Proposal,并将它们提交;第二阶段被称为写阶段,Leader开始提出自己的提案。
ZAB协议中,在Paxos基础上增加了一个数据同步阶段。在同步阶段之前,ZAB也存在一个类似于Paxos读阶段的过程,称为发现阶段。在同步阶段中,新的Leader会确保过半的Follower已经提交了之前Leader周期中的所有事务Proposal,该阶段可以保证Leader在新的周期提出Proposal之前,所有机器都已经完成对之前所有事务Proposal的提交。一旦完成同步阶段后,那么ZAB就会执行和Paxos算法类似的写阶段。
本质区别是设计目标不一样,ZAB主要用于构建一个高可用的分布式数据主备系统,Paxos则用于构建一个分布式的一致性状态机系统。
典型应用
发布订阅(配置管理)
客户端向服务器注册自己需要关注的节点,一旦该节点的数据发生变更,那么客户端就会向响应的客户端发送Watcher时间通知,客户端接收到这个事件通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到Zookeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到Zookeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样的话,如果配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到获取最新配置信息的目的。
命名服务(软负载中心)
被命名的实体可以是集群中的机器、提供的服务地址或远程对象。
比如RPC的服务注册查找中心,ZK客户端与ZK服务器保持连接,连接ZK服务器时RPCServer将自己的IP地址、端口号告诉ZKServer,在ZKServer上创建节点。RPCClient也连接ZK服务器,获取RPCServer的地址(软件负载均衡),并注册Watcher。假如RPCServer挂掉,与ZK服务器的连接断开,那么临时节点会被删除,此时会通知RPCClient,RPCClient可以重新选择RPCServer进行连接。
并且ZK还可以创建全局唯一ID,基于ZK提供的创建节点时使用SEQUENTIAL属性(顺序节点)。
集群管理
希望知道当前集群中有哪些集群在工作;
对集群中每天机器的运行时状态进行数据收集;
对集群中机器进行上下线通知
基于ZK提供的临时节点和Watcher监听特性,可以实现另一种集群机器存活性监控的系统。
比如云主机管理,每台机器上线后,启动机器上的Agent,向ZK的指定节点进行注册(临时节点),此时监控中心会接收到子节点变更事件,即上线通知,于是可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知。
除了要对机器的在线状态进行检测,还要对机器的运行时状态进行监控。在运行的过程中,Agent会定时将主机的运行状态写入ZK上的主机节点,监控中心提供订阅这些节点的数据变更通知来间接地获取主机的运行时信息。
Master选举
在分布式系统中,Master往往用来协调集群中的其他系统单元,具有对分布式系统状态变更的决定权。
场景:集群中的所有系统单元都需要对某个业务提供数据,但计算该数据的代价相当大,于是只希望一台机器或某几台机器来执行该任务,集群中的其他机器共享其计算结果。
那么怎么选择机器呢?Master选举
一种方案是所有机器都向关系数据库插入某条数据,主键都是一样的,谁插入成功,谁就是Master。但是无法感知Master挂掉的这种情形。
而ZK的强一致性可以保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZK将会保证客户端无法重复创建一个已经存在的数据节点(某个path下的节点名是唯一的)。只有一个客户端能够成功创建这个节点,那么这个客户端所在机器就成为了Master。同时,其他没有在ZK上成功创建结点的客户端,都会在该节点的父节点上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现当前的Master挂了,那么其余的客户端将会重新进行Mater选举。
分布式锁
可以采用数据库的锁,只是数据库的性能很差。
排他锁
可以将一个数据节点(比如/exclusive_lock/lock)视为一个锁,所有客户端去创建该节点,创建成功的客户端就获得了这个锁。
如果获得锁的机器宕机,或者执行完业务逻辑后,都会将该节点删除。
无论什么情况下删除了该节点,都会通知所有在该节点(/exclusive_lock)上注册了监听子节点变更的Watcher的机器,这些机器在接收到通知后,会再次重新发起分布式锁的获取。
共享锁
同样是将数据节点视为一个锁,只是这里的数据节点是一个顺序节点,比如/shared_lock/{host_name}-请求类型-序列号。
获取锁时所有客户端都会到/shared_lock这个节点下面创建一个顺序节点,读请求的话,请求类型为R(写的话为W)。
共享锁的定义是可以有读读,但不能有读写、写写、写读。
1、创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册了子节点变更的Watcher监听。
2、确定自己的节点序号在所有子节点中的顺序
3、对于读请求:如果没有比自己序号小的子节点,或者所有比自己序号小的节点都是读请求,那么表示自己已经获得了共享锁;如果比自己序号小的子节点中有写请求,那么就需要进入等待。
对于写请求:如果自己不是序号最小的子节点,那么就需要进入等待
4、接收到Watcher通知后,重复步骤1
释放锁时就直接将自己的节点删除即可。
高可用
Zookeeper集群服务器数推荐为奇数。由于过半存活即可用特性,3台机器挂掉一台仍可用。而5台和6台服务器在挂掉两台后仍可用,挂掉三台都均不可用,因此5台和6台服务器在其容灾能力上是没有区别的,因此Zookeeper集群推荐部署为奇数台服务器。