本文简单分析zookeeper的工作原理,对于如何使用zookeeper不是本文讨论的重点。本文有些基础概念转自网络或书籍。
源码分析基于zookeeper-3.6.0-SNAPSHOT.
ZooKeeper是一个分布式的、高性能的开源应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。
Zookeeper是hadoop的一个子项目,其发展历程无需赘述。在分布式应用中,由于工程师不能很好地使用锁机制,以及基于消息的协调机制不适合在某些应用中使用,因此需要有一种可靠的、可扩展的、分布式的、可配置的协调机制来统一系统的状态。Zookeeper的目的就在于此。
ZK特点如下:
最终一致性
client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。
可靠性
具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受。
实时性
Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
等待无关(wait-free)
慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
原子性
更新只能成功或者失败,没有中间状态。
顺序性
包括全局有序和偏序两种:
Zookeeper中的角色主要有以下三类,如下表所示:
注意:Leader应该是负责接收写请求然后进行广播再更新,Leader和Follower都可以接收读请求。流程可以点击这里
顺序一致性
注意,可以通过将集群配置的
leaderServers
属性设为no
来让leader不接受客户端连接,仅做协调工作。
本地数据库
进行数据备份负载,转发写事务请求给leader,
直接提供快速地读请求,可以和客户端部署在较劲位置,直接提供快速的本地数据读取服务
从leader接收更新事务投票结果,相当于是同步更新数据,但Observer不参与投票
拥有投票权的核心节点部署在同地稳定环境,Observer放置在其他环境,比如离客户端更近的网络位置,这样就算是Observer出问题也不影响投票等ZK核心服务。
因为Observer不会接收proposal并参与投票,Leader不会发送proposal给Observer。Leader发送给Follower的Commit消息只包含zxid,并没有proposal本身。所以,只发送Commit消息给Observer则不会让Observer得知已提交的proposal。这就是使用INFORM消息的原因,此消息本质上是一个包含了已被Commit的proposal的Commit消息。
简而言之,Follower会得到两个消息,而Observer只会得到一个。Follower通过广播得到proposal的内容,接下来获得一个简单Commit消息,此消息只包含了zxid。相反,Observer得到一个包含了已被Commit的proposal的INFORM消息。
可扩展
增减observer不会触发ZK集群选举。而如果是添加follower,则会因为其参加写请求投票过程,从而会对写请求的处理速度造成一定影响,从而造成ZK集群整体吞吐下降。而且Observer掉线也不会影响ZK正常服务。
数据中心桥接
如果直接用两个数据中心组成ZK集群(即一个集群的leader,follower散步在两个数据中心),可能会在复杂的、快速变化的网络延迟等复杂网络环境中出问题,比如投票、选举等,这个设计不可取。
正确做法是在数据中心A部署ZK选举权节点,数据中心B部署Observer作为数据同步和读写请求转发。
注意:Observer的使用并无法完全消除数据中心之间的网络延迟,因为Observer不得不把更新请求转发到另一个数据中心的Leader,并处理INFORM消息,网络速度极慢的话也会有影响,它的优势是为本地读请求提供快速响应。
发布订阅
连接到配置的ZK集群中的任意节点。
系统模型如图所示:
ZK集群内节点都必须能感知到其他节点,在内存中维护状态视图,以及持久化方式存储事务日志和快照。
只要半数以上节点还活着,就能继续提供服务。
客户端通过TCP连接到ZK中的某个节点,一旦失效就会重连到其他节点。
znode
前面提到过ZK是精简的文件系统,但没有文件和目录而是使用Znode。但需要注意的是,ZK设计目标是协调服务,所以每个节点数据容量阈值为1MB。它可以作为容器保存数据或其他znode。
znode还有一个与之关联的ACL(访问控制列表)。
命名空间
所有znode共同构成层次化的结构-即命名空间。
版本号
每次一个 znode 的数据发生改变,版本号随之递增。当一个客户端检索数据时,同时也收到数据对应的版本号。
生命周期
当创建瞬时节点的客户端session一直保持活动,瞬时节点就一直存在。
而当会话终结时,瞬时节点被删除。
不允许任何子节点
持久znode生命周期不依赖客户端session,只有当显示删除时才会被删除。
当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。当客户端与zookeeper服务的连接断开后,这个节点也不会被删除。
当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。
当创建这个节点的客户端与zookeeper服务的连接断开后,这个节点被删除
Znode 维护了一个属性结构,其中包含了表示数据改变、访问控制列表(ACL)改变的版本号、时间戳,可用于缓存校验、协调更新。
具体来说,ZooKeeper中每个ZNode的状态数据结构都是由下列字段组成
watch znode的客户端可以在znode改变时得到通知,但是只能触发1次。可以用curator来实现多次watch。
znode创建时就带有ACL列表,用于操作权限限定。
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。
Google 的粗粒度锁服务 Chubby 的设计开发者 Burrows 曾经说过:“所有一致性协议本质上要么是 Paxos 要么是其变体”。Paxos 虽然解决了分布式系统中,多个节点就某个值达成一致性的通信协议。但是还是引入了其他的问题。由于其每个节点,都可以提议提案,也可以批准提案。当有三个及以上的 proposer 在发送 prepare 请求后,很难有一个 proposer 收到半数以上的回复而不断地执行第一阶段的协议,在这种竞争下,会导致选举速度变慢。
所以 zookeeper 在 paxos 的基础上,提出了 ZAB 协议,本质上是,只有一台机器能提议提案(Proposer),而这台机器的名称称之为 Leader 角色。其他参与者扮演 Acceptor 角色。为了保证 Leader 的健壮性,引入了 Leader 选举机制。
ZAB协议还解决了这些问题
Zab协议有两种阶段,它们分别是恢复模式(选主)和广播模式(同步),都可以无限重复:
leader选举
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式结束。
原子广播
用于状态同步保证了leader和其他节点具有相同的系统状态。原子广播步骤如下:
注意,原子广播具有原子性,也就是说每次修改请求要么失败要么成功
ZAB 协议类似于两阶段提交来保证事务
/my/test=1
,Leader 会生成对应的事务提议(注意,当前 zxid=0x5000010, 提议的 zxid=Ox5000011)。set /my/test=1
写入本地事务日志dataLogDir,zxid=Ox5000011
应用到内存中。上面说的是正常的情况。有两种异常情况:
以下转自CAP 一致性协议及应用解析
在选出 Leader 之后,ZK就进入状态同步的过程。其实就是把最新的zxid
对应的日志数据,应用到其他的节点中。此 zxid 包含 follower 中写入日志但是未提交的 zxid ,称之为服务器提议缓存队列 committedLog 中的 zxid。
同步会完成三个 zxid 值的初始化。
peerLastZxid:该 learner 服务器最后处理的 zxid。
minCommittedLog:leader服务器提议缓存队列 committedLog 中的最小 zxid。
maxCommittedLog:leader服务器提议缓存队列 committedLog 中的最大 zxid。
系统会根据 learner 的peerLastZxid和 leader 的minCommittedLog及maxCommittedLog做出比较后做出不同的同步策略
直接差异化同步
场景:peerLastZxid介于minCommittedLogZxid和maxCommittedLogZxid间
此种场景出现在,上文提到过的,Leader 发出了同步请求,但是还没有 commit 就 down 了。 新选出的 leader 继续完成上一任 leader 未完成的工作,发送 Proposal 数据包和commit 指令数据包 。
例如此刻Leader提议的缓存队列为 0x20001,0x20002,0x20003,0x20004,此处learn的peerLastZxid为0x20002,Leader会将0x20003和0x20004两个提议同步给learner
先回滚再差异化同步/仅回滚同步
此种场景出现在,上文提到过的,Leader写入本地事务日志后,还没发出同步请求,就down了,然后在同步日志的时候作为learner出现。
例如即将要 down 掉的 leader 节点 1,已经处理了 0x20001,0x20002,在处理 0x20003 时还没发出提议就 down 了。后来节点 2 当选为新 leader,同步数据的时候,节点 1 又神奇复活。如果新 leader 还没有处理新事务,新 leader 的队列为,0x20001, 0x20002,那么会让节点 1 回滚到 0x20002 节点处,0x20003 日志废弃,称之为仅回滚同步。
如果新 leader 已经处理 0x30001 , 0x30002 事务,那么新 leader 此处队列为0x20001,0x20002,0x30001,0x30002,那么让节点 1 先回滚,到 0x20002 处,再差异化同步0x30001,0x30002。
全量同步
peerLastZxid小于minCommittedLogZxid或者leader上面没有缓存队列。leader直接使用SNAP命令进行全量同步
为了保证更新事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务,对更新排序,决定了分布式系统的执行顺序。即如果zxid 为z1的事务一定发生在z2(z1 所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字: znode节点有三种类型: 每个znode节点在工作过程中有四种状态: 当节点初始启动或当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的节点都恢复到一个正确的状态。一般来说,ZK选举仅需200ms! Zk的选举算法有两种: 最新版本3.4.x里面已经废弃了其他election方式,只保留了 在网上看到一篇文章,讲了fast选举主要思想,这里贴一下可以帮助理解: 全天下我最牛,在我没有发现比我牛的推荐人的情况下,我就一直推举我当leader。第一次投票那必须推举我自己当leader。 每当我接收到其它的被推举者,我都要回馈一个信息,表明我还是不是推举我自己。如果被推举者没我大(依次比较epoch->zxid->sid(即myid)),我就一直推举我当leader。 我有一个票箱, 和我属于同一轮的投票情况都在这个票箱里面。一人一票,重复的或者过期的票,我都不接受。 一旦我不再推举我自己了(这时我发现别人推举的人比我推荐的更牛),我就把我的票箱清空,重新发起一轮投票(这时我的票箱一定有两票了,都是选的我认为最牛的人)。 一旦我发现收到的推举信息中投票轮要高于我的投票轮,我也要清空我的票箱。并且还是投当初我觉得最牛的那个人(除非当前收到的投票中推荐的人比我最初的推荐牛,我就顺带更新我的推荐)。 不断的重复上面的过程,不断的告诉别人“我的投票是第几轮”、“我推举的人是谁”。直到我的票箱中“我推举的最牛的人”收到了不少于 N /2 + 1的推举投票。 这时我就可以决定我是follower还是leader了(如果至始至终都是我最牛,那我就是leader咯,其它情况就是follower咯)。并且不论随后收到谁的投票,都向它直接反馈“我的结果”。 下面可以分析下其源码,主要是 前置类: QuorumPeer 而且该类是一个线程,其run方法内有一个循环, QuorumCnxManager FastLeaderElection.Messenger.WorkerSender FastLeaderElection.Messenger.WorkerReceiver 重要变量: LOOKING模式,这种情况我们就要和自己的投票信息进行综合比对: OBSERVING模式 FOLLOWING或者LEADING内部逻辑一致,此时我们需要判断这些确定了状态的机器发送来的消息到底合法不,合法才认同: 描述Leader选择过程中的状态变化,这是假设全部实例中均没有数据,假设服务器启动顺序分别为:A,B,C。 目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: 注意:这里可以判断过半,是因为每个ZK的 ZK并非强一致性的,而只是实现了顺序一致性。 每个更新操作要么成功,要么失败。也就是说不会有客户端看到更新了一般或更新失败的结果。 且更新一旦成功,就会持久存在而不会受到某个节点故障的影响。 来自某个客户端的多个更新,会按发送顺序被提交。即某个客户端将znode z修改为a,然后又修改为b,则其他客户端无法在看到z 为b后又变回为a。 ZK的顺序一致性是由Zab协议依赖TCP(zxid小的更新事务一定发生在zxid大的之前)来保证的,而不是paxos算法。 不同客户端非强一致性 原因是zk 写入是必须通过 leader 串行的写入,而且只要一半以上的节点写入成功即可。而任何节点都可提供读取服务。例如:zk,有 同一客户端的一致性保证 在分布式场景下,一般是读多写少,此时使用ZK性能最佳。以下的图表展示了客户端请求中读请求百分比变化时,ZK QPS的变动情况: 可配置参数 snapCount,设置两次ZK数据快照之间的事务操作个数,zk 节点记录完事务日志时,会统计判断是否需要做数据快照(距离上次快照,事务操作次数等于snapCount/2~snapCount 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢 默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。 要在更新时获得较低的延迟,确保ZK有专用的事务日志目录,即单独设定dataLogDir,不要和dataDir相同。 leader关闭客户端连接开关,仅作为投票发起和协调 前文提到过,ZK包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。 利用临时节点,在某个目录下创建,并对该父目录进行watch,那么有服务上下线的时候便会对关注者进行通知。 利用临时顺序节点实现,最小者为锁(即父znode)拥有者。 也是临时顺序节点,最小者为master。 服务发布者在ZK创建节点,并附带服务连接信息。 服务调用者通过服务名就能在ZK找到关注的服务,然后通过其上的信息进行服务调用。 有赞-Zookeeper原理 ZooKeeper (一)概览,中文版 Zookeeper核心原理-源码分析选举的过程 ZooKeeper伸缩性 Zookeeper 原理与优化 ZooKeeper Observers 《Hadoop权威指南》 Apache-ZooKeeper zookeeper原理 zookeeper之数据模型 理解zookeeper选举机制 zookeeper选举机制(参考官方文档和源码) zookeeper之数据模型 zookeeper核心原理(选举) ZooKeeper增加Observer部署模式提高性能 CAP 一致性协议及应用解析
高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。
用于递增计数,表示该事务在当前选择周期内的递增次序(leader 每处理一个事务请求,该值加 1,发生一次 leader 选择,低 32 位要清 0)2.3 节点类型和工作状态
2.3.1 节点类型
leader(主节点)
follower(从节点)
observer(观察节点,与follower区别:observer不参加选举)2.3.2 节点工作状态
2.4 选举流程
2.4.1 选举时机
2.4.2 选举中的相关概念
zxid的高32位。
事务ID
就是ZK节点配置的myid
2.4.3 FastLeaderElection
FastLeaderElection
方式。2.4.3.1 选举思想
2.4.3.2 源码
org.apache.zookeeper.server.quorum.FastLeaderElection
。
该类用来管理Quorum(法定人数)协议。该类会设置一个数据报socket,将会始终在响应中反应其当前leader的视图,该响应包括:
使用TCP协议来在节点之间通信实现选举。
该类发送sendqueue中的投票信息
该类负责收投票信息,并会放到recvqueue内// 选举用的逻辑时钟epoch,election epoch
AtomicLong logicalclock = new AtomicLong()
// 提议的leader
long proposedLeader;
// 提议的zxid(事务id)
long proposedZxid;
// 提议的leader的epoch
long proposedEpoch;
// 投票发送队列
LinkedBlockingQueue<ToSend> sendqueue;
// 投票接收队列
LinkedBlockingQueue<Notification> recvcqueue;
// 记录投票的情况,key为投票机的sid,value为其投票信息
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
// 本节点的投票箱
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
// 提议的leader的节点版本号
n.version
// 提议的leader的sid
n.leader
// 提议的leader的epoch
n.peerEpoch
// 提议的leader的zxid
n.zxid
// 提议者的投票epoch
n.electionEpoch
// 提议者所处状态,LO,LE,F,O
n.state
// 提议者的sid
n.sid
/** * 开启一轮新leader选举,每当达到法定人数个节点修改状态为LOOKING,该方法被调用 * 而且会向其他所有节点。 */
public Vote lookForLeader() throws InterruptedException {
try {
self.jmxLeaderElectionBean = new LeaderElectionBean();
MBeanRegistry.getInstance().register(
self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
self.jmxLeaderElectionBean = null;
}
if (self.start_fle == 0) {
self.start_fle = Time.currentElapsedTime();
}
try {
// 收到的票箱
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
// 本节点的投票箱
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = minNotificationInterval;
synchronized(this){
// 逻辑时钟原子自增
logicalclock.incrementAndGet();
// 初始时都给自己投票
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 将我方投票发送给其他节点
sendNotifications();
SyncedLearnerTracker voteSet;
// 循环和其他节点之间互相发送投票通知,直到选出一个leader。
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
// 从接收投票箱内获取投票,带超时时间
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
/* * Sends more notifications if haven't received enough. * Otherwise processes new notification. */
// 超过等待时间仍没有收到足够投票,就发送更多本节点的投票
// 否则处理收到的投票
if(n == null){
// 未收到投票信息,继续发送,直到选出leader
if(manager.haveDelivered()){
// 所有消息队列已经投送,就继续发送投票变更给所有节点
sendNotifications();
} else {
// 否则尝试与所有节点建立连接,为继续发送投票做准备
manager.connectAll();
}
/* * Exponential backoff */
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
// 检查给定的sid(myid)是否在当前或下一个投票视图中表示
else if (validVoter(n.sid) && validVoter(n.leader)) {
/* * Only proceed if the vote comes from a replica in the current or next * voting view for a replica in the current or next voting view. */
// 仅当投票来自当前或下一个投票视图中副本的当前或下一个投票视图中的副本时才会继续。
switch (n.state) {
case LOOKING:
// 收票状态为LOOKING
if (getInitLastLoggedZxid() == -1) {
// 我方zxid为-1,非法,忽略
break;
}
if (n.zxid == -1) {
// 收到的投票为zxid为-1,非法,忽略
break;
}
if (n.electionEpoch > logicalclock.get()) {
// 收到的投票的epoch大于当前的逻辑时钟
// 此时就更新逻辑时钟为收到的投票epoch
logicalclock.set(n.electionEpoch);
// 还要清空收票箱,因为认为他们都过期了
recvset.clear();
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
// 以下情况为true
// 1.收票提议leaderEpoch高于当前epoch
// 2.epoch相等但是提议leaderZxid更大
// 3.epoch,zxid相等,但收票提议leader sid更大
// 此时就更新我方提议状态为收票提议
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
// 否则以我方提议更新
// leader,zxid,epoch
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
// 发送更新后的提议
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
// 收票epoch比我方epoch还小,忽略之
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {
// 收票epoch等于我方epoch
// 且符合更新条件就更新我方提议;否则不更新,坚持之前的
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 并发送最新提议
sendNotifications();
}
// don't care about the version if it's in LOOKING state
// 以收票节点的sid作为key,value为其提议内容
// 放入收票箱,记录这次收到的投票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
voteSet = getVoteTracker(
recvset, new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch));
if (voteSet.hasAllQuorums()) {
// 已经收到所有节点投票
// 再次确认是否还有新的投票,leader是否发生变更
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
// 新收到的提议满足条件,放回recvqueue
// 然后重走最外层的循环逻辑
// 否则继续当前while重新收票
recvqueue.put(n);
break;
}
}
// 这就代表这没有再收到任何新的消息
if (n == null) {
// 此时就更新peer状态
// proposedLeader为我方sid,peerState为LEADING
// proposedLeader不为我方sid,
// peerState为FOLLOWING或OBSERVING
setPeerState(proposedLeader, voteSet);
// 构建最终的vote
Vote endVote = new Vote(proposedLeader,
proposedZxid, logicalclock.get(),
proposedEpoch);
// 清空收票箱
leaveInstance(endVote);
// 返回投票结果
return endVote;
}
// 否则n!=null,继续循环
}
break;
case OBSERVING:
// 如果收票状态为观察者模式什么也不做
break;
case FOLLOWING:
// 如果收票状态为跟随者模式和领导者模式处理逻辑相同
case LEADING:
// 如果收票状态为领导者模式
/* * Consider all notifications from the same epoch * together. */
if(n.electionEpoch == logicalclock.get()){
// 收票epoch和我方投票epoch相同
// 就放入
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
voteSet = getVoteTracker(recvset, new Vote(n.version,
n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() &&
checkLeader(outofelection, n.leader, n.electionEpoch)) {
setPeerState(n.leader, voteSet);
Vote endVote = new Vote(n.leader,
n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
/* * Before joining an established ensemble, verify that * a majority are following the same leader. */
outofelection.put(n.sid, new Vote(n.version, n.leader,
n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(outofelection, new Vote(n.version,
n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() &&
checkLeader(outofelection, n.leader, n.electionEpoch)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
setPeerState(n.leader, voteSet);
}
Vote endVote = new Vote(n.leader, n.zxid,
n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
LOG.warn("Notification state unrecoginized: " + n.state
+ " (n.state), " + n.sid + " (n.sid)");
break;
}
} else {
if (!validVoter(n.leader)) {
LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
}
if (!validVoter(n.sid)) {
LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
}
}
}
return null;
} finally {
try {
if(self.jmxLeaderElectionBean != null){
MBeanRegistry.getInstance().unregister(
self.jmxLeaderElectionBean);
}
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
self.jmxLeaderElectionBean = null;
LOG.debug("Number of connection processing threads: {}",
manager.getConnectionThreadCount());
}
}
2.4.3.3 源码小结
节点启动,从持久化的文件内读取本节点zxid,然后设初始peerState为LOOKING,随后启动拥有阻塞队列recvcqueue的用于接收其他节点投票的WorkerReceiver线程,以及拥有阻塞队列sendqueue的用来发送本节点选举结果的WorkerSender线程。
WorkerReceiver会循环从QuorumCnxManager的recvQueue中拉取信息,放入FastLeaderElection的recvQueue中。
QuorumPeer线程有个循环,每次会在peer状态为LOOKING
时调用lookForLeader
方法
lookForLeader方法默认为fast选举模式。首先会将当前逻辑时钟自增,然后更新提议会初始提议,并发送第一次投票给其他节点。接下来是while循环,条件是当前peer状态为LOOKING模式。
什么也不做,继续循环
2.4.3.4 选举流程图如下:
2.4.3.5 选举状态图
2.4.3.6 选举示例
zoo.cfg
文件中已经有了类似以下配置:server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
2.5 读写请求
2.5.1 写请求
2.5.2 读请求
所有节点都能提供读请求服务,且更新内存中znode树前所有节点都会先将更新持久化到磁盘,但读请求只读内存,所以速度很快。0x03 一致性
3.1 概述
3.2 原子性和持久性
3.3 顺序一致性
3.4 非强一致性
不同客户端连接ZK集群,可能看到不一致视图,因为并不是强一致性的。1~5
个节点,写入了一个最新的数据,最新数据写入到节点 1~3
,会返回成功。然后读取请求过来要读取最新的节点数据,请求可能被分配到节点 4~5
。而此时最新数据还没有同步到节点4~5
。会读取不到最近的数据。如果想要读取到最新的数据,可以在读取前使用 sync
命令。
一个客户端连上ZK集群后,如果因为该节点连接出错导致客户端连接其他节点,则无法连接状态滞后于当前ZK节点的其他节点。也就是说,同一客户端不会出现先看到新值,重新连接后又看到旧值的情况0x04 性能
0x05 调优
5.1 dataDir和dataLogDir
dataDir是用来存储内存数据库快照的位置,数据快照用来记录 zk 服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中
而dataLogDir目录下的日志文件用来记录所有事务操作,可通过 dataLogDir 配置文件目录,文件是以写入的第一条事务 zxid 为后缀,方便后续的定位查找。zk 会采取“磁盘空间预分配”的策略,来避免磁盘 Seek 频率,提升 zk 服务器对事务请求的影响能力。
5.2 增加Observer提升读写性能
5.3 leader调优
0x06 应用
6.1 概述
6.2 配置中心
比如所有APP1配置放到路径znode /conf
下,客户端用zk.exist("/conf",true)
对该节点下的数据进行监控,那么配置发生变化时就能感知到。6.3 集群管理
6.4 分布式锁
6.5 master选举
6.6 服务中心
0xFF 参考文档
1 好文推荐
此文从入口开始分析源码
此文分析引入Observer原因和ZK伸缩性关系
见过的ZK最全实战文档,都可以写书了。本文有几张图是直接用的他的。2 参考文档