1.概述
前面一章分析了集群启动阶段的选举过程,运行中重新开始选举和启动时的选举流程也是一致的.
一旦完成选举,通过执行QuorumPeer
的setPeerState
将设置好选举结束后自身的状态。然后,将再次执行QuorumPeer
的run的
新的一轮循环,
QuorumPeer
的run
的每一轮循环,先判断自身当前状态:
(1). 自身为LOOKING
则需在本轮循环开启选举,并完成选举。
(2). 自身为FOLLOWING
protected Follower makeFollower(FileTxnSnapLog logFactory) throws IOException {
return new Follower(this, new FollowerZooKeeperServer(logFactory, this, this.zkDb));
}
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
follower.shutdown();
setFollower(null);
updateServerState();
}
则应转换为从节点,先通过follower.followLeader();
与主同步,再履行起从节点的角色任务。
(3). 自身为LEADING
protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException, X509Exception {
return new Leader(this, new LeaderZooKeeperServer(logFactory, this, this.zkDb));
}
try {
setLeader(makeLeader(logFactory));
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
updateServerState();
}
则应转换为主节点,先通过leader.lead();
完成集群同步后,再履行起主节点的角色任务。
本部分讨论集群同步过程。集群同步是一个完成选举的主和从相互协作最终大家达成一致的过程。
2.集群同步过程最初的两次同步
2.1.利用同步确定新集群的轮次
2.1.1.主节点的leader.lead()
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
self.tick.set(0);
zk.loadData();
leaderStateSummary = new StateSummary(self.getCurrentEpoch(), zk.getLastProcessedZxid());
cnxAcceptor = new LearnerCnxAcceptor();
cnxAcceptor.start();
上述动作里,先是设置ZabState
为DISCOVERY
。选举尚未结束时ZabState
是ELECTION
来着。
其中zk.loadData();
由于在启动节点选举前已经执行过了一次基于快照+redo
的数据实体恢复,所以这里啥也不用做。
构建一个StateSummary
实例,这个实例反映了节点数据实体对应的轮次,lastZxid
信息.
通过cnxAcceptor.start();
使得主节点开始作为一个服务端,允许其他集群成员来连接以便执行集群同步及后续的请求处理。
对每个接入的集群成员,在主节点方面将通过accept
得到通信套接字,并分配一个LearnerHandler
来维护和集群成员的通信。
LearnerHandler fh = new LearnerHandler(socket, is, Leader.this);
fh.start();
fh.start();
将开启一个线程,在此线程中将接收来自连接对端的包,并对其执行处理。
接下来,主节点执行的是:
long epoch = getEpochToPropose(self.getId(), self.getAcceptedEpoch());
主节点执行getEpochToPropose
的目的是为了获得新产生的集群的轮次要设置为何值?
此处将产生同步等待,直到算上主节点自身,有半数以上集群成员连接到的主节点并执行了getEpochToPropose
才能获得继续。继续时,将主节点的acceptedEpoch
设置为所有执行getEpochToPropose
的节点中acceptedEpoch
最大值+1
.
对主节点,此处的同步等待超时下,将引发主节点停止。并设置自身状态为LOOKING
。这样,在QuorumPeer
的run
的新一轮循环里,将重新开始选举过程。
2.1.2.从节点的follower.followLeader()
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
QuorumServer leaderServer = findLeader();
connectToLeader(leaderServer.addr, leaderServer.hostname);
connectionTime = System.currentTimeMillis();
上述动作里,先是设置ZabState
为DISCOVERY
。选举过程中ZabState
是ELECTION
来着。
然后,执行findLeader
找到主节点。借助自身的投票和集群全局配置很容易定位出来。
connectToLeader
将同步方式发起到服务端连接。
若指定时间或指定次数内连接未建立,将引发从节点停止。并设置自身状态为LOOKING
。这样,在QuorumPeer
的run
的新一轮循环里,将重新开始选举过程。若连接成功,在异步发包下会启动一个线程用于异步发包。
从节点接下来执行:
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
这里面从节点先向主节点发一个包
long lastLoggedZxid = self.getLastLoggedZxid();
QuorumPacket qp = new QuorumPacket();
qp.setType(Leader.FOLLOWERINFO);// 包类型
qp.setZxid(ZxidUtils.makeZxid(self.getAcceptedEpoch(), 0));// 包里的zxid
LearnerInfo li = new LearnerInfo(self.getId(), 0x10000, self.getQuorumVerifier().getVersion());
ByteArrayOutputStream bsid = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(bsid);
boa.writeRecord(li, "LearnerInfo");
qp.setData(bsid.toByteArray());// 包的数据体是一个LearnerInfo实例。包含从节点sid,0x10000,集群全局信息。
writePacket(qp, true);
现在从节点等待主节点的回复。
2.1.3.主节点实现从节点接入及收包处理
主节点会为每个接入到其的从节点,分配一个LearnerHandler
实例,此实例将占据一个独立的线程来执行对应从节点的数据包收取和处理。
LeaderHandler
的run
一开始执行:
learnerMaster.addLearnerHandler(this);
这样将向leader
的learners
集合中加入此LearnerHandler
实例。
然后将开始收取首个包。并对首个包进行合法性检测(对从节点首个包类型必须是Leader.FOLLOWERINFO
)。
若检测失败,服务端方面会停止线程,关闭被动连接,从leader
的learners,forwardingFollowers,observingLearners
集合移除此LearnerHandler
实例。客户端方面则将引发从节点停止,并设置自身状态为LOOKING
。这样,在QuorumPeer
的run
的新一轮循环里,将重新开始选举过程。后续分析过程失败过程处理类似。
检测通过后,对首个包进行解析
QuorumPacket qp = new QuorumPacket();
// 反向序列化获得包
ia.readRecord(qp, "packet");
byte[] learnerInfoData = qp.getData();// 获得包中数据体
ByteBuffer bbsid = ByteBuffer.wrap(learnerInfoData);
this.sid = bbsid.getLong();
this.version = bbsid.getInt(); // protocolVersion--0x10000
String followerInfo = learnerMaster.getPeerInfo(this.sid);
// 这样将获得从节点首个包里的zxid,从中提取出epoch
long lastAcceptedEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
long peerLastZxid;
StateSummary ss = null;
long zxid = qp.getZxid();
long newEpoch = learnerMaster.getEpochToPropose(this.getSid(), lastAcceptedEpoch);
2.1.4. 集群同步阶段确定新集群的epoch
通过2.1.1
到2.1.3
我们知道。
选举结束后,主节点作为服务端允许集群从节点的接入。
然后,集群主节点将阻塞等待。
对每个结束选举的从节点,将连接主节点,成功后,发出首个Leader.FOLLOWERINFO
包。然后等待回复。
主节点为每个接入的从节点分配LearnerHandler
实例,并启动线程,处理收包和收包处理。
每个LearnerHandler
的线程收取首个包Leader.FOLLOWERINFO
后,通过learnerMaster.getEpochToPropose
陷入与主节点一样的阻塞等待。
当算上主节点自身有半数以上成员陷入上述阻塞等待后,将基于所有等待成员中acceptedEpoch
最大值+1
作为新集群的epoch
设置到leader
的epoch
。若上述过程某个成员连接上出现错误或等待超时,则将执行连接断开。此成员将停止其角色。重新变为LOOKING
状态,重新进入选举过程。主节点自己等待超时或出错,则主节点停止作为主,重新变为LOOKING
状态,并进入选举流程。主节点停止时会引发每个连到主节点的从节点也停止,并重新变为LOOKING
状态。
2.2.利用同步再次确认选出的主是所有参与选举成员中状态最靠前的
我们分别针对主节点,从节点分析完成getEpochToPropose
等待后的处理流程
2.2.1.对主节点
zk.setZxid(ZxidUtils.makeZxid(epoch, 0));
synchronized (this) {
lastProposed = zk.getZxid();
}
newLeaderProposal.packet = new QuorumPacket(NEWLEADER, zk.getZxid(), null, null);
newLeaderProposal.addQuorumVerifier(self.getQuorumVerifier());
waitForEpochAck(self.getId(), leaderStateSummary);
主节点,后续执行waitForEpochAck
。
主节点执行此步骤会阻塞,在waitForEpochAck
中收集到半数以上的成员及其StateSummary
后,且满足:
(1). 完成收集未引发超时
(2). 不存在从节点StateSummary
领先主节点的情况。
上述两个条件,主节点将继续。并设置electionFinished
为true
。
若某个条件不满足,主节点将停止,并断开与连到其的从的连接,这样将引发从节点也停止。最终主,从均停止。并发起下一轮选举。
2.2.2.对从节点
分析LeaderHandler
的run
中后续动作
long newLeaderZxid = ZxidUtils.makeZxid(newEpoch, 0);
byte[] ver = new byte[4];
ByteBuffer.wrap(ver).putInt(0x10000);
QuorumPacket newEpochPacket = new QuorumPacket(Leader.LEADERINFO, newLeaderZxid, ver, null);
oa.writeRecord(newEpochPacket, "packet");
messageTracker.trackSent(Leader.LEADERINFO);
bufferedOutput.flush();// 立即发送
QuorumPacket ackEpochPacket = new QuorumPacket();
ia.readRecord(ackEpochPacket, "packet");
即给从节点回复一个Leader.LEADERINFO
包,其中包含基于新集群epoch
得到的zxid
,版本信息(0x10000
)。
从节点这边:
readPacket(qp);
final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());// 取得回复包里的zxid
leaderProtocolVersion = ByteBuffer.wrap(qp.getData()).getInt();// 取得ver
byte[] epochBytes = new byte[4];
final ByteBuffer wrappedEpochBytes = ByteBuffer.wrap(epochBytes);
wrappedEpochBytes.putInt((int) self.getCurrentEpoch());// 放入自身currentEpoch
self.setAcceptedEpoch(newEpoch);// 用回复包里zxid导出的epoch设置自身acceptedEpoch
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
writePacket(ackNewEpoch, true);// 向主节点发包
return ZxidUtils.makeZxid(newEpoch, 0);
即首先采用新集群的epoch
设置自身的acceptedEpoch
,再构造一个反映自身数据实体轮次,lastZxid
的Leader.ACKEPOCH
回复包执行回复.
继续分析LeaderHandler
的run
中
QuorumPacket ackEpochPacket = new QuorumPacket();
ia.readRecord(ackEpochPacket, "packet");// 等待从节点的回复
messageTracker.trackReceived(ackEpochPacket.getType());
ByteBuffer bbepoch = ByteBuffer.wrap(ackEpochPacket.getData());//
ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());// 从节点的currentEpoch,从节点的zxid
learnerMaster.waitForEpochAck(this.getSid(), ss);
waitForEpochAck
这个执行中将陷入等待。直到满足:
(1). 算上主自身有半数以上成员均针对主节点执行了waitForEpochAck
。
(2). 收集到半数以上waitForEpochAck
,未引发等待超时。
(3). 未出现某个从节点的ss
领先于主节点的ss
。
上述三者满足时,等待者和主节点继续运行。
若LearnerHandler
中执行waitForEpochAck
超时,将引发对应的从节点停止,并重新进入下一轮选举。
主节点完成waitForEpochAck
这一同步后,将electionFinished
设置为true
。
到目前为止,我们分析了成员完成选举确认自身身份后,经历过了两个由主节点这边主导的阻塞等待。
第一个阻塞等待是getEpochToPropose
,通过此等待基于所有连主成员中acceptedEpoch
最大值+1
作为新集群epoch
。
此阶段,对从节点等待超时或出错,会使得其停止作为从,并重新进入选举。
此节点,对主节点等待超时或出错,会使得其自身和所有连到其的从均停止自身角色,并重新进入选举。
成功则,使得我们获得新集群的epoch
,及基于epoch
的zxid
。
第二个阻塞等待是waitForEpochAck
,通过此等待主要是再一次确认主节点的合法性。即主节点自身的进度确实是参与选举的各个节点中最靠前的。
比如一个1,2,3
三个成员的集群。
(1). 1,2,3
启动,并运行很长时间。
(2). 全部停止。
(3). 手动删除1,2
的快照,redo
。
(4). 启动1,2
。使得1,2
各自完成选举,预期2
为主,1
为从。
(5). 启动3
,假设此时1,2
各自处于waitForEpochAck
。3
选举结束,依据收到的Notification
确定2
为主。并连接2
,也进入waitForEpochAck
。此时就会产生某个从节点的状态领先主节点的情况。这时,就要使得主和从全部停止角色,并再次选举。再次选举,将选择3
为主。此时,waitForEpochAck
将获得通过。一旦主和从的waitForEpochAck
结束。此时即使有新的从连到主,且领先于主,也不会被考虑了。
下面,将进入真正的集群同步阶段。
3. 真正的集群同步
我们继续分别从主节点,从节点角度分析其后续流程。
3.1. 主节点
self.setCurrentEpoch(epoch);
self.setLeaderAddressAndId(self.getQuorumAddress(), self.getId());
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
waitForNewLeaderAck(self.getId(), zk.getZxid());
只有完成前述两个阶段的同步,且未出错。主节点才将新集群的epoch
设置到其currentEpoch
中。
注意,对主节点和从节点,只要其完成了前述中第一个同步,就会各自设置自己的acceptedEpoch
。
所以,accepted
反映的是集群完成选举且形成新的集群轮次的次数。(一个完成选举,形成集群轮次的集群并不一定可以成为对外服务的集群。必须继续完成前述第二个同步,并完成这里要讨论的实际的集群同步才可以。)
主节点执行waitForNewLeaderAck
是我们遇到的新集群形成中主节点主导的第三次同步。
为了理解这个同步的意义,我们需要先分析,从节点方面在第二个同步完成后的后续动作.
3.2.从节点
先分析LearnerHandler
的run
中后续处理
这部分处理可以描述为:
(1). 从节点与主节点数据实体同步.
这一步是同步的关键所在,按从节点中数据实体lastZxid
和主节点中数据lastZxid
的情况.
有三种同步类型:
a. 快照同步
b. 差异化同步
c. 截断同步
我们下面分别对其分析.
对每种同步类型,我们分别分析此类型下的同步模式,然后给出一种会采用此种同步类型的实际场景.
a. 快照同步
a.1.同步模式
先发送一个new QuorumPacket(Leader.SNAP, zxidToSend, null, null)
这样的包,其中zxidToSend
是主节点数据实体的lastZxid
.
在此包的数据体中发送主节点数据实体序列化后的内容.
a.2.何时采用
若从节点数据实体的lastZxid
落后于主节点数据实体的lastZxid
,且落后的较多时,采用快照同步.
b.差异同步
b.1.同步模式
先发送一个new QuorumPacket(Leader.DIFF, lastZxid, null, null);
这样的包,其中lastZxid
是主节点数据实体的lastZxid
.
然后针对主节点领先于从节点的每个redo
项,先发送一个此redo
项对应的QuorumPacket
,再发送一个new QuorumPacket(Leader.COMMIT, packetZxid, null, null);
b.2.何时采用
若从节点数据实体的lastZxid
落后于主节点数据实体的lastZxid
,但落后的不多时,采用差异同步.
c.截断同步
c.1.同步模式
发送一个new QuorumPacket(Leader.TRUNC, lastZxid, null, null);
这样的包,其中lastZxid
是主节点数据实体的lastZxid
.
c.2.何时采用
若从节点数据实体的lastZxid
领先于主节点数据实体的lastZxid
,采用截断同步.
会出现此场景的一个实例:
假设1,2,3
是集群三个节点.
时刻一,1,2,3
分别启动构成集群并处理10
个请求.
时刻二,1,2,3
分别停止.
时刻三,手动清理1,2
.
时刻四,启动1,2
构成集群并处理4
个请求.
时刻五,3
启动,并加入集群,在3
的集群同步阶段即触发此场景.
(2). 将主节点中已经提交但尚未落入数据实体的各个请求,按发送一个此请求对应的QuorumPacket
,再发送一个new QuorumPacket(Leader.COMMIT, packetZxid, null, null);
的方式实现请求在从节点上的提交.
(3). 将主节点中提议但尚未提交的请求中领先于从节点的提议发送给从节点.
(3). 给从节点发送Leader.NEWLEADER
包.
然后执行:
qp = new QuorumPacket();
ia.readRecord(qp, "packet");
learnerMaster.waitForNewLeaderAck(getSid(), qp.getZxid());
我们接着分析从节点对收到的这些包的处理。
3.3.从节点对集群同步包的处理
从节点在registerWithLeader
后执行的流程为:
// 取得新集群epoch
long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);
if (newEpoch < self.getAcceptedEpoch()) {
throw new IOException("Error: Epoch of leader is lower");
}
long startTime = Time.currentElapsedTime();
self.setLeaderAddressAndId(leaderServer.addr, leaderServer.getId());
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
syncWithLeader(newEpochZxid);
self.setZabState(QuorumPeer.ZabState.BROADCAST);
从节点点在syncWithLeader
里,会具体处理来自主节点的用于集群同步的包,实现和主节点的同步.
类似前面分析过程,我们以下分别分析采用DIFF,TRUNC,SNAP
三种同步方式下,从节点在syncWithLeader
中所作的针对性处理.
(1). DIFF
同步
首先DIFF
下,主节点给从节点发的包按按顺序分为以下几类:
a. 首个DIFF
包
b. 后续若干和DIFF
配套的PROPOSAL+COMMIT
c. 后续若干主节点中已经提交但尚未落地到数据实体的PROPOSAL+COMMIT
d. 主节点中处于提议阶段的若干PROPOSAL
e. 标志同步最后一个包的NEWLEADER
从节点的处理策略是:
a. 用packetsCommitted
收集上述b,c
类型里的每个PROPOSAL
包
b. 用packetsNotCommitted
收集上述b,c,d
类型里的每个PROPOSAL
包
这里值得注意的是b,c
类型的包,既出现在packetsCommitted
容器,又出现在packetsNotCommitted
容器.
c. 针对NEWLEADER
包的处理为:
self.setCurrentEpoch(newEpoch);// 只有同步结束,才能设置自身currentEpoch
writeToTxnLog = true;
isPreZAB1_0 = false;
sock.setSoTimeout(self.tickTime * self.syncLimit);
self.setSyncMode(QuorumPeer.SyncMode.NONE);
zk.startupWithoutServing();
if (zk instanceof FollowerZooKeeperServer) {
FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk;
for (PacketInFlight p : packetsNotCommitted) {
fzk.logRequest(p.hdr, p.rec, p.digest);
}
packetsNotCommitted.clear();
}
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
主要的处理是将packetsNotCommitted
中的每个包执行持久化到日志文件的处理,且给主节点发送Leader.ACK
.
(2). TRUNC
同步
首先TRUNC
下,主节点给从节点发的包按按顺序分为以下几类:
a. 首个TRUNC
包
b. 后续若干主节点中已经提交但尚未落地到数据实体的PROPOSAL+COMMIT
c. 主节点中处于提议阶段的若干PROPOSAL
d. 标志同步最后一个包的NEWLEADER
从节点的处理策略是:
a. 针对TRUNC
包的处理为:
按包中lastZxid
,截断自身redo
日志,使得自身领先部分被移除.
重新基于redo
和快照加载数据实体,并设置其lastZxid
.这样就实现了数据实体的同步效果.
b. 针对上述b
类型的PROPOSAL+COMMIT
每对包处理策略是:
对PROPOSAL
,将其放入packetsNotCommitted
对COMMIT
,将其从packetsNotCommitted
移除,并直接在数据实体上迭代这个包.
c. 针对上述c
类型的PROPOSAL
包处理策略是:
对PROPOSAL
,将其放入packetsNotCommitted
d. 针对d
中NEWLEADER
包处理策略是:
zk.takeSnapshot(syncSnapshot);
self.setCurrentEpoch(newEpoch);
writeToTxnLog = true;
isPreZAB1_0 = false;
sock.setSoTimeout(self.tickTime * self.syncLimit);
self.setSyncMode(QuorumPeer.SyncMode.NONE);
zk.startupWithoutServing();
if (zk instanceof FollowerZooKeeperServer) {
FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk;
for (PacketInFlight p : packetsNotCommitted) {
fzk.logRequest(p.hdr, p.rec, p.digest);
}
packetsNotCommitted.clear();
}
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
主要的处理为:为数据实体生成快照来实现已经处理部分持久化存储,将提议部分序列化到日志文件,给主节点发送ACK
.
(3). SNAP
同步
首先SNAP
下,主节点给从节点发的包按按顺序分为以下几类:
a. 首个SNAP
包
b. 后续若干主节点中已经提交但尚未落地到数据实体的PROPOSAL+COMMIT
c. 主节点中处于提议阶段的若干PROPOSAL
d. 标志同步最后一个包的NEWLEADER
从节点的处理策略是:
a. 针对SNAP
包的处理为:
对包的数据部分执行反向序列化来重新构建自身的数据实体,从而达到同步效果.
b. 针对上述b
类型的PROPOSAL+COMMIT
每对包处理策略是:
对PROPOSAL
,将其放入packetsNotCommitted
对COMMIT
,将其从packetsNotCommitted
移除,并直接在数据实体上迭代这个包.
c. 针对上述c
类型的PROPOSAL
包处理策略是:
对PROPOSAL
,将其放入packetsNotCommitted
d. 针对d
中NEWLEADER
包处理策略是:
zk.takeSnapshot(syncSnapshot);
self.setCurrentEpoch(newEpoch);
writeToTxnLog = true;
isPreZAB1_0 = false;
sock.setSoTimeout(self.tickTime * self.syncLimit);
self.setSyncMode(QuorumPeer.SyncMode.NONE);
zk.startupWithoutServing();
if (zk instanceof FollowerZooKeeperServer) {
FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk;
for (PacketInFlight p : packetsNotCommitted) {
fzk.logRequest(p.hdr, p.rec, p.digest);
}
packetsNotCommitted.clear();
}
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
主要的处理为:为数据实体生成快照来实现已经处理部分持久化存储,将提议部分序列化到日志文件,给主节点发送ACK
.
3.4.主节点这边收到Leader.ACK
时处理
即LearnerHandler
的run
中同步后的处理:
qp = new QuorumPacket();
ia.readRecord(qp, "packet");
LOG.debug("Received NEWLEADER-ACK message from {}", sid);
learnerMaster.waitForNewLeaderAck(getSid(), qp.getZxid());
sock.setSoTimeout(learnerMaster.syncTimeout());
learnerMaster.waitForStartup();
queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));
即,先通过waitForNewLeaderAck
进入主节点主导的第三次同步.
当算上主节点自身,有半数以上成员执行了learnerMaster.waitForNewLeaderAck
时,表示半数以上成员已经达成集群同步.
这意味着,此时集群才算正式启动,可以启动服务端面向外部客户提供接入和请求处理服务.
上述在等待主节点的面向外部客户的服务端启动后,给从节点发送Leader.UPTODATE
包.
3.5.从节点收到Leader.UPTODATE
包的处理
self.setZooKeeperServer(zk);
self.adminServer.setZooKeeperServer(zk);
ack.setZxid(ZxidUtils.makeZxid(newEpoch, 0));
writePacket(ack, true);
zk.startServing();
self.updateElectionVote(newEpoch);
if (zk instanceof FollowerZooKeeperServer) {
FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk;
for (PacketInFlight p : packetsNotCommitted) {
fzk.logRequest(p.hdr, p.rec, p.digest);
}
for (Long zxid : packetsCommitted) {
fzk.commit(zxid);
}
}
上述过程可简要描述为:
a. 从节点面向外部客户的服务端启动,允许外部客户接入并提供请求处理服务
b. 再次给主节点发送Ack
c. 更新自身集群选票中的currentEpoch
d. 对packetsNotCommitted
,packetsCommitted
中尚且存在的包,分别执行序列化和提交操作.
DIFF
模式下,此时完成实际的同步处理.
此后进入从节点常规运行阶段:
self.setZabState(QuorumPeer.ZabState.BROADCAST);
completedSync = true;
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
readPacket(qp);
processPacket(qp);
}
这样从节点已经完成集群同步,且集群已经开始对外提供服务了.
3.5.主节点的后续
(1). LearnerHandler
的run()
中的后续
LearnerHandler
的run
中后续进入常规的事件循环.
在事件循环里先收集来自从节点的包,再对包执行处理.
(2). Leader
的lead()
的后续
startZkServer();// 起点服务端,开启外部服务
self.setZooKeeperServer(zk);
self.setZabState(QuorumPeer.ZabState.BROADCAST);
self.adminServer.setZooKeeperServer(zk);
boolean tickSkip = true;
String shutdownMessage = null;
while (true) {
synchronized (this) {
long start = Time.currentElapsedTime();
long cur = start;
long end = start + self.tickTime / 2;
while (cur < end) {
wait(end - cur);
cur = Time.currentElapsedTime();
}
if (!tickSkip) {
self.tick.incrementAndGet();
}
SyncedLearnerTracker syncedAckSet = new SyncedLearnerTracker();
syncedAckSet.addQuorumVerifier(self.getQuorumVerifier());
syncedAckSet.addAck(self.getId());
for (LearnerHandler f : getLearners()) {
if (f.synced()) {
syncedAckSet.addAck(f.getSid());
}
}
if (!this.isRunning()) {
shutdownMessage = "Unexpected internal error";
break;
}
if (!tickSkip
&& !syncedAckSet.hasAllQuorums()
&& !(self.getQuorumVerifier().overrideQuorumDecision(getForwardingFollowers())
&& self.getQuorumVerifier().revalidateOutstandingProp(this,
new ArrayList<>(outstandingProposals.values()), lastCommitted))) {
shutdownMessage = "Not sufficient followers synced, only synced with sids: [ "
+ syncedAckSet.ackSetsToString() + " ]";
break;
}
tickSkip = !tickSkip;
}
for (LearnerHandler f : getLearners()) {
f.ping();
}
}
if (shutdownMessage != null) {
shutdown(shutdownMessage);
}
这样,主节点方面也已经完成集群同步,且集群已经开始对外提供服务了.
且Leader
的lead()
后续会定期检测与主同步的节点数量,在节点数量不足半数时,停止主,并与所有连到其的从节点断开连接.这样,这些节点包含主自身将再次进入集群选举阶段.