从本文开始正式开始学习zk中核心集群的实现原理。
1、单机调试集群方法
首先创建三个zoodata目录,并将默认zoo.cfg文件拷贝进去
单独配置每个cfg文件:
dataDir[持久化文件与myid文件保存的地址,每个配置文件均不同],clientPort[客户端接口端口,单机测试保证均不相同],server[集群服务地址,配置相同],最后在各自的dataDir目录下创建myid文件[内容为0,1,2编号,与cfg里server.编号 相同]。之后就可以idea配置不同的启动参数,进行集群的调试了。
dataDir=/zookeeper/apache-zookeeper-3.7.0/conf/zoocfg/zoo3
clientPort=2183
server.0=127.0.0.1:2888:3888
server.1=127.0.0.1:2889:3889
server.2=127.0.0.1:2890:3890
2、启动流程
首先我们开启三个server,然后关闭一个,打上断点重新开启服务,逐步调试
//首先区别于单机,集群的入口变为了
runFromConfig(){
quorumPeer.initialize();
quorumPeer.start();
}
quorumPeer.start(){
//加载磁盘数据
loadDataBase();
//开启io服务
startServerCnxnFactory();
//进行选举
startLeaderElection();
//peer主循环
super.start();
}
startLeaderElection(){
this.electionAlg = createElectionAlgorithm(electionType);
}
createElectionAlgorithm(){
//这个版本只剩case 3 的选举方法了
//服务器之间用于选举的连接管理类
createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
//与其他服务建立连接与监听
listener.start();
FastLeaderElection fle = new FastLeaderElection(this, qcm);
//开启选举算法消息收发线程
fle.start();
}
3、选举算法
首先选举算法的实现,3.7这个版本用于选举的算法只剩FastLeaderElection这个类了,这里深入学习一下,算法参数我们就先不深究了,先来看看整体的架构,可以看到整体的逻辑是FastLeaderElection类维护了与其他peer的连接,并处理收发消息(均是通过队列+线程的异步形式),发送消息就是简单的从队列中取出消息然后发送,而接收消息实现逻辑较为复杂,主要是针对收发方不同的节点状态以及选票信息进行选票的轮换,这里我们简化一下:
public class FastLeaderElection implements Election {
//与其他服务端的TCP连接管理,处理实际io的收发
QuorumCnxManager manager;
//发送选票消息队列---tosend可以是Notification或者ack
LinkedBlockingQueue<ToSend> sendqueue;
//接收选票消息队列---Notification为选票改变消息
LinkedBlockingQueue<Notification> recvqueue;
//负责实际收发消息的处理类
Messenger messenger;
}
//内部类,处理算法层面的收发选票
protected class Messenger {
WorkerSender{
run(){
//发送消息,添加进发送队列
recvqueue.add()
}
}
WorkerReceiver{
run(){
response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
1、参数校验
2、都不为Looking则将当前的选举结果返回给发送方
3、若都为Looking根据接收到的选举轮次与最大事务id,分别执行不同的算法流程
+大体规则为:忽略选举轮次小的消息,给选举大的消息投票,若相同则比较zxid,sid大的优先
}
}
}
那么选举是如何开始的呢,从上文在服务器启动过程中,会开启选举相关连接与线程。在zk中会在两种情况下开始选举:1、节点启动 2、follower与leader丢失连接。这里我们先看一下在2小节略过的peer主loop,在LOOKING状态会调用lookForLeader进行选举。
while (running) {
case LOOKING:
//开启选举
startLeaderElection();
setCurrentVote(makeLEStrategy().lookForLeader());
case OBSERVING:
setObserver(makeObserver(logFactory));
observer.observeLeader();
case FOLLOWING:
setFollower(makeFollower(logFactory));
follower.followLeader();
case LEADING:
setLeader(makeLeader(logFactory));
leader.lead();
}
FastLeaderElection的主要选举逻辑在lookForLeader方法里,这里就直接偷一张流程图了,代码整体逻辑比较复杂:
4、peer主loop
从上节可以看到peer会根据节点状态进入不同的处理逻辑,并一直循环下去。新的服务启动之后LOOKING状态,会发起一次选举流程。待选举完成之后,则各自进入following与leading流程,这里从leader视角分析以下主要流程:
leader流程
1、启用新的epoch,并与每个follower建立连接,连接处理采用LearnerHandler线程
2、发送NEWLEADER包,并等待follower完成同步
3、周期发送ping给follower维持连接
4、LearnerHandler线程处理具体业务逻辑交互主要是是否进行数据同步与ACK交互
5、对外client请求的处理也采用责任链模式,为以下这些
PrepRequestProcessor:创建和修改状态的Request关联的header和txn
ProposalRequestProcessor:将写请求发送proposal到所有的follower
SyncRequestProcessor:将发出去的proposal批量写入磁盘
AckRequestProcessor:当proposal真正写入了磁盘后,向本机发送ack包
CommitProcessor:匹配本地submitted的请求和收到的committed的请求
ToBeAppliedRequestProcessor:把写入到磁盘的proposal从toBeApplied中删除
finalProcessor:把commit的proposal写入到本机的内存状态中