找回密码
 会员注册
查看: 33|回复: 0

ZooKeeper核心通识

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64454
发表于 2024-9-20 19:03:52 | 显示全部楼层 |阅读模式
作者:mosun,腾讯PCG后台开发工程师文章分三部分展开陈述:ZooKeeper核心知识、ZooKeeper的典型应用实现原理、ZooKeeper在中间件的落地案例。为了应对大流量,现代应用/中间件通常采用分布式部署,此时不得不考虑CAP问题。ZooKeeper(后文简称ZK)是面向CP设计的一个开源的分布式协调框架,将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、集群管理、Master选举、分布式锁、分布式队列等功能。ZK之所以能够提供上述一套分布式数据一致性解决方案,核心在于其设计精妙的数据结构、watcher机制、Zab一致性协议等,下面将依次剖析。数据结构ZK在内存中维护了一个类似文件系统的树状数据结构实现命名空间(如下),树中的节点称为znode。然而,znode要比文件系统的路径复杂,既可以通过路径访问,又可以存储数据。znode具有四个属性data、acl、stat、children,如下public class DataNode implements Record {    byte data[];    Long acl;    public StatPersisted stat;    private Set children = null;}data:znode相关的业务数据均存储在这里,但是,父节点不可存储数据;children:存储当前节点的子节点引用信息,因为内存限制,所以znode的子节点数不是无限的;stat:包含znode节点的状态信息,比如:事务id、版本号、时间戳等,其中事务id和ZK的数据一直性、选主相关,下面将重点介绍;acl:记录客户端对znode节点的访问权限;注意:znode的数据操作具有原子性,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。znode可存储的最大数据量是1MB,但实际上我们在znode的数据量应该尽可能小,因为数据过大会导致zk的性能明显下降。每个ZNode都对应一个唯一的路径。事物ID:ZxidZxid由Leader节点生成。当有新写入事件时,Leader节点生成新的Zxid,并随提案一起广播。Zxid的生成规则如下:epoch:任期/纪元,Zxid的高32位,ZAB协议通过epoch编号来区分Leader周期变化,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期;可以假设leader就像皇帝,epoch则相当于年号,每个皇帝都有自己的年号;事务计数器:Zxid的低32位,每次数据变更,计数器都会加一;zxid是递增的,所以谁的zxid越大,就表示谁的数据是最新的。每个节点都保存了当前最近一次事务的Zxid。Zxid对于ZK的数据一致性以及选主都有着重要意义,后边在介绍相关知识时会重点讲解其作用原理。znode类型节点根据生命周期的不同可以将划分为持久节点和临时节点。持久节点的存活时间不依赖于客户端会话,只有客户端在显式执行删除节点操作时,节点才消失;临时节点的存活时间依赖于客户端会话,当会话结束,临时节点将会被自动删除(当然也可以手动删除临时节点)。注意:临时节点不能拥有子节点。节点类型是在创建时进行制定,后续不能改变。如create/n1node1创建了一个数据为”node1”的持久节点/n1;在上述指令基础上加上参数-e:create-e/n1/n3node3,则创建了一个数据为”node3”的临时节点/n1/n3。create命令还有一个可选参数-s用于指定创建的节点是否具有顺序特性。创建顺序节点时,zk会在路径后面自动追加一个递增的序列号,这个序列号可以保证在同一个父节点下是唯一的,利用该特性我们可以实现分布式锁等功能。基于znode的上述两组特性,两两组合后可构建4种类型的节点:PERSISTENT:永久节点EPHEMERAL:临时节点PERSISTENT_SEQUENTIAL:永久顺序节点EPHEMERAL_SEQUENTIAL:临时顺序节点Watcher监听机制Watcher监听机制是ZK非常重要的一个特性。ZK允许Client端在指定节点上注册Watcher,监听节点数据变更、节点删除、子节点状态变更等事件,当特定事件发生时,ZK服务端会异步通知注册了相应Watcher的客户端,通过该机制,我们可以利用ZK实现数据的发布和订阅等功能。Watcher监听机制由三部分协作完成:ZK服务端、ZK客户端、客户端的WatchManager对象。工作时,客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当ZK服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑。注意:watcher变更通知是一次性的:当数据发生变化的时候,ZK会产生一个watcher事件,并且会发送到客户端。但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置Watcher的客户端不会再次收到消息。可以通过循环监听去达到永久监听效果。客户端watcher顺序回调:watcher回调是顺序串行化执行的,只有回调后客户端才能看到节点最新的状态。watcher回调逻辑不应太复杂,否则可能影响watcher执行。不会告诉节点变化前后的具体内容:watchEvent是最小的通信单元,结构上包含通知状态、事件类型和节点路径,但是,不会告诉节点变化前后的具体内容。时效性:watcher只有在当前session彻底失效时才会无效,若在session有效期内快速重连成功,则watcher依然存在,仍可收到事件通知。ZK集群为了确保服务的高可用性,ZK采用集群化部署,如下:ZK集群服务器有三种角色:Leader、Follower和ObserverLeader:一个ZK集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。Follower:一个ZK集群可同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,参与事务请求Proposal的投票及Leader选举投票。Observer:Observer是3.3.0版本开始引入的一个服务器角色,一个ZK集群可同时存在多个Observer,功能与Follower类似,但是,不参与投票。“早期的ZooKeeper集群服务运行过程中,只有Leader服务器和Follow服务器。随着集群规模扩大,follower变多,ZK在创建节点和选主等事务性请求时,需要一半以上节点AC,所以导致性能下降写入操作越来越耗时,follower之间通信越来越耗时。为了解决这个问题,就引入了观察者,可以处理读,但是不参与投票。既保证了集群的扩展性,又避免过多服务器参与投票导致的集群处理请求能力下降。”ZK集群中通常有很多服务器,那么如何区分不同的服务器的角色呢?可以通过服务器的状态进行区分LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。LEADING:领导者状态。表明当前服务器角色是Leader。FOLLOWING:跟随者状态,同步leader状态,参与投票。表明当前服务器角色是Follower。OBSERVING:观察者状态,同步leader状态,不参与投票。表明当前服务器角色是Observer。ZK集群是一主多从的结构,所有的所有的写操作必须要通过Leader完成,Follower可直接处理并返回客户端的读请求。那么如何保证从Follower服务器读取的数据与Leader写入的数据的一致性呢?Leader万一由于某些原因崩溃了,如何选出新的Leader,如何保证数据恢复?Leader是怎么选出来的?Zab一致性协议ZK专门设计了ZAB协议(ZookeeperAtomicBroadcast)来保证主从节点数据的一致性。下面分别从client向Leader和Follower写数据场景展开陈述。写Leader场景数据一致性客户端向Leader发起写请求Leader将写请求以Proposal的形式发给所有Follower并等待ACKFollower收到Leader的Proposal后返回ACKLeader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送CommmitLeader将处理结果返回给客户端注意:Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有4个Follower,只需其中两个返回ACK即可,因为(2+1)/(4+1)>1/2Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据写Follower场景数据一致性1.客户端向Follower发起写请求,Follower将写请求转发给Leader处理;其它流程与直接写Leader无任何区别注意:Observer与Follower写流程相同最终一致性Zab协议消息广播使用两阶段提交的方式,达到主从数据的最终一致性。为什么是最终一致性呢?从上文可知数据写入过程核心分成下面两阶段:第一阶段:Leader数据写入事件作为提案广播给所有Follower结点;可以写入的Follower结点返回确认信息ACK。第二阶段:Leader收到一半以上的ACK信息后确认写入可以生效,向所有结点广播COMMIT将提案生效。根据写入过程的两阶段的描述,可以知道ZooKeeper保证的是最终一致性,即Leader向客户端返回写入成功后,可能有部分Follower还没有写入最新的数据,所以是最终一致性。ZooKeeper保证的最终一致性也叫顺序一致性,即每个结点的数据都是严格按事务的发起顺序生效的。ZooKeeper集群的写入是由Leader结点协调的,真实场景下写入会有一定的并发量,那Zab协议的两阶段提交是如何保证事务严格按顺序生效的呢?ZK事物的顺序性是借助上文中的Zxid实现的。Leader在收到半数以上ACK后会将提案生效并广播给所有Follower结点,Leader为了保证提案按ZXID顺序生效,使用了一个ConcurrentHashMap,记录所有未提交的提案,命名为outstandingProposals,key为ZXID,Value为提案的信息。对outstandingProposals的访问逻辑如下:Leader每发起一个提案,会将提案的ZXID和内容放到outstandingProposals中,作为待提交的提案;Leader收到Follower的ACK信息后,根据ACK中的ZXID从outstandingProposals中找到对应的提案,对ACK计数;执行tryToCommit尝试将提案提交:判断流程是,先判断当前ZXID之前是否还有未提交提案,如果有,当前提案暂时不能提交;再判断提案是否收到半数以上ACK,如果达到半数则可以提交;如果可以提交,将当前ZXID从outstandingProposals中清除并向Followers广播提交当前提案;Leader是如何判断当前ZXID之前是否还有未提交提案的呢?由于前提是保证顺序提交的,所以Leader只需判断outstandingProposals里,当前ZXID的前一个ZXID是否存在。代码如下:所以ZooKeeper是通过两阶段提交保证数据的最终一致性,并且通过严格按照ZXID的顺序生效提案保证其顺序一致性的。选主原理ZK中默认的并建议使用的Leader选举算法是:基于TCP的FastLeaderElection。在分析选举原理前,先介绍几个重要的参数。服务器ID(myid):每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的ID(整数)。该参数在选举时如果无法通过其他判断条件选择Leader,那么将该ID的大小来确定优先级。事务ID(zxid):单调递增,值越大说明数据越新,权重越大。逻辑时钟(epoch-logicalclock):同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加。ZK的leader选举存在两类,一个是服务器启动时leader选举,另一个是运行过程中服务器宕机时的leader选举,下面依次展开介绍。以下两节引自从0到1详解ZooKeeper的应用场景及架构。服务器启动时的leader选举1、各自推选自己:ZooKeeper集群刚启动时,所有服务器的logicClock都为1,zxid都为0。各服务器初始化后,先把第一票投给自己并将它存入自己的票箱,同时广播给其他服务器。此时各自的票箱中只有自己投给自己的一票,如下图所示:2、更新选票:第一步中各个服务器先投票给自己,并把投给自己的结果广播给集群中的其他服务器,这一步其他服务器接收到广播后开始更新选票操作,以Server1为例流程如下:(1)Server1收到Server2和Server3的广播选票后,由于logicClock和zxid都相等,此时就比较myid;(2)Server1收到的两张选票中Server3的myid最大,此时Server1判断应该遵从Server3的投票决定,将自己的票改投给Server3。接下来Server1先清空自己的票箱(票箱中有第一步中投给自己的选票),然后将自己的新投票(1->3)和接收到的Server3的(3->3)投票一起存入自己的票箱,再把自己的新投票决定(1->3)广播出去,此时Server1的票箱中有两票:(1->3),(3->3);(3)同理,Server2收到Server3的选票后也将自己的选票更新为(2->3)并存入票箱然后广播。此时Server2票箱内的选票为(2->3),(3->3);(4)Server3根据上述规则,无须更新选票,自身的票箱内选票仍为(3->3);(5)Server1与Server2重新投给Server3的选票广播出去后,由于三个服务器最新选票都相同,最后三者的票箱内都包含三张投给服务器3的选票。3、根据选票确定角色:根据上述选票,三个服务器一致认为此时Server3应该是Leader。因此Server1和Server2都进入FOLLOWING状态,而Server3进入LEADING状态。之后Leader发起并维护与Follower间的心跳。运行时Follower重启选举本节讨论Follower节点发生故障重启或网络产生分区恢复后如何进行选举。1、Follower重启投票给自己:Follower重启,或者发生网络分区后找不到Leader,会进入LOOKING状态并发起新的一轮投票。2、发现已有Leader后成为Follower:Server3收到Server1的投票后,将自己的状态LEADING以及选票返回给Server1。Server2收到Server1的投票后,将自己的状态FOLLOWING及选票返回给Server1。此时Server1知道Server3是Leader,并且通过Server2与Server3的选票可以确定Server3确实得到了超过半数的选票。因此服务器1进入FOLLOWING状态。运行时Leader重启选举Follower发起新投票:Leader(Server3)宕机后,Follower(Server1和2)发现Leader不工作了,因此进入LOOKING状态并发起新的一轮投票,并且都将票投给自己,同时将投票结果广播给对方。2、更新选票:(1)Server1和2根据外部投票确定是否要更新自身的选票,这里跟之前的选票PK流程一样,比较的优先级为:logicLock>zxid>myid,这里Server1的参数(L=3,M=1,Z=11)和Server2的参数(L=3,M=2,Z=10),logicLock相等,zxid服务器1大于服务器2,因此服务器2就清空已有票箱,将(1->1)和(2->1)两票存入票箱,同时将自己的新投票广播出去(2)服务器1收到2的投票后,也将自己的票箱更新。3、重新选出Leader:此时由于只剩两台服务器,服务器1投票给自己,服务器2投票给1,所以1当选为新Leader。4、旧Leader恢复发起选举:之前宕机的旧Leader恢复正常后,进入LOOKING状态并发起新一轮领导选举,并将选票投给自己。此时服务器1会将自己的LEADING状态及选票返回给服务器3,而服务器2将自己的FOLLOWING状态及选票返回给服务器3。5、旧Leader成为Follower:服务器3了解到Leader为服务器1,且根据选票了解到服务器1确实得到过半服务器的选票,因此自己进入FOLLOWING状态。脑裂对于一主多从类的集群应用,通常要考虑脑裂问题,脑裂会导致数据不一致。那么,什么是脑裂?简单点来说,就是一个集群有两个master。通常脑裂产生原因如下:假死:由于心跳超时(网络原因导致的)认为Leader死了,但其实Leader还存活着。脑裂:由于假死会发起新的Leader选举,选举出一个新的Leader,但旧的Leader网络又通了,导致出现了两个Leader,有的客户端连接到老的Leader,而有的客户端则连接到新的Leader。通常解决脑裂问题有Quorums(法定人数)方式、Redundantcommunications(冗余通信)方式、仲裁、磁盘锁等方式。ZooKeeper采用Quorums这种方式来防止“脑裂”现象,只有集群中超过半数节点投票才能选举出Leader。典型应用场景数据发布/订阅我们可基于ZK的Watcher监听机制实现数据的发布与订阅功能。ZK的发布订阅模式采用的是推拉结合的方式实现的,实现原理如下:当集群中的服务启动时,客户端向ZK注册watcher监听特定节点,并从节点拉取数据获取配置信息;当发布者变更配置时,节点数据发生变化,ZK会发送watcher事件给各个客户端;客户端在接收到watcher事件后,会从该节点重新拉取数据获取最新配置信息。注意:Watch具有一次性,所以当获得服务器通知后要再次添加Watch事件。负载均衡利用ZK的临时节点、watcher机制等特性可实现负载均衡,具体思路如下:把ZK作为一个服务的注册中心,基本流程:服务提供者server启动时在ZK进行服务注册(创建临时文件);服务消费者client启动时,请求ZK获取最新的服务存活列表并注册watcher,然后将获得服务列表保存到本地缓存中;client请求server时,根据自己的负载均衡算法,从服务器列表选取一个进行通信。若在运行过程中,服务提供者出现异常或人工关闭不能提供服务,临时节点失效,ZK探测到变化更新本地服务列表并异步通知到服务消费者,服务消费者监听到服务列表的变化,更新本地缓存注意:服务发现可能存在延迟,因为服务提供者挂掉到缓存更新大约需要3-5s的时间(根据网络环境不同还需仔细测试)。为了保证服务的实时可用,client请求server发生异常时,需要根据服务消费报错信息,进行重负载均衡重试等。命名服务命名服务是指通过指定的名字来获取资源或者服务的地址、提供者等信息。以znode的路径为名字,znode存储的数据为值,可以很容易构建出一个命名服务。例如Dubbo使用ZK来作为其命名服务,如下所有Dubbo相关的数据都组织在/dubbo的根节点下;二级目录是服务名,如com.foo.BarService;三级目录有两个子节点,分别是providers和consumers,表示该服务的提供者和消费者;四级目录记录了与该服务相关的每一个应用实例的URL信息,在providers下的表示该服务的所有提供者,而在consumers下的表示该服务的所有消费者。举例说明,com.foo.BarService的服务提供者在启动时将自己的URL信息注册到/dubbo/com.foo.BarService/providers下;同样的,服务消费者将自己的信息注册到相应的consumers下,同时,服务消费者会订阅其所对应的providers节点,以便能够感知到服务提供方地址列表的变化。集群管理基于ZK的临时节点和watcher监听机制可实现集群管理。集群管理通常指监控集群中各个主机的运行时状态、存活状况等信息。如下图所示,主机向ZK注册临时节点,监控系统注册监听集群下的临时节点,从而获取集群中服务的状态等信息。Master选举ZK中某节点同一层子节点,名称具有唯一性,所以,多个客户端创建同一节点时,只会有一个客户端成功。利用该特性,可以实现maser选举,具体如下:多个客户端同时竞争创建同一临时节点/master-election/master,最终只能有一个客户端成功。这个成功的客户端成为Master,其它客户端置为Slave。Slave客户端都向这个临时节点的父节点/master-election注册一个子节点列表的watcher监听。一旦原Master宕机,临时节点就会消失,zk服务器就会向所有Slave发送子节点变更事件,Slave在接收到事件后会竞争创建新的master临时子节点。谁创建成功,谁就是新的Master。分布式锁基于ZK的临时顺序节点和Watcher机制可实现公平分布式锁。下面具体看下多客户端获取及释放zk分布式锁的整个流程及背后的原理。下面过程引自七张图彻底讲清楚ZooKeeper分布式锁的实现原理【石杉的架构笔记】。假如说客户端A先发起请求,就会搞出来一个顺序节点,大家看下面的图,Curator框架大概会弄成如下的样子:这一大坨长长的名字都是Curator框架自己生成出来的。然后,因为客户端A是第一个发起请求的,所以给他搞出来的顺序节点的序号是"1"。接着客户端A会查一下"my_lock"这个锁节点下的所有子节点,并且这些子节点是按照序号排序的,这个时候大概会拿到这么一个集合:接着客户端A会走一个关键性的判断:唉!兄弟,这个集合里,我创建的那个顺序节点,是不是排在第一个啊?如果是的话,那我就可以加锁了啊!因为明明我就是第一个来创建顺序节点的人,所以我就是第一个尝试加分布式锁的人啊!bingo!加锁成功!大家看下面的图,再来直观的感受一下整个过程。假如说客户端A加完锁完后,客户端B过来想要加锁,这个时候它会干一样的事儿:先是在"my_lock"这个锁节点下创建一个临时顺序节点,因为是第二个来创建顺序节点的,所以zk内部会维护序号为"2"。接着客户端B会走加锁判断逻辑,查询"my_lock"锁节点下的所有子节点,按序号顺序排列,此时看到的类似于:同时检查自己创建的顺序节点,是不是集合中的第一个?明显不是,此时第一个是客户端A创建的那个顺序节点,序号为"01"的那个。所以加锁失败!加锁失败了以后,客户端B就会通过ZK的API对他的顺序节点的上一个顺序节点加一个监听器,即对客户端A创建的那个顺序节加监听器!如下接着,客户端A加锁之后,可能处理了一些代码逻辑,然后就会释放锁。那么,释放锁是个什么过程呢?其实很简单,就是把自己在zk里创建的那个顺序节点,也就是:这个节点被删除。删除了那个节点之后,zk会负责通知监听这个节点的监听器,也就是客户端B之前加的那个监听器,说:兄弟,你监听的那个节点被删除了,有人释放了锁。此时客户端B的监听器感知到了上一个顺序节点被删除,也就是排在他之前的某个客户端释放了锁。此时,就会通知客户端B重新尝试去获取锁,也就是获取"my_lock"节点下的子节点集合,此时为:集合里此时只有客户端B创建的唯一的一个顺序节点了!然后呢,客户端B判断自己居然是集合中的第一个顺序节点,bingo!可以加锁了!直接完成加锁,运行后续的业务代码即可,运行完了之后再次释放锁。注意:利用ZK实现分布式锁时要避免出现惊群效应。上述策略中,客户端B通过监听比其节点顺序小的那个临时节点,解决了惊群效应问题。分布式队列基于ZK的临时顺序节点和Watcher机制可实现简单的FIFO分布式队列。ZK分布式队列和上节中的分布式锁本质是一样的,都是基于对上一个顺序节点进行监听实现的。具体原理如下:利用顺序节点的有序性,为每个数据在/FIFO下创建一个相应的临时子节点;且每个消费者均在/FIFO注册一个watcher;消费者从分布式队列获取数据时,首先尝试获取分布式锁,获取锁后从/FIFO获取序号最小的数据,消费成功后,删除相应节点;由于消费者均监听了父节点/FIFO,所以均会收到数据变化的异步通知,然后重复2的过程,尝试消费队列数据。依此循环,直到消费完毕。中间件落地案例KafkaZK在Kafka集群中扮演着极其重要的角色。Kafka中很多信息都在ZK中维护,如broker集群信息、consumer集群信息、topic相关信息、partition信息等。Kafka的很多功能也是基于ZK实现的,如partition选主、broker集群管理、consumer负载均衡等,限于篇幅本文将不展开陈述,这里先附一张网上截图大家感受下,详情将在Kafka专题中细聊。DubboDubbo使用Zookeeper用于服务的注册发现和配置管理,详情见上文“命名服务”。参考文献https://mp.weixin.qq.com/s/tiAQQXbh7Tj45_1IQmQqZghttps://www.jianshu.com/p/68b45694026chttps://time.geekbang.org/column/article/239261https://blog.csdn.net/lihao21/article/details/51810395https://zhuanlan.zhihu.com/p/378018463https://juejin.cn/post/6974737393324654628https://blog.csdn.net/liuao107329/article/details/78936160https://blog.csdn.net/en_joker/article/details/78799737https://blog.51cto.com/u_15077535/4199740https://juejin.cn/post/6844903729406148622https://blog.csdn.net/Saintmm/article/details/124110149https://www.wumingx.com/linux/zk-kafka.html抽奖红包封面后台回复:1024参与
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 00:36 , Processed in 0.458424 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表