前情提要
上一篇博客我们提到SendWorker
和RecvWorker
这两个线程形成了两个链条,在选举过程中处于收发数据的重要位置。但是我们留了两个问题:1. queueSendMap
这里的数据从何而来,2. recvQueue
收到的数据是谁来使用的?本篇主要就是来处理这两个问题。本篇也会被收录到【Zookeeper 源码解读系列目录】中。
FastLeaderElection
上篇我们讲解过SendWorker
和RecvWorker
两个线程的作用。而这两个线程是在Listener
这个线程中运行的,所以我们还是要一步一步的返回去。这两个线程走完以后只有一个return;
,那么就是说Listener
这个线程的run()
方法已经走完了。所以我们回到QuorumPeer. createElectionAlgorithm()
方法中,接着listener.start()
继续看。
case 3:
qcm = createCnxnManager();//初始化负责各个服务器之间的底层leader选举过程中的网络通信
QuorumCnxManager.Listener listener = qcm.listener; //取出listener
if(listener != null){
listener.start(); //启动listener线程
le = new FastLeaderElection(this, qcm); //硕果仅存的算法类
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
既然只剩下硕果仅存的算法类可以作为线索,我们也只能通过解析FastLeaderElection
类做了什么事情来继续我们的源码分析:
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this.stop = false;
this.manager = manager;
starter(self, manager);
}
private void starter(QuorumPeer self, QuorumCnxManager manager) {
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
sendqueue = new LinkedBlockingQueue<ToSend>();
recvqueue = new LinkedBlockingQueue<Notification>();
this.messenger = new Messenger(manager);
}
进入构造方法,就有看到两个queue:sendqueue
和revcqueue
,以及一个Messenger
类实例对象的构造。那我们点进Message类:
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
WorkerSender线程
发现里面又启动了两个线程WorkerSender
和WorkerReceiver
。我们还是一个一个来看,既然启动了,呢吗先点进去WorkerSender.run()
看里面跑了什么东西:
public void run() {
while (!stop) {
try {
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
process(m);
} catch (InterruptedException e) {
break;
}
}
LOG.info("WorkerSender is down");
}
看到几乎没有什么代码,在这里取出了sendqueue
里面的内容ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS))
,然后调用了process(m)
方法处理:
void process(ToSend m) {
ByteBuffer requestBuffer = buildMsg(m.state.ordinal(),
m.leader,
m.zxid,
m.electionEpoch,
m.peerEpoch);
manager.toSend(m.sid, requestBuffer);
}
这里面更简单,只有一行manager.toSend(m.sid, requestBuffer)
。这个manager
又是什么呢,找到声明发现是QuorumCnxManager
这个类的对象,那我们继续进入toSend(m.sid, requestBuffer)
方法:
public void toSend(Long sid, ByteBuffer b) {
//如果是发给自己的,则不需要经过网络,自己转成Message,并加入到recvQueue即可
if (this.mySid == sid) {
b.position(0);
addToRecvQueue(new Message(b.duplicate(), sid));
} else {
//如果不是发给自己的,发送给其他服务器,加入到服务器所对应的发送队列中
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
if (bqExisting != null) {
addToSendQueue(bqExisting, b);
} else {
addToSendQueue(bq, b);
}
//如果还没有连接到这个服务器,就去连接一下
connectOne(sid);
}
}
方法里面只有一个逻辑判断if (this.mySid == sid)
这里是说如果是发给自己的投票,则不需要经过网络,自己转成Message
,加入到recvQueue
即可。接着看else
的逻辑:如果不是发给自己的,是发送给其他服务器的。就应该加入到服务器所对应的发送队列中,这里就发现了queueSendMap
。这里逻辑也很简单,其实就是把要发的数据放入queueSendMap
中。那么到这里我们的第一个问题就回答出来了:queueSendMap
中的数据就是来自sendqueue
,被WorkerSender
线程从sendqueue
读出来放进去的。
WorkerReceiver线程
说完WorkerSender
线程,我们回头看WorkerReceiver
线程,进入run()
方法:
public void run() {
Message response;
while (!stop) {
// Sleeps on receive
try{
response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
if(response == null) continue;
/**暂时略,代码很多,后面讲**/
} catch (****Exception e) {
/**Exceptions**/
}
}
LOG.info("WorkerReceiver is down");
}
进入后看到,首先又是response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
拿数据。如果拿出来的数据response == null
继续循环continue;
,如果不为null
又有一大堆的逻辑。到这里就很明显就是处理消息的逻辑了,因为数据拿出来肯定就是要处理的嘛,这里的代码具体什么用的先不解释。我们先看从哪里拿的数据,进入pollRecvQueue(3000, TimeUnit.MILLISECONDS);
这个方法:
public Message pollRecvQueue(long timeout, TimeUnit unit) throws InterruptedException {
return recvQueue.poll(timeout, unit);
}
一进去就发现了我们的recvQueue.poll(timeout, unit)
,很明显WorkerReceiver
这个线程就是取数据的线程,而且是从recvQueue
里面取出来数据的。到这里我们的第二个问题也有答案了:WorkerReceiver
就是消费recvQueue
内容的线程。注意这两个queue
,都是小写的,和刚才首字母大写的用处是不一样的,一定要区别开。这里先剧透一下,WorkerReceiver
线程会把消息根据逻辑的不同一部分放到sendqueue
一部分放到revcqueue
里,这部分就是在我略去的代码里,一会儿我们看。那么我们到目前可以有一个合理的推断:这两个队列sendqueue
和revcqueue
的后面,就是领导者选举的算法的核心部分,真正的算法部分。总上所述,我们可以有一个这样的概念图:
记着这个图,我们接下来还会多次引用。领导者选举算法只要把要发送的投票发送到sendqueue
这个队列里可以了,后面就不需要在管了。而取投票只要去revcqueue
里面取就代表了本机从别人那里取到了投票。
QuorumPeer.LOOKING
到这里我们就可以往上返回了,返回到最初我们进入的方法startLeaderElection()
这里继续往下走。大家还记得要返回到哪里吗?问个更远的问题,大家还记得我们是从哪里进来的吗?看懵了的同学基本上就找不到了,笔者看源码的时候也是一样,不用担心,提醒一下:
----入口----> QuorumPeerMain.main();
----转到----> QuorumPeerMain.initializeAndRun(args);
----转到----> QuorumPeerMain.runFromConfig(config);
----转到----> quorumPeer.start();
我们要返回去的方法就是QuorumPeer.start()
方法:
public synchronized void start() {
loadDataBase();
cnxnFactory.start();
startLeaderElection(); //跳出来,继续走
super.start();
}
下一步就是super.start();
,意思就是说QuorumPeer
这个线程类的主线程终于要开始动了。其实这个线程也是我们的老朋友了,基本上已经快讲一个遍了。那么我们现在去QuorumPeer.run()
方法里看最后一个还没有说的分支Looking
分支。在之前的博客里,我们说了LOOKING
状态,代表还没有确定好角色,就是正在进行领导者选举,所以领导者选举的方法应该就是在这里:
public void run() {
/**已经讲过,略**/
try {
while (running) {
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
if (Boolean.getBoolean("readonlymode.enabled")) {
/**只读模式,无关紧要,略**/
} else {
try {
setBCVote(null);
//真正关心的方法在lookForLeader()
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
case OBSERVING: /**已经讲过,略**/ break;
case FOLLOWING: /**已经讲过,略**/ break;
case LEADING: /**已经讲过,略**/ break;
}
}
} finally {
/**已经讲过,略**/
}
}
投票给自己
在集群刚刚启动的时候,由于getPeerState()
中的status
默认的就是LOOKING
状态,所以一台机器启动的时候就会被switch
语句分配进入case LOOKING:
里去。跳过只读模式部分的内容,真正关心的方法就是lookForLeader()
。点进去发现这是个接口,找到FastLeaderElection
类对这个方法的实实现方法,代码很多我们还是分段讲解:
public Vote lookForLeader() throws InterruptedException {
/**初始化,略**/
try {
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); //投票箱map
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
//启动时先投票给自己,然后发送出去
synchronized(this){
logicalclock.incrementAndGet();//时钟+1,记录自己经历了几轮投票
//更新提议,包含(myid,lastZxid,epoch),更新成为自己
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
LOG.info("New election. My id = " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
sendNotifications(); //发送自己的投票
while ((self.getPeerState() == ServerState.LOOKING) && ......
/**暂时略,后面讲**/
}
跳过一些初始化,找到recvset = new HashMap<Long, Vote>()
,这个map
就是我们提到的投票箱的那个箱子,Key(Long)
代表其他服务器,Value(Vote)
代表该服务器投的票。如果有更新,按照key
来更新就好了。往下我们看到一个synchronized
包围的代码,这里其实就是我们说的一台机器首先投票给自己的逻辑,logicalclock.incrementAndGet()
用来记录自己投票轮数然后加1(首次启动,初始化为1)。紧接着updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch())
更新投票的提议,包含的内容有myid
(自己的机器代号)、lastZxid
(自己机器的最新事务id)、epoch
(自己这里是第几届),其实也就是说设置投的票为自己的内容。这里只是设置了内容,后面发送自己的投票sendNotifications()
会用到。所以我们进入进入sendNotifications()
方法:
private void sendNotifications() {
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
//new一个发送的内容
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch);
/**Log4j**/
sendqueue.offer(notmsg); //放入发送队列
}
}
进入方法以后,立即就看到了一个for
循环,而且是循环QuorumServer
的。很明显这个循环就是发现那些能够投票的机器的。然后new
了一个ToSend
再放入发送队列sendqueue.offer(notmsg)
。那么根据上面的分析,这个循环会不断地生成投给自己的投票,生成的数目就是配置的有选举权的机器数目,当然也包括自己。也就是说要给所有的机器发送投给自己的票。第一个步骤投给自己的逻辑就走完了。
接收集群中其他机器的投票
跳出sendNotifications()
走到下面的while ((self.getPeerState() == ServerState.LOOKING) && (!stop))
里面:
public Vote lookForLeader() throws InterruptedException {
/**初始化,略**/
try {
/**已经讲过,略**/
sendNotifications(); //发送自己的投票
//while会不停的拿别人发来的票
while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS); //获取其他服务器的投票
if(n == null){//获取的选票是空的,说明没有收到别的服务器发来的数据
if(manager.haveDelivered()){//看下是否有未发送的数据
sendNotifications();//重新发送选票
} else {
manager.connectAll(); //重新连接
}
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
else if(validVoter(n.sid) && validVoter(n.leader)) {......
/**暂时略,后面讲**/
}
如果服务器还在运行,且状态是Looking就会再往下走。就看到了一个很熟悉的队列recvqueue
,并且从里面取出了Notification n = recvqueue.poll((notTimeout, TimeUnit.MILLISECONDS)
其他服务器的投票。
但是要注意一点:首次启动的时候这里并不会接到其他的服务器发来的数据。但是其实也还会有数据的,因为这个数据就是自己投票给自己的数据。不需要经过网络,WorkSender
这个线程就直接调用方法把自己的票放到了recvQueue
中。然后(概念图)就又流回到了recvqueue
里,这里还要注意WorkSender
也会把要发往别的服务器的投票从sendqueue
取出放到queueSendMap
中,这里放的当然就是配置中的需要经过网络的要到其他服务器的投票。
对选票的验证
当然这里的Notification n
就是取到的结果,也就是我们说的别的服务器发来的选票。紧接着是一个判断if(n == null)
,获取的选票是空的。说明没有收到别的服务器发来的数据。如果没有收到别的服务器发来的票,那么基本上有两种情况:第一是不是我的票没有发出去;第二发出去了但是没有收到回复。manager.haveDelivered()
就是判断这一点的:
boolean haveDelivered() {
//循环每个需要发送给所有服务器的队列,如果发现里面有数据,说明投票没有发出去,那么就返回false
for (ArrayBlockingQueue<ByteBuffer> queue : queueSendMap.values()) {
LOG.debug("Queue size: " + queue.size());
if (queue.size() == 0) {
return true;
}
}
return false;
}
这个方法里会循环queueSendMap
。如果发现里面有数据,说明投票没有发出去,那么就返回false。如果发现里面有queue
没有数据queue.size()==0
说明,我的这个票发成功了。但是只要进入这个if (queue.size() == 0)
就说明,在外面我拿到的选票n
一定是null
,意味着我肯定没有得到回复,返回true
,再次发送这个数据。
所以当if(manager.haveDelivered())
是true
的时候,就调用sendNotifications()
重新发送选票。因为既没有别人来的选票,而我发的选票也没有得到其他机器的回复,那么我就把我配置的选票再发送一次,再次等待各个目标机的回复。如果进入了else
又是什么情况呢?说明queueSendMap
这个map
里面有数据,意味着我的票没有发出去。那么可能的原因就是还没有建立连接,调用manager.connectAll()
连接所有配置的服务器:
public void connectAll(){
long sid;
//找到没有发出去的数据
for(Enumeration<Long> en = queueSendMap.keys(); en.hasMoreElements();){
sid = en.nextElement();
connectOne(sid);//连接
}
}
进入这里以后,可以看到服务器的id
是从queueSendMap
取出来的。取得是queueSendMap.keys()
,就是说哪台的没有发出去,就去连接哪台connectOne(sid)
,我们再深一步进入:
synchronized public void connectOne(long sid){
if (!connectedToPeer(sid)){//判断如果真的没有连接,才回去重新连接
InetSocketAddress electionAddr;
if (view.containsKey(sid)) {
electionAddr = view.get(sid).electionAddr; //获取选举地址
} else {
LOG.warn("Invalid server id: " + sid);
return;
}
try {
LOG.debug("Opening channel to server " + sid);
Socket sock = new Socket(); //建立socket
setSockOpts(sock);
sock.connect(view.get(sid).electionAddr, cnxTO);
LOG.debug("Connected to server " + sid);
if (quorumSaslAuthEnabled) {
initiateConnectionAsync(sock, sid);
} else {
initiateConnection(sock, sid); //连接好了以后初始化
}
} catch (***Exception e) {
/**Exceptions**/
}
} else {
LOG.debug("There is a connection already for server " + sid);
}
}
进入以后就看见一个判断if (!connectedToPeer(sid)) 如果真的没有连接,才回去重新连接。然后获取地址建立socket
,初始化initiateConnection(sock, sid)
,点进去:
public void initiateConnection(final Socket sock, final Long sid) {
try {
//开始链接
startConnection(sock, sid);
} catch (IOException e) {
LOG.error("Exception while connecting, id: {}, addr: {}, closing learner connection",
new Object[] { sid, sock.getRemoteSocketAddress() }, e);
closeSocket(sock);
return;
}
}
只有一个开始连接的方法startConnection(sock, sid)
,这里面就是一样的逻辑了if (sid > this.mySid)
连接的控制,新建SendWorker
和RecvWorker
然后启动等等,这样就完成了connectAll()
。
所以我们就清楚了,if(n == null)
这一块的逻辑其实就是一个保险机制,用来检验投票的发送和连接成功与否用的。那么验证过了n!=null
就是选举的核心逻辑了,到这里说明收到了外来的选票而且合法。服务器就可以放心大胆的进行领导者选举,选出一个合格的Leader
了。
总结
到这里,我们先停一停,后面还有不少的逻辑,放到一篇里内容太过冗长了。那么我们总结下目前Zookeeper准备选举前干了什么事情。首先当然还是要开启socket
,其次启动了Send
和Receive
线程构成了两个投票收发链,然后投票给自己,接着验证选票。如果发现没有收到选票,就重新发送。如果发现选票没有发出去,就检查连接,重新尝试连接集群里的各个机器。那么本章就到此结束,我们下一篇接着探索选举的过程,谢谢大家。