zk是apache下的一个开源项目,官方介绍为:"zooKeeper是一种集中式服务,用于维护配置信息,命名,提供分布式同步和提供组服务"。
zk集群中同一个时间只有一个leader,它会和各个follower和observer发起进行心跳,来保证集群中各个节点是否是可用的。进行写操作,并且将写操作的结果同步到各个follower和observer上。
zk集群中可能会有多个follower,它会响应leader的心跳。对客户端提供读数据的功能,将写请求转发给leader,并且参与写请求的半数投票,在leader挂掉后,可以参与新的leader的选举。
和follower类似,但是没有投票权,增加集群读数据的能力。
为了保证写操作的一致性,zk专门设计了一种名为原子广播(ZAB)的支持崩溃恢复的协议。基于该协议,zk实现了一种主从模式的系统架构来保持集群中各个副本的数据一致性。
ZAB协议中,所有的写操作都由leader完成,leader写入本地日志后,再将数据同步给follower和observer。一旦leader无法工作,ZAB协议会自动从follower中选举一个节点成为新的leader(即领导选举),重新对外提供服务。
如图所示,写操作会有5步
1.客户端向leader发起写请求,也可以将follower和observer发起写请求,follower和observer会将写请求转发给leader
2.leader将写请求以proposal的形式发送给各个follower,并等待ACK
3.follower收到leader的propose请求后,返回ACK给leader
4.leader得到半数的ACK后(这里的ACK其实就是投票,半数指的是(leader+follower)/2,leader默认会给自己投一票),向所有的follower和observer发送commit,将写数据同步给各个节点。
5.从接受client请求的那个节点把response返回给客户端
leader,follower,observer都可直接处理读请求,从本地内存读取数据并返回给客户端
由于读请求不需要节点之间的交互,所以follower,observer越多,整体可处理的读请求越大,即读性能越好。
当集群中的leader挂掉时,需要重新选举一个follower作为leader,这时就需要使用fastleaderElection来进行领导的选举了。
每个zk节点,都需要在数据文件下创建一个myid文件,该文件包含整个集群唯一的Id(整数)。例如,某ZooKeeper集群包含三个节点,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
类似于RDBMS中的事务ID,用于标识一次更新操作的proposal ID,为了保证顺序性,zxid单调递增。zk使用一个64数来表示zxid,高32位为leader的epoch(用来表示这是第几次的选举),从1开始,每次选举出一个新的leader,epoch加1。低32位为该epoch的序列号,每次epoch变化,都将低32位进行重置,这样保证了zxid的全局递增性。高32位越大,说明参与成功选举的次数越多。低32位越大,说明在当前epoch内,收到leader的proposal(写提议)的次数越多。
Leading: 领导者状态,表示当前节点是leader节点,它会维护follower,obsever之间的心跳
Following: 跟随者状态,表示当前节点是follower,它知道当前集群内leader是谁
Observing: 观察者状态,表示当前节点是obsderver,和following相似,只是它不参与选举投票和写操作投票
Looking: 不确定leader状态,该状态下认为当前集群内没有leader,会发起一次leader选举投票
每个节点进行领导选举时,会发送如下关键信息:
logicClock: 每个节点会维护一个自增的整数,名为logicClock,它表示这是当前节点时第多少轮发起的投票选举。
state: 当前节点的状态
self_id: 当前节点的myid
self_zxid: 当前节点上所保存的最大的zxid
vote_id: 当前节点投票的节点的myid
vote_zxid: 当前节点投票的节点的最大的zxid
自增选举轮次:zk规定所有投票都必须在同一轮次中,每个节点在开始新一轮的投票时,都会对logicClock进行自增操作。
初始化选票:每个节点在广播自己的选票前,会将自己的投票箱清空,该投票箱记录了由其他节点广播来的选票。例:节点2投票给节点3,节点3投票给节点1,则节点1的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它节点收到该新选票后会在自己票箱中更新该节点的选票。
发送初始化选票:每个节点开始都是通过广播把票投给自己,初始化投票投的都是自己。如节点1的vote_id就是self_id,vote_zixd都是self_zxid。
接受外部选票:每个节点会尝试从其它节点获取选票,并记录到自己的投票箱内。如果无法获取到外部选票,则会确认自己是否和其他节点保持着有效连接。如果是,则重新发送自己的选票。如果否,则马上与之建立连接。
判断选举轮次:收到选举投票后,首先会根据投票信息中的logicClock来进行不同的处理
1.外部投票的logicClock大于自己的logicClock,说明当前节点投票轮次落后于其他节点的投票轮次,清空自己的投票箱,并且把自己的logicClock更新为外部投票的logicClock,然后再对比自己的投票和收到的投票,确定是否需要变更自己的投票,最终再将自己的投票广播出去。
2.外部投票的logicClock小于自己的logicClock,当前节点的轮次大于收到的外部选票,直接忽略。
3.外部投票和自己的logicClock相等,则进行选票PK
选票PK:选票PK是基于self_id,self_zxid和vote_id,vote_zxid的对比来进行的
1.外部投票的logicClock大于自己的logicClock,将自己的logicClock以及自己选票的logicClock更新为收到的外部投票的logicClock
2.若logicClock一致,则对比两个的vote_zxid,若外部投票的vote_zxid大,则将自己选票的vote_zxid和vote_id更新为外部投票的vote_zxid和vote_id并广播出去,另外将收到的票以及自己更新后的票放进自己的投票箱,如果箱内已存在已存在相同的选票(self_id,self_zxid),则直接覆盖
3.若两者的vote_zxid一致,则比较vote_id,若外部投票的vote_id比较大,则将自己票中的vote_id更新成外部投票的vote_id并广播出去,另外将收到的票以及自己更新过后的选票放进自己的投票箱(其实有点不公平,myid成为领导的可能性就越大)
统计选票:如果已经确定有过半节点投出了认可自己的投票(可能是更新后的投票),则中止投票,否则接受其他节点的投票,继续进行投票选举
更新节点状态:投票中止后,各个节点会更新自己的状态,若过半的节点都将票投给了自己,则将自己的状态改成leading,否则将自己的状态改成following。
初始投票投给自己:集群刚启动时,所有节点的logiticClock都为1,zxid都为0,各节点初始化后,都投票给自己,并将自己的投票放入自己的票箱,如下图所示。
在上图中,(1,1,0)第一位数字代表投出选票的节点的logicClock,第二位则是vote_id,第三位是vote_zxid,由于是初始步骤,vote_id其实就是self_id,vote_zxid就是self_zxid。此时,各个节点的票箱中,只有自己投给自己的一票。
更新选票:节点收到外部投票后,会进行选票PK,相应更新自己的选票并广播出去,并将合适的选票放入自己的票箱。
如上图所示,节点1收到节点2(1,2,0)和节点3(1,3,0)的选票后,由于logicClock都相等,所有vote_zxid也都相等,这时就判断vote_id的大小,节点3的选票(1,3,0)最大。这时将自己的选票更新为(1,3,0),并将自己的票箱清空,将(1,3),(3,3)投入自己的票箱内,然后将更新后的选票(1,3,0)广播出去。
同理,节点2接受节点3的选票后,将自己的投票更新为(1,3,0),清空自己的票箱,将(2,3),(3,3)投入自己的票箱内。
节点3根据上述规则,不需要更新自己的选票,自身的票箱内的选票依然是(3,3)
节点1和节点2将自己更新过后的选票广播出去之后每个节点内的投票箱内的情况都是一致的。(1,3),(2,3),(3,3)
根据选票确定角色:根据上述选票,三个节点一致认为节点3应该是leader,因为节点1和节点2进入following状态,节点3进入leading状态。之后leader(节点3)负责维护和follower(节点1,节点2)之间的心跳连接。
Follower重启投票给自己:Follower重启,或者发生网络分区后找不到leader,会进入looking状态并发起新一轮的选举
发现已有leader后成为follower:节点3收到节点1的投票后,将自己的状态Leading以及选票返回给节点1,节点2收到节点1的投票后,将自己的状态following以及选票返回给节点1。此时节点1知道节点3是leader,并且通过节点2和节点3返回的选票可以确定节点3确实得到了半数以上的投票,因此节点1进入following状态
Follower发起新投票:leader(节点3)宕机后,follower(节点1,节点2)长时间没有收到心跳连接的请求后,知道leader宕掉了,因此进入looking状态,发起新一轮的投票,并且都将票投给自己
广播更新选票:节点1和节点2各自收到对方的投票后,判断是否要更新自己的选票,这里有两种情况
1.节点1和节点2的zxid相同,例如在节点3宕机前节点1和节点2完全与之同步,此时比较myid即可,节点2的myid大于节点1,节点1会更新自己的选票为节点2的选票。
2.节点1和节点2的zxid不同,在旧leader宕机之前,其主导的写操作,只需要半数节点确认即可,而不需要所有节点确认。换句话说,节点1和节点2可能一个与旧leader同步(即zxid相同),另一个与旧leader不同步(即zxid比leader小)。此时选票的更新取决于谁的zxid大。
在上图中,节点1的zxid为11,节点2的zxid为10。因此,节点2更新自己的选票为(3,1,11),并将自己的投票箱清空,放入(1,1),(2,1)到自己的投票箱。
选出新leader:经过上述的选票后,节点1和节点2的选票都投给了节点1。因此,节点1进入leading状态,节点2进入following状态,节点1成为leader后维护与节点2的心跳连接
旧leader恢复后重新发起选举:旧的leader恢复后,进入looking状态并发起新一轮的选举,并将票投给自己。节点1会将自己的leading状态和选票(3,1,11)返回给节点3,节点2会将自己的following状态和选票(3,1,11)返回给节点3。
旧Leader成为Follower:节点3收到返回的选票后,确认节点1是leader,并且节点1确实收到了半数的投票。因此,自己进入following状态。
FailOver前状态:为更好演示leader failover过程,为更好演示Leader Failover过程,本例中共使用5个ZooKeeper节点。A作为Leader,共收到P1、P2、P3三条消息,并且Commit了1和2,且总体顺序为P1、P2、C1、P3、C2。根据顺序性原则,其它Follower收到的消息的顺序肯定与之相同。其中B与A完全同步,C收到P1、P2、C1,D收到P1、P2,E收到P1,如下图所示。
这里要注意:
由于A没有C3,意味着收到P3的节点的总个数不会超过一半,也即包含A在内的最多只有两个节点收到P3,其他节点均未收到P3
由于A已经写入了C1,C2。说明它已经commit了P1,P2,因此整个集群有超过一半的节点,即最少三个节点收到P1、P2。在这里所有节点都收到了P1,除E外其它节点也都收到了P2
选出新leader:旧Leader也即A宕机后,其它节点根据上述FastLeaderElection算法选出B作为新的Leader。C、D和E成为Follower且以B为Leader后,会主动将自己最大的zxid发送给B,B会将Follower的zxid与自身zxid间的所有被Commit过的消息同步给Follower,如下图所示。
在上图中
P1和P2都被A commit,因此B会通过同步保证P1,P2,C1,C2都存在与C,D,E中
P3由于未被A commit,同时P3未存在于大数据幸存的节点中,因此它不会被同步到其他follower中。
通知Follower可对外服务:同步完数据后,B会向C,D,E发送NEWLEADER命令并等待大多数节点的ACK(下图中D和E已返回ACK,加上B自身,已经占集群的大多数),然后向所有节点发送UPTODATE命令,收到该命令的节点即可对外提供服务。
在上例中,P3未被A commit,同时因为没有过半节点收到P3,因此B也未commit P3(如果有过半节点收到P3,即使A没有 commit P3,B也会commit P3,即C3),所以B不会将P3广播出去
具体做法是,B成为leader后,先判断自身未commit的消息(即P3)是否存在于大多数的其他节点中,从而决定是否要将其commit。然后B可得出自身所包含的commit过的消息中的最小zxid(记作min_zxid)与最大zxid(max_zxid),C,D,E会向B发送自身commit过的最大消息的zxid(记为max_zxid)以及未被commit过的所有消息(记为zxid_sets),B根据这些消息进行如下操作
1.如果follower的max_zxid和leader的max_zxid完全相同,说明该follower和leader完全同步,无须同步任何数据
2.如果follower的max_zxid在leader的(min_zxid,max_zxid)范围内,leader会通过TRANC命令通知follower将其zxid_sets中大于follower中max_zxid的所有消息全部删除
(未解决的疑惑,如果存在一个follower节点的max_zxid和leader相同,但是存在未commit的比max_zxid大的消息,这种情况下这个未commit的消息不会被删除掉)
上述操作保证了未被Commit过的消息不会被Commit从而对外不可见。
上述例子中Follower上并不存在未被Commit的消息。但可考虑这种情况,如果将上述例子中的节点数量从五增加到七,节点F包含P1、P2、C1、P3,节点G包含P1、P2。此时节点F、A和B都包含P3,但是因为票数未过半,因此B作为Leader不会Commit P3,而会通过TRUNC命令通知F删除P3。如下图所示。
zk提供了一个类似于linux文件系统的树形结构,该树形内每个节点被称为znode,可按照如下两个维度分类。
从节点创建后保存的角度
从节点是否重复的角度
ZooKeeper简单高效,同时提供如下语义保证,从而使得我们可以利用这些特性提供复杂的服务。
顺序性:客户端发起的更新会按发送顺序被应用到 ZooKeeper 上
原子性:更新操作要么成功要么失败,不会出现中间状态
单一系统镜像:一个客户端无论连接到哪一个服务器都能看到完全一样的系统镜像(即完全一样的树形结构)。注:根据上文《ZooKeeper架构及FastLeaderElection机制》介绍的 ZAB 协议,写操作并不保证更新被所有的 Follower 立即确认,因此通过部分 Follower 读取数据并不能保证读到最新的数据,而部分 Follwer 及 Leader 可读到最新数据。如果一定要保证单一系统镜像,可在读操作前使用 sync 方法。
可靠性:一个更新操作一旦被接受即不会意外丢失,除非被其它更新操作覆盖
最终一致性:写操作最终(而非立即)会对客户端可见
对于zk的读操作而言,都可附带一个Watch,一旦相应的数据发生变法,则该watch被触发,类似于监听者机制。
Watch的特性
主动推送:Watch被触发时,由 zk服务器主动将更新推送给客户端,而不需要客户端轮询。
一次性:数据变化时,Watch 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watch 被触发后重新注册一个 Watch。
可见性:如果一个客户端在读请求中附带 Watch,Watch 被触发的同时再次读取数据,客户端在得到 Watch 消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。
顺序性:如果多个更新触发了多个 Watch ,那 Watch 被触发的顺序与更新顺序一致。
对于分布式锁(这里特指排它锁)而言,任意时刻,最多只有一个进程(对于单进程内的锁而言是单线程)可以获得锁。
对于分布式锁,需要保证获得锁的进程在释放锁之前可再次获得锁,即锁的可重入性。
锁的获得者应该能够正确释放已经获得的锁,并且当获得锁的进程宕机时,锁应该自动释放,从而使得其它竞争方可以获得该锁,从而避免出现死锁的状态。
当获得锁的一方释放锁时,其它对于锁的竞争方需要能够感知到锁的释放,并再次尝试获取锁。
总结:
总结:
A系统发送消息到mq,B系统从mq中获取消息进行处理。那A系统如何知道B系统的处理结果呢?通过zk就是可以分布式系统之前的协调工作,A系统发送完消息到mq中,就可以在zk上对某个节点的值设置一个监听器,一旦B系统将消息消费成功之后,就修改节点的值,A系统马上就知道B系统是否消费成功了。
例如: 订单中心创建订单的时候,需要进行库存的锁定。订单中心创建完订单,就可以向mq发送一条消息,告诉商品中心需要进行库存的锁定。订单中心在zk中创建一个带有orderId的node,并注册监听。商品中心在收到mq的消息,锁定库存成功之后,就修改对应的orderId的node的值,订单中心就知道商品中心的库存锁定成功了。
见11章
zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么?
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。
本文参考:
1.郭俊前辈的文章《实例详解ZooKeeper ZAB协议、分布式锁与领导选举》https://dbaplus.cn/news-141-1875-1.html
2.Github博主yanglbme 的advanced-java项目中的文章《Zookeeper 都有哪些应用场景?》 https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/zookeeper-application-scenarios.md