在集群启动过程代码分析的时候我们看到,有一个群首选举的过程,接下来我们跟着源码来探究其选举流程和原则。
选举主流程
上一篇文章中我们简要的介绍了集群的启动过程,主要有两步涉及的群主选举
public synchronized void start() {
loadDataBase();
startServerCnxnFactory();
adminServer.start();
startLeaderElection(); // 开启主节点选举
super.start(); // 本线程启动
}
再深入代码发现FastLeaderElection里面也是多个Thread类,所以这两个步骤很多过程是并行执行的。我们先看第一步中,串行部分:
public synchronized void startLeaderElection() {
try {
if (getPeerState() == ServerState.LOOKING) {
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); // 第一启动,创建一个选票。
}
} catch (IOException e) {
....// 省略
}
// 创建选举算法,其实应该是开启选举(多个线程配合构成了一个后台选举流程)
this.electionAlg = createElectionAlgorithm(electionType);
}
protected Election createElectionAlgorithm(int electionAlgorithm) {
Election le = null;
//TODO: use a factory rather than a switch
// 从上面看,目前只有一种该选举算法,其他的都是待开发状态的。
switch (electionAlgorithm) {
case 1:
throw new UnsupportedOperationException("Election Algorithm 1 is not supported.");
case 2:
throw new UnsupportedOperationException("Election Algorithm 2 is not supported.");
case 3:
QuorumCnxManager qcm = createCnxnManager(); // 专门用于选举通信的管理器
QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm); // 使用原子类,保证线程安全,并获取之前管理器
if (oldQcm != null) { // 如果有旧的,则清除之前的信息
LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");
oldQcm.halt();
}
QuorumCnxManager.Listener listener = qcm.listener;
if (listener != null) {
listener.start(); // 监听线程
FastLeaderElection fle = new FastLeaderElection(this, qcm);
fle.start(); // 选举的主要组件,启动后,主线程也同步往下走
le = fle; // 设置选举算法为fle FastLeaderElection
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
创建群主选举的通信管理器,并开启选举通信,listener.start()
启动选举也就是FastLeaderElection.start()
也就是最开始的QuorumPeer.start();
选举的通信管理我们单独作为了一个章节来讲,有兴趣的可以看源码解析三、选举中的通信.我们这里只讲FastLeaderElection启动过程、QuorumPeer.run方法
FastLeaderElection
主要属性如下:
QuorumCnxManager manager;// 通信管理器
private SyncedLearnerTracker leadingVoteSet; // 投票
LinkedBlockingQueue sendqueue; // 待发送的消息队列,也就是自己的选票
LinkedBlockingQueue recvqueue; //收到的消息,也就是收到的投票信息
QuorumPeer self; // 自引用
Messenger messenger; // 消息管理器
AtomicLong logicalclock = new AtomicLong();
long proposedLeader; // 投票的sid
long proposedZxid; // 投票的zxid
long proposedEpoch; // 投票的epoch
starter 初始化,票据和自己的zxid都为-1 ,初始化发送和接收队列。
private void starter(QuorumPeer self, QuorumCnxManager manager) {
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
sendqueue = new LinkedBlockingQueue();
recvqueue = new LinkedBlockingQueue();
this.messenger = new Messenger(manager);
}
/**
* 启动messger线程,其实就是启动发送和接收线程,我们稍后详解。
*/
public void start() {
this.messenger.start();
}
Messenger
主要工作的类还是message,这是一个多线程实现的消息管理器
protected class Messenger {
WorkerSender ws; // 发送消息
WorkerReceiver wr; // 接受消息
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
this.wsThread = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]");
this.wsThread.setDaemon(true);
this.wr = new WorkerReceiver(manager);
this.wrThread = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]");
this.wrThread.setDaemon(true);
}
void start() {
this.wsThread.start();
this.wrThread.start();
}
}
WorkerReceiver
WorkerReceiver主要功能都在run方法
public void run() {
Message response;
while (!stop) {
// Sleeps on receive
try {
// 从消息通信管理器中的recvQueue队列拿数据,也就是其他节点发过来的消息。可以看选举通信那一章节。
response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
if (response == null) {
continue;
}
Notification n = new Notification();
int rstate = response.buffer.getInt();
long rleader = response.buffer.getLong(); // 省略部分内容
int version = 0x0;
QuorumVerifier rqv = null;
if (!validVoter(response.sid)) {
sendqueue.offer(notmsg); // 如果sid不合法返回一个消息
} else {
QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING;
switch (rstate) {/*设置state,如LOOKING*/}
// 组装n Notification 省略部分内容
n.leader = rleader;
// 如果还在LOOKING的状态,则将这条选举信息方队消息队列recvqueue
if (self.getPeerState() == QuorumPeer.ServerState.LOOKING) {
recvqueue.offer(n);
// 如果发现对端发来消息。epoch比当前节点小,则发送我们的投票过去。
// 尝试让它更新投票(此时与getCurrentVote是不同的,还没有这个值)
if ((ackstate == QuorumPeer.ServerState.LOOKING)&& (n.electionEpoch < logicalclock.get())) {
Vote v = getVote();
QuorumVerifier qv = self.getQuorumVerifier();
ToSend notmsg = new ToSend(getVote());
sendqueue.offer(notmsg);
}
} else {
// 如果本节点应不再是LOOKING状态,且对端状态是LOOKING,则发送我们的投票信息
Vote current = self.getCurrentVote();
if (ackstate == QuorumPeer.ServerState.LOOKING) {
// 如果自身是Leader角色,判断在设定的时间段内(即initLimit*tickTime)
//是否有半数以上的Follower与Leader建立连接,假如没有半数以上的Follower
//与Leader建立连接的话Leader便会退出领导,并重新开始集群Leader的选举
if (self.leader != null) {
if (leadingVoteSet != null) {
self.leader.setLeadingVoteSet(leadingVoteSet);
leadingVoteSet = null;
}
self.leader.reportLookingSid(response.sid);
}
QuorumVerifier qv = self.getQuorumVerifier();
// 将拿到的消息放到消息队列sendqueue
ToSend notmsg = new ToSend(current);
sendqueue.offer(notmsg);
}
}
}
} catch (InterruptedException e) {
LOG.warn("Interrupted Exception while waiting for new message", e);
}
}
LOG.info("WorkerReceiver is down");
}
过程总结如下:
- 从选举通信消息管理器中拿到消息
- 组装成投票信息
- 验证消息的sid是否合法,不合法则返回给对方一个消息,将其放入sendqueue
- 判断自身的状态,如果是LOOKING,则将这条消息放到接受队列recvqueue
- 如果对端也是LOOKING状态,且对方的Epoch比我们的旧,则发送我们的投票,尝试让其更新,将其放入sendqueue
- 如果本节点应不是LOOKING状态,且对端状态是LOOKING,则发送我们的投票信息,将其放入sendqueue
如果本身是LEADING状态,有可能会触发自身状态的改变,这个留到后面讲
WorkerSender
相对于WorkerReceiver,WorkerSender更简单,就是将需要发送的消息从sendqueue放到manager里的queueSendMap准备发送。
// 拿到消息然后发送
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");
}
void process(ToSend m) {
ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), m.leader, m.zxid, m.electionEpoch, m.peerEpoch, m.configData);
// 调用的是通信管理器的方法。其实就是放到queueSendMap 对应sid的队列中
manager.toSend(m.sid, requestBuffer);
}
主线程中的群主选举
主要集中在run方法中,由于run方法是在节点运行的所有过程,因此我们这里只列举与群主选举有关的部分:
代码分析
@Override
public void run() {
updateThreadName();
LOG.debug("Starting quorum peer");
// 开启jmx
try {
// 主循环
while (running) {
switch (getPeerState()) {
case LOOKING: // 此时为群主选举状态
LOG.info("LOOKING");
ServerMetrics.getMetrics().LOOKING_COUNT.add(1);
if (Boolean.getBoolean("readonlymode.enabled")) {
// 只读模式下的server处理
} else {
try {
reconfigFlagClear();
// 初始值是false,后面leader选举失败会变为true
if (shuttingDownLE) {
shuttingDownLE = false;
startLeaderElection();
}
setCurrentVote(makeLEStrategy().lookForLeader()); // 核心选举算法
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
// 其他状态的处理
}
}
}
于是我们的目光转到setCurrentVote(makeLEStrategy().lookForLeader());
其中makeLEStrategy()是获取选举算法,我们在上一步createElectionAlgorithm已经设置了,也就是FastLeaderElection。
我们选举真正的核心是lookForLeader()方法:
/**
* 开启新一轮的选举,只要我们节点从其他状态转换成为LOOKING状态,就会触发该方法,并通知其他节点
*/
public Vote lookForLeader() throws InterruptedException {
self.start_fle = Time.currentElapsedTime();
try {
// 存储当前选举的选票,也就是逻辑时钟为logicalclock(当前时钟)的选票,当前参与者使用recvset来推断是否有大多数参与者投票支持它。
Map recvset = new HashMap();
//前一轮选票,以及当前群主选票(但是只保存FOLLOWING和LEADING的投票),参与者可以通过它来判断谁是leader
Map outofelection = new HashMap();
int notTimeout = minNotificationInterval;
synchronized (this) {
logicalclock.incrementAndGet();
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); //1. 初始化自己的选票
}
sendNotifications(); // 2. 广播自己的选票
SyncedLearnerTracker voteSet;
// 主循环,直到找到leader
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
// 3.从recvqueue拿到下一个选票
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
// 4. 如果没有消息,那么就再广播一次,这是因为有可能其他节点没有收到消息。
if (n == null) {
if (manager.haveDelivered()) { // 只要有为空的,说明发出去,但是没有成功,则再广播一遍。
sendNotifications();
} else {
manager.connectAll(); // 都不为空,说明连接可能创建失败或还没创建好
}
} // 5. 验证选票的正确性,一是验证当前候选人和上一轮候选人是否包括该sid,
//二是验证当前候选人和上一轮候选人是否包括该选票的推举人。
else if (validVoter(n.sid) && validVoter(n.leader)) {
switch (n.state) {
case LOOKING:
// 6. 如果选票的逻辑始终大于当前节点的逻辑时钟,则说明当前节点慢多个轮次。
//则重置当前节点的时钟,并清除之前的选票,然后广播更新后的选票
if (n.electionEpoch > logicalclock.get()) {
logicalclock.set(n.electionEpoch);
recvset.clear();
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch); // 用初始化的各个值来pk选票。
} else {
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
sendNotifications(); // 广播自己的选票
} else if (n.electionEpoch < logicalclock.get()) {
break;
// 相等的时候也要pk
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
// 7. 将选票放入recvset,注意每个服务器都只有一份,所以重发发没有副作用。
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 8. 统计选票(针对当前选票),也就是当前别选举的节点的投票。
voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));
// 如果该投票仲裁有效(也就是当前投票数超过一半)
if (voteSet.hasAllQuorums()) {
while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) { // 看是否有新的选票进来
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid,
// 并且选票比当前节点的票据更高,则更新
proposedEpoch)) {
recvqueue.put(n);
break;
}
}
// 如果没有更好的选票,则结束选票,选择当前所选举的Leader为Leader
if (n == null) {
setPeerState(proposedLeader, voteSet);
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING:
LOG.debug("Notification from observer: {}", n.sid);
break;
case FOLLOWING:
case LEADING:
// 如果轮次是一样的,则进行统计。
if (n.electionEpoch == logicalclock.get()) {
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() && checkLeader(recvset, n.leader, n.electionEpoch)) {
setPeerState(n.leader, voteSet);
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
// 如果不是同一轮次。或者还没有结束,将此选票放入outofelection
// 这里有点难以理解,不过我们可以看不同的地方。
// outofelection没有加入过LOOKING状态的选票,也就是所有LEADER和FOLLOW的选票,判断条件是这些节点总数的一半,而不包括looking状态的节点。
// 上面的判断针对的是electionEpoch等于目前的轮次情况,而这里解决的问题是不相等的情况下,还能更新logicalclock
// 因为如果其他节点都是follower和leader状态,那么我们永远无法更新logicalclock
outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() && checkLeader(outofelection, n.leader, n.electionEpoch)) {
synchronized (this) {
logicalclock.set(n.electionEpoch);
setPeerState(n.leader, voteSet);
}
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
}
} else {
}
}
return null;
} finally {
}
}
过程总结
看完代码,我们再来总结一下流程
- 初始化自己的选票
- 广播自己的选票
- 从recvqueue拿到下一个选票
- 如果消息为空,那么就再广播一次,这是因为有可能其他节点没有收到消息。
- 验证选票的正确性,一是验证当前候选人和上一轮候选人是否包括该sid,二是验证当前候选人和上一轮候选人是否包括该选票的推举人。
- 对选票做处理:
- LOOKING状态的选票
- 如果选票的逻辑始终大于当前节点的逻辑时钟,则说明当前节点慢多个轮次。则重置当前节点的时钟,并清除之前的选票,然后广播更新后的选票
- 将选票放入recvset,注意每个服务器都只有一份,所以重复发没有副作用。
- 统计选票(针对当前选票),也就是当前别选举的节点的投票。如果该投票仲裁有效(也就是当前投票数超过一半)看是否有新的选票进来,如果没有更好的选票,则结束选票,选择当前所选举的Leader
- FELLOW或者LEADER的选票
- 如果轮次是一样的,进行统计所有的选票,如果成功,则返回。
- 再对不在LOOKING状态的选票进行一次统计,并设置新的轮次。这个针对新节点加入已经稳定后的集群。
- 没有结束则继续获取选票。
- LOOKING状态的选票
[图片上传失败...(image-2d2cd3-1606390914706)]
一些参数介绍:
logicalclock(当前节点)=electionEpoch(当前的投票) // 选举的轮次,默认是0,每执行一次选举都会++
proposedEpoch(当前的节点)=peerEpoch(当前的投票) // 每次leader选举完成之后,都会选举出一个新的
peerEpoch,作为该Leader轮次,用来标记事务请求所属的轮次。currentEpoch 如果不是Leader的时候,一直是初始化的Epoch,如果是leader,成为leader之后的最新的轮次
投票pk
- 如果当前票据的Leader轮次大于当前节点的Leader轮次,则返回true;
- 如果当前票据和当前节点自己的票据Leader轮次相等,则比较事物id zxid,如果当前票据更大,则返回true;
- 如果当前票据和当前节点自己的票据Leader轮次和事物zxid都相等,则比较两者的sid 如果当前sid更大,返回true;
- 其他情况返回false,也就是不用更新选票。
流程总结
结合另一章节介绍的选举中的通信,我们可以梳理出,选举过程中所有组件的交互关系。一共三个组件QuorumCnxManager、FastLeaderElection、QuorumPeer。
都是作为一个或多个线程来并行处理,从而选出最后的主节点:
脑裂问题
之所以没有单独放到一个章节来讲,主要是因为zookeeper如今的版本已基本解决了脑裂的问题。
什么是脑裂
很多集群模式工作的组件比如zookeeper、elasticSearch,他们通常有一个master节点,负责调度或者说负责管理、通信,总之一定有一个中心节点(可以看作是集群的大脑)。在正常工作的情况下,一般没有问题,但如果出现通信异常、程序异常
的情况,就是出现两个master节点,这就是脑裂现象。
如何产生
- 集群部分通信断开
假设我们有六个节点,一个网段有三台,某一刻,两个网段的连接断开:
在如上的情况下,网络断开后,另一个网段就会有可能自主的产生一个新的master节点,这样就发生了脑裂
- master节点断开后又连上
假设我们有四个节点,在某个瞬间,master节点断开通信,或者延迟。那么这个时候其他节点会重新选举出一个新的master节点,等到master节点恢复正常,它会认为它依然是master节点,这个时候集群出现了两个,也就是脑裂。
解决方案
在当前版本中,zookeeper已经解决以上两个问题,如果依照上文看懂了zookeeper的源码,那应该就能给出答案。
- 使用仲裁机制Quorum,也就是master节点产生,必须获得半数以上的节点
如上网段二是三台机器,那么就完全没有办法选举出master节点,注意这里是半数以上>,而不是半数或者以上>=。 - 使用epch机制,每次选举都会累加
在上述情况二中,master断开,其他三个节点重新选举,选票为3超过了一半有效,且epoch+1,等之前的master重新恢复之后,它的epoch已经是旧的了,所以其他三个节点都不会认同,而是会触发其(旧master)更新,这样就防止了脑裂