前言
本系列到现在已经把Zookeeper的单机模式全部讲解完毕,用了8篇博客来系统性的介绍了Zookeeper的客户端与服务端的交互过程,操作命令在客户端与服务端的执行流程等等内容。那么我们接下来就要开始攻克另一个Zookeeper的堡垒----集群模式的源码解析。本篇也会被收录到【Zookeeper 源码解读系列目录】中。
集群模式的角色
Zookeeper集群模式下一共有三个角色:
名称 | 说明 |
---|---|
Leader | Zookeeper的主节点,只有leader能处理写请求 |
Follower | 处理leader的proposal,转发写请求,参与选举 |
Observer | 处理leader的proposal,转发写请求,不参与选举 |
从名字上也能看出来Leader
最重要,也只有Leader
能够处理写请求。然后是Follower
和Observer
,这两个本质上没有什么区别,都得处理leader
发来的proposal
,如果Follower/Observer
收到写请求,就会转发给leader
处理。区别就在于前一个参与选举,后一个不参与选举。这里如何控制的,其实早就已经在单机模式解析配置的时候说过了,这里简单提一下,被标识为Observer
的机器在选举之前是没有加入到服务器列表里的,它是在最后面才被加入到服务器列表里面,选举的时候没有Observer
机器的IP,所以没有选举,我们后面还会再讲。当然这也是为了提高整个集群的吞吐量而设置的,比如有100台机器,假设某天Leader
挂了,那么参加选举的机器就是99台。如果其中有50台是Observer,那么参加选举的机器就是49台,选举的时间就会大大的减少,这个的取舍要根据具体场景,不多说。在Zookeeper源码里面Follower
和Observer
处理的时候有个统一的名称叫Learner
,下文如果提到Learner
大家知道是说他俩就好了。
既然集群模式分了角色,那么服务启动的步骤就也会比单机模式要复杂一些:
- 加载配置
- 启动socket
- 选举leader
- 同步数据(保证数据一致性的关键)
- 初始化请求处理器RequestProcessor(同样是处理器链)
- 返回Response
前4步是服务端的启动流程,后两步是服务端接收数据的处理流程,也就是说前4步不走完,客户端是连不上服务端的,服务端当然也不能接收客户端的连接。
解析配置文件
那么我们还是按照程序的入口一步一步的走进去,我们首先要做的还是解析配置文件里面的内容:
----入口----> QuorumPeerMain.main();
----转到----> QuorumPeerMain.initializeAndRun(args);
----转到----> QuorumPeerConfig.parse(args[0]);
我们进入parse(args[0])
这个方法:
public void parse(String path) throws ConfigException {
File configFile = new File(path);
/**Log**/
try {
if (!configFile.exists()) {
throw new IllegalArgumentException(configFile.toString() + " file is missing");
}
Properties cfg = new Properties();
FileInputStream in = new FileInputStream(configFile);//变成流文件
try {
cfg.load(in);//装载成为Properties
} finally {
in.close();
}
parseProperties(cfg);//解析内容
} catch (IOException e) {
/**Exceptions**/
}
}
看到里面是非常常规的文件处理流程,先把文件变成文件流对象FileInputStream
,然后用Properties
类对象加载cfg.load(in);
文件流对象为Properties
类实例赋值。最后调用parseProperties(cfg);
方法解析配置文件里面的内容,我们再次进入这个解析配置文件的核心方法查看,到目前为止和单机模式没什么区别:
public void parseProperties(Properties zkProp)
throws IOException, ConfigException {
int clientPort = 0;
String clientPortAddress = null;
//赋值+解析参数
for (Entry<Object, Object> entry : zkProp.entrySet()) {
String key = entry.getKey().toString().trim();
String value = entry.getValue().toString().trim();
if (key.equals("***")) {
/**略**/
} else if (key.equals("peerType")) {
if (value.toLowerCase().equals("observer")) {
peerType = LearnerType.OBSERVER; //观察者
} else if (value.toLowerCase().equals("participant")) {
peerType = LearnerType.PARTICIPANT; //follower
} else
{
throw new ConfigException("Unrecognised peertype: " + value);
}
} else if (key.startsWith("server.")) {//识别到server.开头的字符串
int dot = key.indexOf('.');
long sid = Long.parseLong(key.substring(dot + 1));
String parts[] = splitWithLeadingHostname(value);//把内容分割开
if ((parts.length != 2) && (parts.length != 3) && (parts.length !=4)) {
LOG.error(value
+ " does not have the form host:port or host:port:port " +
" or host:port:port:type");
}
LearnerType type = null;
String hostname = parts[0];//拿到hostname
Integer port = Integer.parseInt(parts[1]);//拿到传输port
Integer electionPort = null;
if (parts.length > 2){
electionPort=Integer.parseInt(parts[2]);//拿到选举port,这就是传输ack的port
}
if (parts.length > 3){
if (parts[3].toLowerCase().equals("observer")) {//识别observer字段
type = LearnerType.OBSERVER;
} else if (parts[3].toLowerCase().equals("participant")) {//识别participant字段
type = LearnerType.PARTICIPANT;
} else {
throw new ConfigException("Unrecognised peertype: " + value);
}
}
if (type == LearnerType.OBSERVER){
//是观察者,加入到observers里面
observers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type));
} else {
//不是观察者,加入到servers里面
servers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type));
}
}
}
/**解析其他的参数,略**/
if (servers.size() == 0) { //服务器数量==0
/**Exceptions**/
} else if (servers.size() == 1) { //单机模式
/**略**/
} else if (servers.size() > 1) {
if (servers.size() == 2) {
LOG.warn("No server failure will be tolerated. " + "You need at least 3 servers.");
} else if (servers.size() % 2 == 0) {
LOG.warn("Non-optimial configuration, consider an odd number of servers.");
}
/**略**/
if(serverGroup.size() > 0){
/**不涉及serverGroup参数的配置,这里恒定==0,略**/
} else {
/**Log**/
quorumVerifier = new QuorumMaj(servers.size());//构造集群验证器
}
//最终把observers加到servers里面
servers.putAll(observers);
File myIdFile = new File(dataDir, "myid"); //取myid
if (!myIdFile.exists()) {
throw new IllegalArgumentException(myIdFile.toString()
+ " file is missing");
}
BufferedReader br = new BufferedReader(new FileReader(myIdFile));
String myIdString;
try {
myIdString = br.readLine();
} finally {
br.close();
}
try {
serverId = Long.parseLong(myIdString);//serverId取的就是创建的myid,后面也会简称为sid
MDC.put("myid", myIdString);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("serverid " + myIdString + " is not a number");
}
LearnerType roleByServersList = observers.containsKey(serverId) ? LearnerType.OBSERVER
: LearnerType.PARTICIPANT;
if (roleByServersList != peerType) {
/**Log**/
peerType = roleByServersList;
}
}
}
进入后发现里面是一堆写在zoo.cfg
配置文件里面的参数,这些参数就是在这里解析并赋值的,这个我们在单机模式下也说过。尽管删除了冗余代码,长度依然不短,我们按照if
块进行讨论。
先到第一个if (key.equals("peerType"))
块中,只有两个可以识别的值"observer"
和"participant"
。就是说如果配置文件中配了peerType=observer
就是观察者模式,然后程序里就会重置peerType = LearnerType.OBSERVER;
。如果配置文件中配了参与者peerType=participant
就是参与者模式,也就是我们常说的follower
。但是如果不配置peerType
这个参数,其实也没关系,因为peerType
声明的默认值就是LearnerType.PARTICIPANT
,所以如果你不配置,默认这台机器就是一个follower
。
再往下if (key.startsWith("server."))
就是识别我们配置的server.1=localhost:2887:3887
这些集群的服务器地址这里,这里会识别到server.
开头的字符串,并以此决定是单机还是集群。换句话说如果是别到server.
开头的参数,就认为是集群模式,那么反之就是单机模式。我们这里讲的就是集群模式,这里肯定是识别的,所以就走下去。首先分割parts[] = splitWithLeadingHostname(value);
这个字符串,然后解析主机名hostname= parts[0];
,解析传输端口port = Integer.parseInt(parts[1]);
,解析选举的端口electionPort=Integer.parseInt(parts[2]);
这个端口也用来传输ack数据,还有如果if (parts.length > 3)
说明最后配置的有角色字段,那么匹配到字段equals("observer")
就是观察者,匹配到字段equals("participant")
就是follower
,什么都没有匹配到默认就是follower
。
接着if (type == LearnerType.OBSERVER)
如果发现是观察者机器,就添加到observers
这个队列里面observers.put(***);
。反之就默认是follower
,添加到servers
队列里servers.put(***);
。所以我们就是在这里把观察者机器和普通机器区分开的。
略过一大堆代码以后,到if (servers.size() == 0)
这里,这部分的逻辑主要是验证服务器的数量的,单机模式已经说过说不再啰嗦,我们直接跳到quorumVerifier = new QuorumMaj(servers.size());
,这个就是在构造集群验证器,用来决定符不符合过半机制的,先记下,我们一会儿详细说。
接着走发现servers.putAll(observers);
这里就是最终把observers
加到servers
里面,大家看验证器构造完成后,才把观察者机器加入到总的机器列表里面,这也是为什么观察者不参与选举,因为在构造选举验证器的时候根本就没有观察者机器,此时servers
这里才包含了我们所有的服务器。
再往下构造文件对象File myIdFile = new File(dataDir, "myid");
取出来myid
,这里配置的路径已经是我们配置的数据路径了,不再是zoo.cfg
的路径。一样的文件处理逻辑,最终myid
就被传递给了serverId
,以后也会简称为sid
,但是大家要清楚这个sid
就是我们配置的myid
。继续就碰到了一个很重要的验证LearnerType roleByServersList = observers.containsKey(serverId) ? LearnerType.OBSERVER : LearnerType.PARTICIPANT;
这里的作用是:如果发现myid
对应的后面有关键字observer
那么就把这个myid
对应的机器重新标识为OBSERVER
,如果没有那就是PARTICIPANT
。
然后在if (roleByServersList != peerType)
里面更新对应机器的peerType = roleByServersList;
。这里的逻辑其实说明了哪台机器是观察者还是由配置server.1=localhost:2887:3887:observer
这句话最后的observer
决定的,而不是文件中peerType=observer
决定的,即便配置成peerType=participant
,后面主机名那里只要配置了observer
,还是会被刷新掉的。
集群验证类的构造
现在我们掉头回去看quorumVerifier = new QuorumMaj(servers.size());
这里是怎么构造过半验证类的。首先QuorumMaj
这个类就是过半机制验证的类,我们看下这个类:
public class QuorumMaj implements QuorumVerifier { ... }
发现这个类实现了QuorumVerifier
类,从名字看就知道这是个集群验证类。我们接着到QuorumMaj类的构造方法里面看下:
public QuorumMaj(int n){
this.half = n/2;
}
里面只有一句话,是为了生成过半this.half
这个数字,这个half
也在下面的一个containsQuorum()
方法中使用了:
public boolean containsQuorum(Set<Long> set){
return (set.size() > half);
}
这里的路基也十分的简单,当传入的set
中的元素数量大于half
的时候(set.size() > half);
,返回true
,否则返回false
。这两个方法联系在一起我想大家一定都能想到,这里就是过半验证机制密切相关的地方。其实没错,它就是验证集群用是否过半用的,我们调用这个方法传一堆服务器(set
)进去,然后就去判断传入的服务器的数量是不是大于配置的集群的一半,如果过半就是说验证通过集群可用。如果不过半就不通过这个集群不能用,而且这个过半是指follower
的服务器过半,并不包含observer
。因为我们在构造half
的时候传入的是servers.size()
,而这个servers
并不包含observers
。我们以后也会多次见到containsQuorum(Set<Long> set)
这个方法,自此解析配置文件结束。
集群模式服务端的启动
解析讲完了,我们得跳出来这个方法,大家还记得跳到哪里吗?集群模式下代码的跳跃性更大一些,因为希望跳转的步骤被缩减了,所以大家要留心我们从哪进去的,又从哪里跳出来接着走。
----入口----> QuorumPeerMain.main();
----转到----> QuorumPeerMain.initializeAndRun(args);
----转到----> QuorumPeerConfig.parse(args[0]);
我们其实一直都在ZooKeeperMain.initializeAndRun(args);
这个方法里面,所以我们从parse(args[0])
跳出来当然要继续initializeAndRun(args)
这个方法:
protected void initializeAndRun(String[] args)
throws ConfigException, IOException
{
//服务端配置类
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);//解析配置文件,进入
}
/**略**/
if (args.length == 1 && config.servers.size() > 0) {//是否是集群配置
runFromConfig(config);//集群模式
} else {
ZooKeeperServerMain.main(args); //单机模式
}
}
我们解析完了文件以后,config.servers.size()
必定是大于0的,所以这次我们就可以进入集群模式的启动方法runFromConfig(config);
里面探究集群模式了:
public void runFromConfig(QuorumPeerConfig config) throws IOException {
/**Log**/
try {
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); //构造socket
//打开socket连接准备接收客户端信息
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//新构造一个机器的实例
quorumPeer = getQuorumPeer();
//取配置文件中找到的集群下有多少服务器存进来
quorumPeer.setQuorumPeers(config.getServers());
//把配置文件中的内容直接拿过来
quorumPeer.setTxnFactory(new FileTxnSnapLog(
new File(config.getDataLogDir()),
new File(config.getDataDir())));
quorumPeer.setElectionType(config.getElectionAlg());
quorumPeer.setMyid(config.getServerId());
quorumPeer.setTickTime(config.getTickTime());
quorumPeer.setInitLimit(config.getInitLimit());
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
quorumPeer.setClientPortAddress(config.getClientPortAddress());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
/**sasl略**/
quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
//设置完毕以后,初始化
quorumPeer.initialize();
quorumPeer.start(); //重点在这里
quorumPeer.join();
} catch (InterruptedException e) {
LOG.warn("Quorum Peer interrupted", e);
}
}
进入后,我们看第一步当然还是要构造socket连接cnxnFactory = ServerCnxnFactory.createFactory();
,然后打开socket,准备接收客户端的请求cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns())
,但是这里只是打开了客户端的请求,并没有打开和别的server的连接。下面quorumPeer = getQuorumPeer()
新构造一个集群中的服务器实例,集群中的一台每个单独的机器都会走到这里去构造一个自己的实例。再往下一堆set
方法设置参数,不多说。设置完毕以后,就可以初始化我们的机器了quorumPeer.initialize();
。那接下来看到一个重要的方法quorumPeer.start();
那我们就进去看看,是怎么启动的:
public synchronized void start() {
loadDataBase();//从快照里导出数据到内存
cnxnFactory.start();
startLeaderElection();
super.start();
}
快照里导出数据到内存
进入以后看到里面调用了几个方法,我们一个一个的来看,首先来看loadDataBase();
这个方法,它的作用是从快照里导出数据到内存:
private void loadDataBase() {
/**略**/
try {
zkDb.loadDataBase();//重要
/**略**/
} catch(IOException ie) {
LOG.error("Unable to load database on disk", ie);
throw new RuntimeException("Unable to run quorum server ", ie);
}
}
里面看起来代码很多其实我们要关注的就只有一行zkDb.loadDataBase();
,那么我们接着进入:
public long loadDataBase() throws IOException {
long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
initialized = true;
return zxid;
}
这个方法内容有没有很眼熟?没错我们讲解单机模式的时候的讲过。从这里可以看出,Zookeeper
服务端启动的时候集群模式和单机模式是有相通的地方的,比如这里就是启动的时候都要从快照里加载数据到内存中。那么我们继续进入snapLog.restore(***)
方法里面去:
public long restore(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
snapLog.deserialize(dt, sessions);//反序列化
return fastForwardFromEdits(dt, sessions, listener);//找事务并取出
}
首先还是反序列化调用方法snapLog.deserialize(dt, sessions);
,把快照的日志反序列化出来数据加载到DataTree
中。然后用fastForwardFromEdits(dt, sessions, listener)
方法找出事务并取出加载到内存,详细内容请转到【加载快照数据到内存】,包括服务端挂了应该如何同步日志等等内容。
QuorumPeer.run()
跳出回到quorumPeer.start()
方法接着分析:
public synchronized void start() {
loadDataBase();//从快照里导出数据到内存
cnxnFactory.start(); //接受客户端的请求
startLeaderElection(); //领导者选举
super.start();
}
往下走cnxnFactory.start();
开启NIOServerCnxnFactory线程,接受客户端的请求,这部分单机模式已讲过不再多说。startLeaderElection();
领导者选举后面专门开篇讲。再后面super.start();
这里启动的是QuorumPeer
自己的run()
方法,因为QuorumPeer
就是一个线程类,那么就去QuorumPeer.run()
方法里面看下。在进入方法之前,先说一个重要的概念叫做PeerState
,用来标识区分服务端在选举过程中的角色或者在集群中做扮演的角色。PeerState
一共有四种:
PeerState | 说明 |
---|---|
LOOKING | 代表此服务器还没有确定好角色,就是正在进行领导者选举。如果选举完了,PeerState状态会改变为FOLLOWING或者LEADING。 |
FOLLOWING | 此服务器确定是一个跟随者Follower。 |
LEADING | 此服务器确定是一个领导者Leader |
OBSERVING | 此服务器确定是一个观察者Observer |
了解完有这四种状态以后,我们先来看如果选举为Leader
后会做什么操作,到run()方法里:
public void run() {
/**略**/
try {
while (running) {
switch (getPeerState()) { //取PeerState
case LOOKING: //未确定角色
/**LOOKING暂时略**/
break;
case OBSERVING: //确定为观察者
/**OBSERVING暂时略**/
break;
case FOLLOWING: //确定为跟随着
/**FOLLOWING暂时略**/
break;
case LEADING: //确定为领导者
LOG.info("LEADING");
try {
setLeader(makeLeader(logFactory));//new一个leader
leader.lead(); //核心方法
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
} finally {
/**略**/
}
}
除去不相干代码以后,我们着重看里面的while (running)
这里其实就是一个永久循环,紧接着就是去取getPeerState()
,并且用PeerState
作为一个分支的判断条件。要注意因为是永久循环,状态一旦修改就会走到别的分支去了,Zookeeper也正是通过这个循环来进行不断改变角色的。那么如上所说,我们到case LEADING:
这个分支里。首先看到makeLeader(logFactory);
就是说当一台服务器选为leader
后,先new
一个leader
并且用setLeader(***)
赋值,然后调用leader.lead();
方法,这个才是核心方法。
Leader与Learner的交互
看代码前我们先想一个问题:一台服务器当选为leader
后要做什么事情呢?我们分析一下,首先leader
需要跟很多个follower
和observer
(我们统称为learner
)通信对不对,这里提醒一下通信用的就是配置文件中的第二个端口号。既然leader
要和各个服务器通信,第一步要做什么?必然是先开启socket吧。第二步呢,传输数据嘛,所以按照我们在单机模式里的逻辑,leader
又会给每个单独learner
开启一个线程,这个线程应该会不断地从socket
里面获取数据,处理,传输等等任务。那么现在去代码看下我们猜的对不对,到lead()
里去找到连接部分:
void lead() throws IOException, InterruptedException {
/**略**/
try {
self.tick.set(0);
zk.loadData();//加载数据
leaderStateSummary = new StateSummary(self.getCurrentEpoch(), zk.getLastProcessedZxid());
cnxAcceptor = new LearnerCnxAcceptor(); //创建线程
cnxAcceptor.start(); //调用start()启动线程
/**代码很多,暂时无关**/
} finally {
zk.unregisterJMX(this);
}
}
进入这个方法以后,早早的就可以看到创建了一个线程LearnerCnxAcceptor
,然后直接就开启了这个线程cnxAcceptor.start();
,老规矩,我们要去LearnerCnxAcceptor.run()
里看看里面写了什么:
public void run() {
try {
while (!stop) {
try{
Socket s = ss.accept();//开启socket
s.setSoTimeout(self.tickTime * self.initLimit);
s.setTcpNoDelay(nodelay);
BufferedInputStream is = new BufferedInputStream(s.getInputStream());
//构造了一个LearnerHandler
LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
fh.start();
} catch (SocketException e) {
if (stop) {
LOG.info("exception while shutting down acceptor: " + e);
stop = true;
} else {
throw e;
}
} catch (SaslException e){
LOG.error("Exception while connecting to quorum learner", e);
}
}
} catch (Exception e) {
LOG.warn("Exception while accepting follower", e);
}
}
我们看到Socket s = ss.accept();
这里开启socket
等待连接,然后构造了一个LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
,很明显这个LearnerHandler
也是一个线程。那么我们大概有这样一个概念了,还记得之前说过每一个socket
都会单独开启一个线程吗。
要提醒下,外面是LearnerCnxAcceptor
线程进来的,然后LearnerCnxAcceptor
线程会接收socket
连接,一旦有一个socket
来了就用ss.accept()
开启连接,那么Socket s
就会单独new
一个线程LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
,然后再启动fh.start();
这个线程继续后面的操作。从名字上看学习者处理器(LearnerHandler
)线程是为了处理leader
和follower
数据交互的。那么就又有一个问题了,作为follower
到底会不会主动和leader
连接呢,所以我们接下来就必须去看下follower
里面又写了什么。
我们接着跳出来找到QuorumPeer.run()
里面,找到case FOLLOWING:
里面的逻辑:
```java
public void run() {
/**略**/
try {
while (running) {
switch (getPeerState()) { //取PeerState
case LOOKING: //未确定角色
/**LOOKING暂时略**/
break;
case OBSERVING: //确定为观察者
/**OBSERVING暂时略**/
break;
case FOLLOWING: //确定为跟随着
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));//先new一个Follower
follower.followLeader(); //核心方法
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
case LEADING: //确定为领导者
/**LEADING暂时略**/
}
}
} finally {
/**略**/
}
}
我们可以看到几乎是一样的逻辑,先makeFollower(logFactory)
就是new
一个follower
,然后调用follower.followLeader();
这个核心方法,那么我们进入看看follower
会不会发一个请求给leader
:
void followLeader() throws InterruptedException {
/**略**/
try {
QuorumServer leaderServer = findLeader(); //首先找到leader
try {
connectToLeader(leaderServer.addr, leaderServer.hostname);//connectToLeader
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);//记住这里!!!
/**暂时略后面讲**/
} finally {
zk.unregisterJMX((Learner)this);
}
}
进入后我们发现它会QuorumServer leaderServer = findLeader();
首先调用方法找到哪个是leader
,然后再用connectToLeader(leaderServer.addr, leaderServer.hostname)
方法,去连接找到的领导者,从传入的参数也能看出来这里面基本上就是再用socket
了,还是进入看下是怎么连接的:
protected void connectToLeader(InetSocketAddress addr, String hostname)
throws IOException, ConnectException, InterruptedException {
sock = new Socket(); //构建一个socket
sock.setSoTimeout(self.tickTime * self.initLimit);
for (int tries = 0; tries < 5; tries++) {
try {//给leader地址addr,发送一个socket请求
sock.connect(addr, self.tickTime * self.syncLimit);
sock.setTcpNoDelay(nodelay);
break;
} catch (IOException e) {
if (tries == 4) { //第五次还连接不上报错
LOG.error("Unexpected exception",e);
throw e;
} else {
LOG.warn("Unexpected exception, tries="+tries+ ", connecting to " + addr,e);
sock = new Socket();
sock.setSoTimeout(self.tickTime * self.initLimit);
}
}
Thread.sleep(1000);
}
/**略**/
}
首先构建一个socket = new Socket();
,后面for
循环里面就是用sock.connect(addr, self.tickTime * self.syncLimit);
给leader
的地址addr
发送一个socket
请求,主动连接leader
,要注意这个方法里并没有发送数据,只是建立连接而已。此外还有一个小细节,大家看这个for
循环,一共循环了5次对不对,这个意思就是说一个follower
会尝试连接5次才会报错。一旦链接上就会break
跳出,接续别的逻辑,剩下的其实也没有什么重要的东西直接略过,既然follower
发了请求,而leader
正在等着接收,那么leader
接收到这个socket
请求以后,又会干什么呢?我们就需要转到LearnerHandler.run()
里面看下leader
的动作,这部分讲解跳跃性很大,大家一定要看清楚代码走到哪里了,这里为什么要到LearnerHandler.run()
里呢,因为刚才我们讲解到LearnerCnxAcceptor.run()
里面的时候说到领导者用ss.accept();
这里开启了线程,然后说到LearnerHandler
线程开启,但是我们并没有接着讲这个类中的run()
方法,而是转到了上层FOLLOWER
的逻辑块中,所以我们直接接着讲就好了。
----------此处接着LearnerCnxAcceptor.run()
这个方法的ss.accept();
往下到fh.start();
继续------------
所以我们现在来到了LearnerHandler.run()
方法里看下leader
的动作,这里说明一下,本篇后面的代码是需要follower
和leader
有一些互动的,所以为了更好的展示,会只贴需要的代码,其他的一律忽略:
public void run() {
/**略**/
//拿到输入输出流信息,要记清楚 ia 和 oa 两个变量,很重要
ia = BinaryInputArchive.getArchive(bufferedInput);
bufferedOutput = new BufferedOutputStream(sock.getOutputStream());
oa = BinaryOutputArchive.getArchive(bufferedOutput);
QuorumPacket qp = new QuorumPacket();
ia.readRecord(qp, "packet");//当建立连接后首先从socket里面读数据,说明learner要先发送数据
/**略**/
}
进入以后,先拿到了输入输出流信息ia
和oa
,然后直接就看到ia.readRecord(qp, "packet")
,这不就是立刻读取了数据,所以说明learner
首先要向leader
发数据。但是这里要注意这个数据是是什么,我们看参数qp
是QuorumPacket
的实例,也就是说learner
那里是把数据先包装成一个QuorumPacket
才发出来的。那么我们就有了一个目标,返回去查看learner
在哪里包装了QuorumPacket
并且wirte
出来的。其实就在followLeader()
这个方法里面,我们当时说完connectToLeader(***)
就转去讲解leader
了,所以我们这里也接着来。
Epoch
void followLeader() throws InterruptedException {
/**略**/
connectToLeader(leaderServer.addr, leaderServer.hostname);//连接leader
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);//发送数据给leader
/**略**/
}
newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
这个方法就是发送数据给leader
的。看这个方法之前,我们先简单说下Epoch
,这个的知识点我们会在领导者选举的章节里详细的讲解。其实能够走到这里,就说明选举已经结束了,那么leader
一定会统一各个客户端的Epoch
,这里就是接收这个属性的。Epoch
应该怎么理解呢,很像现实中选举的届的概念。比如集群刚刚建成经过第一次选举就是第一届leader
。如果中间又进行了一次选举,那么整个集群就有了第二届leader
,每一届的leader
都会把整个集群的Epoch
统一一下,就好像新皇帝肯定要换个年号一样。简单说完这个内容,我们去看下里面的代码:
protected long registerWithLeader(int pktType) throws IOException{
long lastLoggedZxid = self.getLastLoggedZxid();
QuorumPacket qp = new QuorumPacket(); //构造一个数据包
/**设置qp的数据**/
qp.setType(pktType);
writePacket(qp, true);//写数据
readPacket(qp); //接收的来自leader的数据
/**暂时略**/
}
进入查看发现就是在这里qp = new QuorumPacket();
构造一个数据包,然后writePacket(qp, true);
写数据,所以LearnerHandler
就可以读数据了。再往下看readPacket(qp);
这里又要读数据,我们的learner
不可能只读数据对吧,我之前说过这里是一个密集的交互过程,那么这里一定是接收的来自leader
的数据,先不多说。既然learner
已经写数据了,那么我们的leader
就可以接着走了,于是回到LearnerHandler.run()
里面接着ia.readRecord(qp, "packet");
往下走:
public void run() {
/**略**/
ia.readRecord(qp, "packet");//从socket里面读数据
if(qp.getType() != Leader.FOLLOWERINFO && qp.getType() != Leader.OBSERVERINFO){
LOG.error("First packet " + qp.toString() + " is not FOLLOWERINFO or OBSERVERINFO!");
return;
}
byte learnerInfoData[] = qp.getData(); //拿到数据
if (learnerInfoData != null) {
if (learnerInfoData.length == 8) { //给sid赋值,就是learner的id
ByteBuffer bbsid = ByteBuffer.wrap(learnerInfoData);
this.sid = bbsid.getLong();
} else {
LearnerInfo li = new LearnerInfo();
ByteBufferInputStream.byteBuffer2Record(ByteBuffer.wrap(learnerInfoData), li);
this.sid = li.getServerid();
this.version = li.getProtocolVersion();
}
} else {
this.sid = leader.followerCounter.getAndDecrement();
}
LOG.info("Follower sid: " + sid + " : info : " + leader.self.quorumPeers.get(sid));
if (qp.getType() == Leader.OBSERVERINFO) {
learnerType = LearnerType.OBSERVER;
}
//拿到最新的learner的Epoch
long lastAcceptedEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
long peerLastZxid;
StateSummary ss = null;
long zxid = qp.getZxid(); //得到learner的zxid
long newEpoch = leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);//计算出最新的epoch
/**略**/
}
代码比较长,我们分段讲。接收到发来的数据以后,如果是不是追随者或者观察者发来的,就直接报错返回。如果没问题,就拿到learnerInfoData[] = qp.getData();
其实中的数据。如果数据也没问题,就更新sid
到当前收到的数据。这里怎么理解呢,我们讲过每一个socket
都是一个线程,所以每一个learner
都会发来自己的sid
,这里就是要把当前的连接的learner
的sid
给到现任leader
。这里的learnerInfoData != null
判空就是做这个事情的,这个sid
在下面还会用到。那么我们继续,把learner
最新的届拿出来lastAcceptedEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
。再把learner
的zxid = qp.getZxid();
也就是事务id
提取出来。往下就是选举出最新的届数方法了leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);
,传进去的参数this.getSid()
就是当前learner
的sid
,而lastAcceptedEpoch
则是最新的届,记好这两个参数,我们进入看下更新届是怎么处理的。
public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
synchronized(connectingFollowers) {
if (!waitingForNewEpoch) {
return epoch;
}
if (lastAcceptedEpoch >= epoch) {//核心就是在这里
epoch = lastAcceptedEpoch+1;
}
if (isParticipant(sid)) {//排除observer的sid
connectingFollowers.add(sid);
}
QuorumVerifier verifier = self.getQuorumVerifier();
//进行过半验证
if (connectingFollowers.contains(self.getId()) &&
verifier.containsQuorum(connectingFollowers)) {
//验证通过
waitingForNewEpoch = false;
self.setAcceptedEpoch(epoch);
connectingFollowers.notifyAll(); //唤醒等待验证epoch的connectingFollowers
} else {
//如果没有通过
long start = Time.currentElapsedTime();
long cur = start;
long end = start + self.getInitLimit()*self.getTickTime();
while(waitingForNewEpoch && cur < end) {
connectingFollowers.wait(end - cur); //wait直到选举成功
cur = Time.currentElapsedTime();
}
if (waitingForNewEpoch) {
throw new InterruptedException("Timeout while waiting for epoch from quorum");
}
}
return epoch;
}
}
我们直接到核心的if
块这里,我们看里面的内容是传递进来的届数加一epoch = lastAcceptedEpoch+1;
并且作为最新的届。那么逻辑就是:初始化的时候epoch
是-1,对比如果传入的lastAcceptedEpoch
大,就会重新赋值而且+1,作为新一轮的epoch
。因为既然传递过来了,说明这次一定是一次新的选举,要有一个新的也是最大的epoch
产生。往下既然我们已经有了最新的届了,下一步就是要进行过半认证。因为各个机器learner
的届不一定相同,算出来最大的届以后必须要得到所有机器的同意,所以必须要通过过半机制统一大家都是一个届。进行选举验证之前,我们就要排除观察者if (isParticipant(sid))
,然后把追随者的sid
加到要被验证的set
中connectingFollowers.add(sid);
。再往下的if块,会根据服务器配置进行过半验证,如果验证通过了,就先给自己设置新的届,然后唤醒等待验证epoch
的connectingFollowers
,这里用的是notifyAll()
所以会唤醒所有等待的机器。如果没有通过,就循环connectingFollowers.wait(end - cur);
在这里等待直到选举成功,直到通过过半机制的验证被唤醒。我们知道届Epoch
是怎么生成的以后,就还要回到LearnerHandler.run()
里面继续流程:
ACK机制
public void run() {
/**略**/
//计算出最新的epoch
long newEpoch = leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);
if (this.getVersion() < 0x10000) {
/**version是一个选举相关的参数,一般不走这里**/
} else {
byte ver[] = new byte[4];
ByteBuffer.wrap(ver).putInt(0x10000);
//发送Leader.LEADERINFO给其他
QuorumPacket newEpochPacket = new QuorumPacket(Leader.LEADERINFO, ZxidUtils.makeZxid(newEpoch, 0), ver, null);
//写出去,给learner去读取这个新的Epoch
oa.writeRecord(newEpochPacket, "packet");
bufferedOutput.flush();
QuorumPacket ackEpochPacket = new QuorumPacket();
ia.readRecord(ackEpochPacket, "packet"); //读取ack信息
if (ackEpochPacket.getType() != Leader.ACKEPOCH) {
LOG.error(ackEpochPacket.toString() + " is not ACKEPOCH");
return;
}
ByteBuffer bbepoch = ByteBuffer.wrap(ackEpochPacket.getData());
ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
leader.waitForEpochAck(this.getSid(), ss);
}
peerLastZxid = ss.getLastZxid();//这里拿到最新的follower端的id
/**略**/
}
有了这个新的有了这个新的Epoch
以后,当前的leader
就需要把这个信息给所有的learner
同步一下。我们这里直接到else
去,因为笔者也不太明白version
这个参数到底在做什么,只是在3.4.6
里面添加的新的和选举有关的参数,但是和version
相关的逻辑又基本上都不走,这就很费解,我们先忽略掉。就是在new QuorumPacket(Leader.LEADERINFO, ZxidUtils.makeZxid(newEpoch, 0), ver, null);
这里,新届newEpoch
被包装成了QuorumPacket
。然后通过oa.writeRecord(newEpochPacket, "packet");
写出去,供给自己的learner
去读取这个新的Epoch
,也就是在这里发送Leader.LEADERINFO
消息给自己的learner
知晓。接着发现leader
又开始读了,这次读取的是ackEpochPacket
。这说明learner
接收到LEADERINFO
的新届以后,给leader
又返回数据了。所以我们还得Learner.registerWithLeader(int pktType)
这个方法里看发了什么数据。
protected long registerWithLeader(int pktType) throws IOException{
/**略**/
writePacket(qp, true);//写数据
readPacket(qp); //接收的来自leader的数据
final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
if (qp.getType() == Leader.LEADERINFO) { //LEADERINFO这里就对上了
/**拿出数据更新自己的epoch**/
//构造一个ackNewEpoch
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
//然后又写出去
writePacket(ackNewEpoch, true);
return ZxidUtils.makeZxid(newEpoch, 0);
} else {
if (newEpoch > self.getAcceptedEpoch()) {
self.setAcceptedEpoch(newEpoch);
}
if (qp.getType() != Leader.NEWLEADER) {
LOG.error("First packet should have been NEWLEADER");
throw new IOException("First packet should have been NEWLEADER");
}
return qp.getZxid();
}
}
调过来以后,首先readPacket(qp);
读取leader
的数据,然后if (qp.getType() == Leader.LEADERINFO)这里就对的上了,刚才我们说learner
发了LEADERINFO
对不对,就是发给这里用的。首先拿出数据用if
语句对比更新自己的epoch,这一块没什么可讲的。重点就在后面的部分ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
构造一个ackNewEpoch
,然后又writePacket(ackNewEpoch, true);
又写出去。这里就写到了LearnerHandler.run()
的读取ACK
数据包的ia.readRecord(ackEpochPacket, "packet");
里面,这样就选举出了一个最新的Epoch
届号。
这些流程完成了以后,这里就是我们说的leader
和follower
的ack
机制。这一篇跳跃度非常的大,如果不跟着源码看,很容易就看乱了,而且我们其实仍然没有讲完LearnerHandler.run()
,即便是笔者本人也已经读过源码好几遍以后才开始构思写集群模式的章节,所以希望坚持到这里的各位一定跟着源码走。另外下面针对这部分给大家画了一张图解,希望对大家理解这部分的原理有帮助。
总结
这一篇长文介绍了Zookeeper的集群模式是怎么开启的,因为这一部分的内容如果分成两个博客讲怕是会让各位读者不断地在两个帖子之间切换,所以就写了一篇超长文希望能够更好的讲解这部分。那么而我们今天新一届的Epoch已经确定了,下面就是要做数据同步了。数据同步这里的代码只会更加复杂,笔者尽量为分成两篇博客讲解,希望吧。同样本篇也会被收录到【Zookeeper 源码解读系列目录】中。