一、概述
本篇博文会从 Zookeeper 中 FLE(Fast Leader Election) 算法的代码实现入口着手,依次分析 FLE 算法的初始化流程和主要的选举流程的代码实现,并着重分析 FLE 算法中在 Zookeeper 启动时的选主流程。
注:整个代码基于 Zookeeper 3.5.5 版本。
二、核心类简介
2.1 QuorumCnxManager
首先QuorumCnxManager
是针对使用TCP
进行领导者选举而实现的一款 连接管理器(connection manager) ,它能够为集群中的每对节点之间维护一条连接,同时可以确保每对运行正常且可以通过网络进行通信的服务器之间都 只有一条 连接。
为什么上面会强调每对服务器之间仅有一条连接呢,这主要是因为TCP
是双工的,因此没有必要在每对节点间重复建立连接。如果两个服务器同时尝试建立TCP
连接,那么连接管理器会使用一种比较简单的根据双方的 IP 地址决定断开哪条连接的tie-breaking
机制来进行处理。
在一个 Zookeeper 节点中,对于 其它的每一个 节点连接管理器QuorumCnxManager
都为它们维护了一个发送消息的队列,具体代码实现如下,即queueSendMap
中的键为节点的标识,而值为要发送给该节点的数据,而如果与任何特定节点的连接断开,则发送方线程(Sender Thread)会将消息重新放回列表中。
final ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap;
并且因为这种实现的方式是使用队列来维护要发送到另一个节点的消息,所以每次都会将消息添加到队列的尾部,从而可以保证消息的顺序性。这里先大概讲这些,目前大家先对QuorumCnxManager
有个印象知道它是做什么的就可以了,具体的实现可以之后再分析。
三、源码解析
3.1 选举入口分析
在开始对 FLE 算法的 Zookeeper 实现源码分析之前,我们先来探究一下 Zookeeper 中的选举入口,首先对于选举发生的时机一般存在三种:
-
Zookeeper 集群启动时;
-
Leader 挂掉后(可能为 Leader 与大多数 Follower 失去网络连接);
-
当 Leader 发现支持自己的 Follower 的数量不满足法定数量(未过半)时;
因此我们只需要跟随 Zookeeper 集群的启动流程就可以找到选举的入口,通过 Zookeeper 启动的脚本文件 zkServer.sh 我们可以发现 Zookeeper 在启动时其实是启动了一个主类 QuorumPeerMain,具体的脚本代码如下。
# zkServer.sh
ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=
$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"
因此我们直接进入到这个 QuorumPeerMain 类中,并查看它的 Main 方法逻辑,在 Main 方法中首先会创建一个 QuorumPeer 对象,然后利用 Main 方法的入参来初始化并启动 Zookeeper ,这里 Main 方法的入参 args 其实就是我们自己配置并传入的 zoo.cfg 文件。
在进入到initializeAndRun
方法后首先会对我们传入的配置文件进行解析,将文件中对应的信息解析到QuorumPeerConfig
对象中,然后再调用 runFromConfig
方法来通过构建好的QuorumPeerConfig
对象启动 Zookeeper (准确来说是启动 QuorumPeer)。
在runFromConfig
方法中会根据传入的QuorumPeerConfig
对象创建一个新的QuorumPeer
对象,对于这个QuorumPeer
对象我们可以理解为它就是本台服务器在 Zookeeper 集群中的实际体现,也就是 Zookeeper 中的一个节点,在这个QuorumPeer
对象中会保存关于本台服务器在 Zookeeper 中的所有信息以及当前 Zookeeper 集群的相关信息。在创建好这个QuorumPeer
对象后会调用它的 start 方法来启动该 Zookeeper 节点,并在成功启动后调用join
阻塞主线程,使主线程进入到TIMED_WAITING
的状态进行等待。
// QuorumPeerMain.java
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
main.initializeAndRun(args);
}
}
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException
{
// 省略解析配置文件流程...
if (args.length == 1 && config.isDistributed()) {
// 根据配置文件启动 Zookeeper
runFromConfig(config);
} else {
// there is only server in the quorum -- run as standalone
ZooKeeperServerMain.main(args);
}
}
public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException
{
try {
// 省略将配置解析到 QuorumPeer 流程...
// 启动 QuorumPeer
quorumPeer.start();
// QuorumPeer 启动完成后阻塞主线程
quorumPeer.join();
}
}
上面的流程走过后我们就进入QuorumPeer
启动逻辑一探究竟,通过上面的分析我们已经知道了外部是通过调用QuorumPeer
的start
方法来启动该节点的,而在start
方法中主要进行了下面这六步操作。
-
首先检验当前节点的
myid
是否存在于配置文件中,如果不存在则直接抛异常; -
然后进入数据恢复流程,从
DB
中加载数据集到内存的DataTree
中(这里的DataTree
是 Zookeeper 在内存中保存数据的一种形式); -
开启读取数据的线程
cnxnFactory
和secureCnxnFactory
,这两个线程的监听的端口就是我们在zoo.cfg
中配置的clientPortAddress
或clientPort
以及secureClientPortAddress
或secureClientPort
,这两个线程的主要作用就是接收客户端的请求; -
开启管理者服务器,首先
AdminServer
是用于运行命令的嵌入式管理服务器的接口,目前 Zookeeper 中实现的管理者服务器仅有两个实现,一个是JettyAdminServer
另一个是DummyAdminServer
,而大多数情况下我们都是直接使用空实现的DummyAdminServer
; -
从这里开始进入到领导者选举流程,但在这一步仅完成了选举算法的选择以及相关变量的初始化工作,并未真正开始选举流程;
-
调用父类的 start 方法来运行
QuorumPeer
对象中的选举等逻辑,而因为QuorumPeer
本身就是继承自ZooKeeperThread
的一个线程对象,所以这里当调用了 start 方法后就会执行该类的 run 方法,到这里为止相当于该 Zookeeper 节点已经进入了运行状态;
// QuorumPeer.java
public synchronized void start() {
// 1.检验当前节点的 myid 是否存在于配置文件中
if (!getView().containsKey(myid)) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
// 2.加载数据集到内存的 DataTree 中
loadDataBase();
// 3.开启读取数据的线程
startServerCnxnFactory();
try {
// 4.开启管理者服务器
adminServer.start();
}
// 5.初始化领导者选举(暂未开启选举流程)
startLeaderElection();
// 6.启动该节点并开启选举流程
super.start();
}
3.2 选举初始化
synchronized public void startLeaderElection() {
try {
if (getPeerState() == ServerState.LOOKING) {
// 如果当前节点为 Looking 状态则首先将票投给自己
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
}
}
// 如果选择的选举算法为 LeaderElection 则执行下面逻辑
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(getQuorumAddress().getPort());
responder = new ResponderThread();
responder.start();
}
}
// 创建一个新的选举算法实现对象(策略模式)
this.electionAlg = createElectionAlgorithm(electionType);
}
startLeaderElection
方法中当其状态为LOOKING
时(初始化时所有节点状态均为LOOKING
)首先会将票投给自己,然后创建一个选举算法的实现。对于electionType
的设置需要追溯到起初对于配置文件的解析(相关代码如下)。在配置文件中我们可以通过配置指定选举领导者时所采用的算法,但是当我们不设置这个值时,默认会使用QuorumPeerConfig
中electionAlg
的默认值3
,对应到createElectionAlgorithm
方法中的选择也就是默认使用FastLeaderElection
算法。
// QuorumPeerMain.java
public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException
{
try {
// 从配置对象 QuorumPeerConfig 中获取选择的选举算法
quorumPeer.setElectionType(config.getElectionAlg());
}
}
// QuorumPeerConfig.java
protected int electionAlg = 3;
在 Zookeeper 中的领导者选举算法的使用是一种 策略模式 ,即你可以根据自己的需要选择不同的选举算法实现策略,但这些算法最终都会输出一个合格的Leader
。上面我们已经提到在 Zookeeper 中默认的领导者选举算法为FastLeaderElection
,所以我们直接看方法中这个算法的执行流程。
-
创建并初始化连接管理器
QuorumCnxManager
; -
开启
QuorumCnxManager
中的监听器对选举端口进行监听; -
创建
FastLeaderElection
算法实现实例; -
启动
FastLeaderElection
中的接收(receiver)和发送(sender)线程; -
将创建并初始化完成后的选举算法实现返回;
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
// 1.创建一个连接管理器
QuorumCnxManager qcm = createCnxnManager();
// 2.获取旧的连接管理器
QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
// 3.如果发现存在旧的连接管理器则调用它的 halt 方法(停止其 Listener 的监听并关闭该连接管理器)
if (oldQcm != null) {
oldQcm.halt();
}
// 4.获取新的连接管理器的 Listener(Listener 的主要作用就是监听选举端口)
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
// 5.启动 Listener 对端口进行监听
listener.start();
// 6.创建 FastLeaderElection 实现并通过构造器将连接管理器传入
FastLeaderElection fle = new FastLeaderElection(this, qcm);
// 7.启动 FastLeaderElection 中的发送线程(sender)和接收线程(receiver)
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
在执行流程中我们提到了 选举端口(electionAddr),这个端口就是我们在配置文件中配置节点端口时配置的第二个端口(如下图中的 3888 端口,且第一个端口 2888 为集群间内部通讯使用),而当我们要使用 FLE 算法时必须在文件中配置该端口。
server.1=localhost:2888:3888
还要强调的一点是 Zookeeper 采用分层的结构,因此数据会在应用层和网络层之间进行传递,而这个传递的操作落实到FastLeaderElection
中就是其messager
属性所进行的数据管理,正如上述流程中接收线程和发送线程的启动,实际就是调用的messager
对两个线程进行启动。因此在后续的选举流程分析中,为了保证整个分析过程的流畅性我们不会过多的去关注于网络层的实现。
// FastLeaderElection.java
public void start() {
this.messenger.start();
}
// Messenger
void start(){
this.wsThread.start();
this.wrThread.start();
}
3.3 选举流程
当完成了startLeaderElection
的初始化工作后,QuorumPeer
就会通过调用其父类 ZooKeeperThread
的start
方法来执行自己run
方法中的逻辑(QuorumPeer
的本质就是一个线程),而在run
方法中存在一个主循环来执行包括选举和数据同步在内的所有逻辑,而当节点当前的状态为LOOKING
时就会进入到领导者选举的流程,即通过下面这条语句来设置自己当前的投票对象。
setCurrentVote(makeLEStrategy().lookForLeader());
通过makeLEStrategy
可以获取到选举的算法实现,再调用lookForLeader
方法就会进入到特定选举算法的实现中,所以我们直接进入到FastLeaderElection
的lookForLeader
方法中梳理选举逻辑,同时为了简化分析流程,我们这里默认节点会从recvqueue
队列中获取到选票(选票的具体发送和接收流程涉及很多的网络层代码,所以这里暂且放在一边)。
因为lookForLeader
方法的整体逻辑比较繁杂,因此我们将对于选票分状态处理的代码拆分出来,而对于lookForLeader
方法的逻辑概括一下可以分为以下这几步:
-
创建保存选票的
recvset
和用于验证领导者合法性的outofelection
; -
首先递增自己的
Epoch
,然后更新选票提议自己为领导者并发送选票; -
之后通过循环不断与其它节点交换选票直至选出
Leader
; -
如果无法从
recvqueue
中获取到新的选票(此时选举还未完成),则首先判断当前底层中维护的为每个节点发送消息的队列是否为空,如果为空则直接再次发送选票,如果不为空就尝试修复连接; -
如果可以从
recvqueue
中获取到新的选票并且投票人和选票中的领导者均合法,则进入分状态处理逻辑中;
public Vote lookForLeader() throws InterruptedException {
// 省略 JMX 处理逻辑...
try {
// 接收到的选票集合
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
// 用于验证是否大多数节点都在跟随一个相同的领导者以及领导者的合法性
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
// 超时时间
int notTimeout = finalizeWait;
synchronized(this){
// 递增自己的 Epoch
logicalclock.incrementAndGet();
// 更新选票提议为自己
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 发送选票
sendNotifications();
// 通过循环不断与其它节点交换选票直至选出领导者
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
// 从 recvqueue 队列中获取并移除下一个选票,并在终止时间的两倍后超时
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
// 如果从 recvqueue 队列中没能获取到足够的选票则继续发送更多的选票
if(n == null){
// 检查底层 queueSendMap 中所有节点所对应的队列是否为空(所有消息是否都已传递)
if(manager.haveDelivered()){
// 如果所有队列为空就直接发送选票
sendNotifications();
} else {
// 尝试与每台未建立连接的服务器建立连接
manager.connectAll();
}
// 指数回退,每一次的超时时间为上一次的两倍
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval);
}
// 如果从 recvqueue 队列中获取到新的选票则对其进行验证
// 验证选票的投票者是否有效以及验证选票所投的候选人是否有效
else if (validVoter(n.sid) && validVoter(n.leader)) {
switch (n.state) {
case LOOKING:
// Looking...
break;
case OBSERVING:
// Logging...
break;
case FOLLOWING:
case LEADING:
// Following And Leading...
break;
default:
// Logging...
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 {
// 省略 JMX 逻辑..
}
}
在分状态处理中首先进行的是LOOKING
状态的判断,每个节点初始使的默认状态均为LOOKING
,当接受到选票的投票人的状态为LOOKING
时,即认为该投票人也处于选流程中,因此执行下面的处理逻辑:
-
首先如果选票的
Epoch
大于当前节点的Epoch
,意味着已经开始了新一届选举,因此更新当前节点的Epoch
,并清空节点的recvset
选票集合,且如果选票中候选人的条件优于当前节点的条件,则当前节点更改选票提议,并发送新的选票; -
其次如果选票的
Epoch
小于当前节点的Epoch
,意味着这是一张无效的选票,所以直接忽略; -
最后如果选票的
Epoch
等于当前节点的Epoch
,且如果选票中候选人的条件优于当前节点的条件,则当前节点更改选票提议,并发送新的选票; -
完成上述选票更新和发送后,将选票以投票人的
sid
为键保存到recvset
集合中,且因为是以投票人的sid
为键进行保存,所以该投票人后续的选票会直接覆盖掉之前的选票,即每个投票人在recvset
集合中仅保存一张有效选票; -
完成选票保存后通过
termPredicate
方法验证是否已经具备了足够数量的选票来结束选举,方法的实现主要是统计recvset
集合中与当前节点预期领导者相同的选票数量是否过半; -
如果已经具备了足够数量的选票来结束选举则再次验证当前节点所预期的领导者是否发生了变化,实现的方式就是从
recvset
集合中获取选票,如果选票中的领导者优于当前节点的预期领导者,说明当前节点的预期领导者已经发生变化,因此将该选票再次放回recvset
集合中并在break
后继续循环; -
如果在第六步中将
recvset
集合中所有的选票都判断后没有发现优于当前预期领导者的候选人,说明选举已经完成,因此首先判断自己是否为最终的领导者并修改节点的状态; -
创建选举结果选票
endVote
,并在清空recvset
集合后将选举结果返回,完成领导者选举;
在上面的判断过程中存在一个判断优先级的过程(也就是上面所说的 优于 ),判断的过程主要是由totalOrderPredicate
方法来完成,而具体的判断逻辑如下:
- 首先两者中
Epoch
大者优先; - 其次当两者的
Epoch
相等时,zxid
大者优先; - 最后当两者的
Epoch
和zxid
均相等时,myid
大者优先;
// Looking...
case LOOKING:
// 1. 如果选票的 Epoch 大于节点当前的 Epoch
if (n.electionEpoch > logicalclock.get()) {
// 1.1 更新节点当前的 Epoch
logicalclock.set(n.electionEpoch);
// 1.2 清空已接收到的选票集合
recvset.clear();
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
// 1.3 如果选票中候选人的条件优于当前节点的条件,则当前节点更新选票提议
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
// 1.3 否则还是投给自己(这里主要是因为前面更新了 Epoch 所以需要重新更新选票)
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 1.4 发送选票
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
// 2. 如果选票的 Epoch 小于节点当前的 Epoch 则直接忽略该选票
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
// 3. 如果选票中候选人的条件优于当前节点预期的领导者的条件
// 3.1 则节点更新选票支持刚刚接收到的选票中的候选人
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 3.2 发送选票
sendNotifications();
}
// 将获取到的选票放入到 recvset 选票集合中
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 验证是否已经具备了足够的选票来结束选举
if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) {
// 验证自己提议的领导者是否有任何变化
while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){
// 如果选票集合中存在选票的候选人优于当前自己提议的领导者则将该选票重新放回 recvqueue 集合中并 break
// 继续接收选票直到选票集合中所有选票的候选人都弱于或等于自己提议的领导者
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
// 因为上面会将合格的选票(选票候选人弱于或等于自己提议的领导者)直接从 recvqueue 中移除
// 所以当从 recvqueue 队列中无法再取更多选票时意味着选举结束
if (n == null) {
// 判断自己是否为最终的领导者并修改节点的状态
self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState());
// 创建终止选票(用于输出 FLE 算法所选择出的领导者信息)
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
// 执行终止选举的相关逻辑(主要是清空 recvqueue 队列)
leaveInstance(endVote);
// 返回终止选票(输出选举结果)
return endVote;
}
}
break;
在分状态处理中是将对FOLLOWING
和LEADING
状态放在一起进行处理的,这主要是因为当节点接收到的选票处于该状态时证明其它节点的选举可能已经完成,因此当前节点在 验证 后应该跟随其它节点所选出的那个领导者。
具体的验证流程基本如下代码的注释所示,这里需要强调的是outofelection
这个集合的作用,这个集合主要是用来验证是否大多数节点已经跟随了某个节点,而对于它的统计流程其实和上面LOOKING
中的统计流程是一样的,只不过在这里被统计的集合是outofelection
。
对于上面的描述可以理解为在正常情况下因为领导者只需要超半数的选票即可获选,因此可能在其获选后或者当有新的节点加入时,会出现当节点的状态为LOOKING
时却收到状态为FOLLOWING
和LEADING
的选票,因此这时它只需要按照正常的统计逻辑,在接收到 超半数且支持某个相同领导者 的选票后即可确认存在大量的节点已经跟随这个领导者,所以它也可以放心的跟随。
case FOLLOWING:
case LEADING:
// 如果选票的 Epoch 等于当前节点的 Epoch
if(n.electionEpoch == logicalclock.get()){
// 将该选票添加到选票集合中
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 验证是否已经具备了足够的选票来结束选举并验证选票中领导者的合法性
if(termPredicate(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, n.electionEpoch)) {
// 判断自己是否为最终的领导者并修改节点的状态
self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState());
// 创建终止选票(用于输出 FLE 算法所选择出的领导者信息)
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
// 执行终止选举的相关逻辑(主要是清空 recvqueue 队列)
leaveInstance(endVote);
// 返回终止选票(输出选举结果)
return endVote;
}
}
// 该集合用于当前节点在决定跟随某个领导者之前,验证是否大多数节点都跟随了这个领导者
outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
// 验证选票中的领导者是否已经具备了足够数量的跟随者并验证选票中领导者的合法性
if (termPredicate(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, n.electionEpoch)) {
synchronized(this){
// 更新当前节点的 Epoch
logicalclock.set(n.electionEpoch);
// 判断自己是否为最终的领导者并修改节点的状态
self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState());
}
// 创建终止选票(用于输出 FLE 算法所选择出的领导者信息
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
// 执行终止选举的相关逻辑(主要是清空 recvqueue 队列)
leaveInstance(endVote);
// 返回终止选票(输出选举结果)
return endVote;
}
而在整个的验证过程中,如果有一个当选的领导者,并且存在超过法定数量的节点支持该领导者,则必须通过checkLeader
方法检查该领导者是否 已投票 并确认其当前处于 领导状态 中,而进行此检查的目的,主要是避免其它节点继续选举 已崩溃 且 不再处于领导状态 的节点。
protected boolean checkLeader(Map<Long, Vote> votes, long leader, long electionEpoch){
// 如果所有节点都认为我是一个领导者,那我就是一个领导者
boolean predicate = true;
if(leader != self.getId()){
// 如果我不是领导者,而且选票集合中没有领导者的选票则返回 false
// 这也就意味着我没有接收到领导者开始领导的信号
if(votes.get(leader) == null) predicate = false;
// 如果我不是领导者,而且领导者在我这里的状态不是 LEADING 则返回 false
else if(votes.get(leader).getState() != ServerState.LEADING) predicate = false;
} else if(logicalclock.get() != electionEpoch) {
// 如果我当前的 Epoch 不等于选举的 Epoch 返回 false
predicate = false;
}
return predicate;
}
四、内容总结
在本篇博文里我们主要分析了 Zookeeper 中 FLE 算法的代码入口以及其逻辑的部分代码实现,对于 FLE 算法的更多代码实现细节会放在之后的博文中,并且在后面的博文中也会仔细分析一下 Zookeeper 中的数据收发流程即网络层的代码实现。