Zookeeper作为高可用的分布式协调与管理框架,在越来越多的分布式系统中作为核心组件引入使用,本文简要介绍Zookeeper的集群架构和一些基本概念,以及基于ZAB一致性协议的选主流程,并列举了一些常见的使用场景。
1、Zookeeper基本概念
1.1 Zookeeper集群架构
与典型的分布式架构Master/Slave主从模式不同,Zookeeper使用了Leader、Follower和Observer三种角色。如下表所示:
1)领导者Leader
Zookeeper集群在启动的时候需要从集群所有服务器中选举出一个Leader,由这个Leader来负责管理集群。Leader在整个集群中是唯一的,集群中的其它服务为这个Leader的follower。当集群中的Leader故障时候,Zookeeper需要能够快速的从Followers中选举出下一个Leader。因此,Leader是整个集群工作机制的核心,主要职责如下:
- Leader提供读写服务
- Zookeeper中所有的事务操作都是通过Leader发起的,当follower超过半数响应后commit事务,确认事务成功
- 集群内部各服务器的调度者
2)跟随者Follower
- 提供读服务
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与事务请求Proposal的投票
- 参与Leader选举,参与过半写成功策略
3)观察者Observer
Observer通常用于提升Zookeeper集群的非事务处理能力,通过水平扩展Observer机器来提升Zookeeper集群的读性能。Observer和Follower的区别在于:
- Observer只提供读服务,不参与Leader选举,也不参与写操作过半写成功的策略
- 对于非事务请求,都可以独立处理,而对于事务请求,则会转发给Leader进行处理
1.2 Zookeeper中的数据模型
Zookeeper中的数据模型和Linux文件系统类似的树形结构,不同的是在zookeeper中没有目录和文件的区别,每个节点统一称为znode。Znode是Zookeeper中数据的最小单元,每个Znode上都可以保存数据,同时还可以增加子节点。
提示:比如/Nginx/conf,/是一个znode,/Nginx是/的子znode,/Nginx还可以包含数据,数据内容就是所有安装Nginx的机器IP,/Nginx/conf是/Nginx子znode,它也可以包含内容,数据就是Nginx的配置文件内容。在应用中,我们可以通过这样一个路径就可以获得所有安装Nginx的机器IP列表,还可以获得这些机器上Nginx的配置文件。
Zookeeper中的znode包含四部分内容:
- data:保存数据
- acl:权限,包括create、write、read、delete和admin等权限
- stat:表述当前znode的元数据
- child:当前节点的子节点
1.2.1 ZXID
在Zookeeper中,事务是指能够改变ZooKeeper服务器状态的操作,一般包括节点创建与删除、数据节点内容更新和客户端会话创建与失效等。对于每个事务请求,Zookeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是64位的数字。每个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序。
1.2.2 Znode类型
Zookeeper中的znode节点都有其生命周期,根据不同的生命周期分为不同的类型:持久的(persistent)和短暂的(ephemeral)。
- 持久的表示客户端和服务器端断开连接后,创建的节点不删除
- 短暂的客户端和服务器端断开连接后,创建的节点自己删除
根据上面两种类型,Znode有以下四种形式:
- 持久节点(PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
- 持久节点顺序节点(PERSISTENT_SEQUENTIAL):所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号。
- 临时节点(EPHEMERAL) :和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
- 临时顺序节点(EPHEMERAL_SEQUENTIAL):临时顺序节点结合临时节点和顺序节点的特点,在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除。
1.2.3 Znode节点属性说明
Znode节点的属性如下表所示:
属性名称 | 类型 | 描述 |
---|---|---|
czxid | long | 节点被创建的Zxid值 |
mzxid | long | 节点被修改的Zxid值 |
pzxid | long | 子节点最后一次被修改时候的zxid值 |
ctime | long | 节点被创建的时间 |
mtime | long | 节点最后一次被修改的时间 |
version | long | 节点被修改的版本号 |
dataVersion | long | 节点的内容被修改的版本号 |
aversion | long | 节点的ACL被修改的版本号 |
ephemeralOwner | long | 如果为临时节点,该值为这个节点拥有的会话id;否则,该值为0 |
dataLength | int | 节点数据域的长度 |
numChildren | int | 节点拥有的子节点个数 |
1.2.4 版本号Version
ZooKeeper使用版本号来保证分布式数据原子性操作,对znode的任何更新操作都会引起版本号的变化。每个Znode都具有三种类型的版本信息:
- version:当前数据节点数据内容的版本号
- dataVersion:当前数据节点内容的版本号
- aversion:当前数据节点ACL变更版本号
需要注意的是,Zookeeper中的版本表示对Znode节点的数据内容、子节点列表,或是节点ACL信息的修改次数。这里强调的是修改次数,即使前后两次变更子节点中的数据内容并没有发生变化,version的值依然会更新。
1.2.5 Znode数据持久化
Zookeeper中的数据是运行在内存中的,必然需要持久化机制以保证数据的可恢复。Zookeeper中的持久化有两种方式:
- 事务日志:将执行的命令以日志的形式保存在文件中
- 数据快照:定期对内存中的数据做一次快照并保存在本地磁盘中
在故障恢复时,Zookeeper会先用快照进行恢复,然后使用事务日志进行增量恢复。
1.3 Zookeeper中的通知机制
Zookeeper允许客户端向服务器注册一个Watcher监听,当服务端发生了一些指定的事件,如节点创建删除、数据更新等,就会触发Watcher,并向指定的客户端发送一个事件通知。Zookeeper是通过Watcher机制实现分布式数据的发布/订阅功能。
ZooKeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、ZooKeeper服务器三部分。客户端在向ZooKeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当ZooKeeper服务器触发Watcherls /事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑。
1.4 Zookeeper的特性
Zookeeper的主要特性如下:
- 最终一致性:client不论连接到哪个Server(Leader、Follower或者Observer),client端看到的都是同一个视图,这是zookeeper最重要的特性。
- 可靠性:具有简单、健壮、良好的性能,如果消息被一台服务器接收,那么它将被所有的服务器接收。如果server端更新了一个操作,在客户端再次更新它之前,值是不会发生变化的。
- 可用性:Zookeeper集群架构保证了数据的可用性,没有锁并且超过半数的节点所拥有的数据都是最新的
- 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。
- Zookeeper提供的一致性是弱一致性,首先zookeeper集群确保对znode树的每一个修改都会被复制到集合体中超过半数的机器上,那么就有可能存在部分节点的数据不是最新的而被客户端访问到,这个时候集群中的数据是不一致的。另外,由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据。
- Zookeeper只保证最终一致性,但是实时的一致性可以由客户端调用自己来保证,通过调用sync()方法,在十几秒可以Sync到各个节点。
- 等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待,client之间是相互独立隔离的。
- 原子性:更新只能成功或者失败,没有中间状态。
- 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。
2、Zookeeper的工作原理
2.1 分布式一致性协议ZAB
Zookeeper通过广播机制实现各个节点之间的数据同步,实现这个机制的协议称为Zab协议。Zab协议的全称是Zookeeper Atomic Broadcast(Zookeeper原子广播),是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议 ,是Zookeeper保证数据一致性的核心算法。在前文“分布式一致性算法介绍”中专门有介绍相关内容。
2.1.1 ZAB协议四个阶段
ZAB协议主要有四个阶段:
- Leader election:主要是节点之间进行信息同步,选择出一个leader。leader选举过程,electionEpoch自增,在选举的时候lastProcessedZxid越大,越有可能成为leader
- Discovery:leader获取最新的消息。leader收集follower的lastProcessedZxid,这个主要用来通过和leader的lastProcessedZxid对比来确认follower需要同步的数据范围。选举出一个新的peerEpoch,主要用于防止旧的leader来进行提交操作
- Synchronization: leader将获取到的最新的数据同步到其他的从节点,并补全老数据,删除新数据。follower中的事务日志和leader保持一致的过程,就是依据follower和leader之间的lastProcessedZxid进行,follower多的话则删除掉多余部分,follower少的话则补充,一旦对应不上则follower删除掉对不上的zxid及其之后的部分然后再从leader同步该部分之后的数据
- Broadcast:整个集群就可以对外提供读写服务,且zookeeper集群正常状态下处于该阶段。leader针对客户端的事务请求,然后提出一个议案,发给所有的follower,一旦过半的follower回复OK的话,leader就可以将该议案进行提交了,向所有follower发送提交该议案的请求,leader同时返回OK响应给客户端
2.1.2 ZAB中一些基本概念
- zxid: 唯一标识一个trasaction, 全局唯一递增的64位整数。其中低32位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader都会产生一个新的事务Proposal并对该计数器进行+ 1操作;高32位则代表了Leader服务器上取出本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出对应的epoch值,然后再对这个值加一。
- zxid由 <epoch, count>组成
- Epoch: 每个leader生命周期的一个标识。newEpoch = lastEpoch + 1
- Count :表示每个Epoch期间发生的transaction id, 每个count 都是从0开始加一递增
- zxid的比较; 我们称zxid <e, c> 大于 zxid’<e’, c’>,当满足 (e > e’ ) || (e = e’ & c > c’).
2.1.3 选主流程
当Zookeeper集群中的leader故障或者失去大部分的follower,zookeeper会进入恢复模式,恢复模式需要选举一个新的leader,让所有的节点恢复正常的状态。ZK的选举算法有两种:一种是基于basic-paxos实现的,另外一种是基于fast-paxos实现的。系统默认的算法为fast-paxos,这里重点介绍fast-paxos的选主流程。
- 某个Server向整个Zookeeper集群发送<zxid,id>消息提议自己成为Leader,并等待其它Server响应
- 其它Server在收到提议后会进行epoch和zxid的判断比较,并将推荐的leader信息反馈回去
- 原来的Server在接收到其它Server的反馈信息后,重复选主的流程,最后选出Leader
2.1.4 同步流程
选举完Leader后,Zookeeper进入状态同步过程,将Leader数据同步到Follower节点:
- Leader等待server连接;
- Follower连接leader,将最大的zxid发送给Leader;
- Leader根据follower的zxid确定同步点;
- 完成同步后通知follower已经成为uptodate状态;
- Follower收到uptodate消息后,可以重新接受client的请求对外提供服务
2.2 Zookeeper工作流程
2.2.1 Leader工作流程
从前文中知道Zookeeper中Leader是整个集群内部各服务的调度者,对外提供读写服务。在集群内部,Leader主要有以下功能:
- 集群异常时从快照和日志中恢复数据
- 维持与Learner的心跳,接收Learner的请求并判断请求类型
- 根据不同的请求类型,进行不同的处理。Learner请求的消息类型主要有:PING消息、REQUEST消息、ACK消息、REVALIDATE消息
- PING消息是指Learner的心跳消息
- REQUEST消息是Follower发送的提议消息,包括写请求和同步请求
- ACK消息是Follower对提议的回复,超过半数的Follower通过,则提交该提议
- REVALIDATE消息用来延长Session的有效时间
Leader整个工作流程如下图所示:
2.2.2 Follower工作流程
在Zookeeper集群内部,Follower主要功能如下:
- 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
- 接收Leader消息并进行处理,Leader的消息有:
- PING消息:心跳消息
- PROPOSAL消息:Leader发起的提案,要求Follower投票;
- COMMIT消息:服务器端最新一次提案的信息;
- UPTODATE消息:表明同步完成;
- REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
- SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新
- 接收Client的请求,如果为写请求,发送给Leader进行投票;
- 返回Client结果。
Follower整个工作流程如下图所示:
3、Zookeeper使用场景
Zookeeper作为高可用的分布式协调与管理框架,在越来越多的分布式系统中作为核心组件引入使用。以下将介绍Zookeeper一些典型的使用场景,比如集群管理、服务订阅与发现、配置中心以及分布式锁的实现等。
3.1 集群管理
Zookeeper具有两大特性:
- 客户端如果对Zookeeper的一个znode数据节点注册Watcher监听,那么当数据节点的内容或其子节点的列表发生变更时,Zookeeper服务端就会向订阅的客户端发送变更通知
- 对于在Zookeeper上创建的临时节点,一旦客户端与服务端的会话失效,那么该临时节点也会被自动清除。
集群管理就是利用以上两个特性实现集群中各个主机的运行状态、主机的存活情况的实时监控。Zookeeper集群管理如下图所示:
- 监控系统启动时,先在Zookeeper中注册/clusterManager根节点
- 被监控集群一启动,就在/clusterManager根节点下创建相应的临时子节点,并将自己的运行状态信息定时写入临时子节点或根节点中
- 监控系统在根节点/clusterManager注册watcher监听。一旦被监控的集群有新节点加入或节点异常,就会触发子节点列表变更事件,并将事件推送给监控系统
- 监控系统在接收到Zookeeper触发的事件后,会触发watcher回调,更新监控信息,比如显示节点的存活状态
- 如果集群主机状态信息是写入到根节点数据内容的,监控系统需要在根节点上再注册一个数据内容变更的watcher监听,以实时获取到集群主机的状态数据
- 如果集群主机状态信息写入到临时节点,则监控系统需要在每个主机的临时节点注册数据内容变更的watcher监听,以实时获取到集群主机的状态数据
3.2 配置中心
所谓的配置中心也称为数据发布/订阅系统,就是发布者将数据发布到Zookeeper的一个或多个节点上以动态的获取数据,实现配置信息的集中式管理和数据的动态更新。
在Zookeeper中,客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知。客户端在接收到这个消息通知后,需要主动从服务器获取最新的数据。
- 配置存储:发布者在Zookeeper中选取一个znode节点用于数据存储
- 配置获取:客户端在初始化启动时从zookeeper的节点上读取数据内容。初始化完成后,客户端在配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据的变更,所有订阅的客户端都能收到数据变更的通知
- 配置更新:当进行配置变更时,只需要对Zookeeper中配置节点的内容进行更新,Zookeeper能将数据变更的通知发送到各个客户端。客户端接收到watcher事件后,会触发本地的watcher调用回调方法,从zookeeper中拉取节点的数据内容进行更新。
3.3 分布式锁
在前文1.2.2中介绍了Znode节点类型,Zookeeper分布式锁是基于临时顺序节点实现的。基于Zookeeper的分布式锁实现在“分布式锁的几种实现机制”中也深入介绍过。
3.3.1 排他锁实现
排他锁实现机制是线程在zookeeper上创建临时有序节点,使用watch监控资源节点等待获得锁。具体流程如下:
- 线程x申请锁资源时候,先去判断是否有其它事务持有锁,如果有需等待锁释放
- 多个线程1、2、3并发请求在zk上创建一个名为 /lock 的节点,同时只能有一个线程创建成功,假设线程1创建成功,那么线程2、3在创建,只会提示该节点已经存在,这样模拟线程1加锁成功,让他执行业务
- 此时让线程2、3加锁失败,就监听/lock这个节点,模拟排队等待锁被释放
- 当线程1执行完业务逻辑后,删除/lock节点,模拟释放锁
- 当/lock被删除后,就会被线程2、3监听到,他们就可以重新尝试创建该节点
在上述流程中,客户端通过调用create方法创建表示锁的临时节点/lock,创建成功的客户端获得了锁,同时让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况。当当前获得锁的客户端正常执行完业务逻辑,客户端会主动删除创建的临时节点。同时,如果获得锁的客户端发生宕机或异常,那么zookeeper上这个节点就会被删除也会释放锁资源。
3.3.2 共享锁实现
排他锁有一个缺点就是,如果并发量大,那么同一时刻会有很多连接对同一节点进行监听,但检测到删除事件后,zk需要通知所有的连接,所有连接收到监听后,会同一时间在发生高并发竞争,给性能带来严重损耗。多数场景下考虑使用共享锁实现:
- 读锁:共享锁,如果前面没有写节点,则直接上锁;如果前面有写节点,则等待距离自己最近的写节点释放锁
- 写锁:如果前面没有节点,则直接上锁,如果前面有节点,则等待释放
同时为了避免锁竞争,会使用公平锁机制,将没有获得锁的线程放入队列进行排队,等锁资源释放以后,按照先进先出的算法取出一个线程尝试获取锁。
- 通过Zookeeper上节点表示一个锁,类似于“/lockpath/[hostname]-请求类型-序号”的临时顺序节点
- 客户端通过调用create方法创建表示锁的临时顺序节点,如果是读请求为“/lockpath/[hostname]-R-序号”,写请求为“/lockpath/[hostname]-W-序号”
- 将临时节点加入到锁请求队列中
- 根据先进先出算法,锁队列中的第一个获取锁资源,判断读写请求分为以下步骤:
- 对于读请求,如果没有比自己序号更小的子节点或者比自己序号小的子节点都是读请求,那么表明已经成功获得共享锁,开始执行读取逻辑;如果有比自己序号小的子节点有写请求,则等待锁资源
- 对于写请求,如果不是序号最小的节点,则等待锁资源,否则获得锁开始处理业务
- 当事务处理完成或异常中断,锁资源释放以后会唤醒所有在队列中的线程,从第四步开始尝试重新申请锁资源
- 线程会注册Watcher监听lockpath子节点中前一个节点的状态,形成一个等待队列
3.3.3 实现分析
Zookeeper实现分布式锁有以下特征:
- 解决不可重入:客户端加锁时将主机和线程信息写入锁中,下一次再来加锁时直接和序列最小的节点对比,如果相同,则加锁成功,锁重入。
- 锁释放时机:由于创建的节点是顺序临时节点,当客户端获取锁成功之后session会话突然断开,ZK也会自动删除这个临时节点。
- 单点问题:ZK是集群部署的,主要一半以上的机器存活,就可以保证服务可用性。
3.4 服务发现
Zookeeper的服务发现功能主要是根据指定名称来获取资源或服务的地址、提供者等信息。利用znode的特点以及watcher机制,将服务名称和对应的服务器列表信息动态注册到znode节点中,能够实时感知到后端服务器的状态变化。服务发现有以下作用:
- 负载均衡:轮询服务注册表,尽可能将服务请求均匀分配到所有注册有效的服务器上
- 健康检查:动态维护服务地址注册表,利用心跳请求实时监控注册服务状态,删除无效服务节点,维护有效的地址注册表
- 调用监控:通过统计注册表中国各个子节点被访问次数来监控服务调用情况
- 动态路由:通过配置注册表参数,在不修改服务代码的情况下,动态指定服务访问的机器
- 动态配置:注册表的子节点可以作为单服务器的配置中心,可以不需要修改代码直接修改节点配置,动态修改服务运行的部分参数。
4、总结
从集群管理到Leader选举,从服务发现到服务订阅发布,再到分布式锁的实现,Zookeeper已经作为基础软件广泛应用于分布式应用系统中。本文从Zookeeper的集群架构入手,简要介绍了Leader和Learner几种角色,以及Znode的数据模型,并深入分析了基于ZAB一致性协议的选主流程和工作机制。最后结合几种常见的应用场景进行分析,以加深对Zookeeper的理解。
参考资料:
- https://zookeeper.apache.org/doc/r3.8.0/zookeeperOver.html
- https://blog.csdn.net/weixin_41987908/article/details/104749027
- https://blog.csdn.net/yfm081616/article/details/115609721
- https://blog.csdn.net/u013068377/article/details/52620647
- https://blog.csdn.net/fy_java1995/article/details/109104219
- https://blog.csdn.net/weixin_41947378/article/details/107062257