zookeeper的所有的事务请求都是leader来完成的,而当leader宕机后会进行leader的选举操作,leader选举是zookeeper最重要的技术之一,也是zookeeper实现的zab协议的最开始的阶段。zab协议在Zookeeper's atomic broadcast protocol:Therory and practice这篇论文中有详细介绍。
重要术语
- zxid:这个值是表示的是zookeeper中的事务日志Id,不过在下面的选举中都是指的事务日志的最后一条日志的zxid
- electionEpoch:这表示的是选举的周期
- epoch:这表示的是leader的周期,这个值以前是直接从zxid中获取,之前是zxid的前32位。
代码实现
zookeeper的服务器主要是在looking,observing,following,leading这四个状态进行切换,当状态在looking状态时则表示在进行leader选举,其他状态在异常时也会进入这个状态,下面代码
FastLeaderElection
zookeeper实现了多种选举策略,不过现在基本上只使用FastLeaderElection这一种策略,而这个FastLeaderElection的主要选举逻辑在其lookForLeader方法中,在具体介绍这个方法之前,先看一下其相关的其他的模块的逻辑。
选举的网络IO
下面图展示了FastLeaderElection的选举网路IO,FastLeaderElection获取其他节点的选票信息和发送选票信息则是通过sendqueue和recvqueue这两个队列来完成的,然后对应的WorkerSender会将消息分发到各个节点对应的发送线程中去,而WorkerReciever则是对各个节点发回的消息进行解析。对于当前节点到其他的所有节点的tcp连接都会分配两个线程,一个sendWorker和一个recvWorker,分别处理将消息写入IO流中以及从IO流中读取消息的操作。对于集群中的任意的两个节点都会有一条连接,这条连接是sid的连向sid小的节点。
消息数据结构
下面图展示了选举中各个模块主要维护的数据
首先是FastLeaderElection维护的:
- proposedLeader:当前的节点投票的leder
- proposedZxid: 当前节点投票的leader对应的zxid
- proposedEpoch:当前节点投票的leader对应的epoch
- logicalclock:表示的是当前节点的选举周期
ToSend和Notification中的数据差不多,都是节点的信息交换的数据:
- version:这表示的是协议的版本号,主要是对于滚动升级时和低版本的协议的兼容
- leader:这个节点投票的leader
- zxid:这个节点投票的leader对应的zxid
- electionEpoch:这个节点投票的leader对应的选举周期
- state:这个节点所处的状态
- peerEpoch: 这个节点投票的leader所对应的epoch
- qv:这个时当前节点所知道的集群的信息(主要是动态配置加入的,后续分享再分析)
Vote的数据主要也是这些数据
- id: 投票对应的leader的sid
- zxid:这个投票的leader对应的zxid
- electionEpoch:这个投票的leader对应的选举周期
- state:这个节点所处的状态
- peerEpoch: 这个节点投票的leader所对应的epoch
lookForLeader
下面主要来分析一下lookForLeader这个方法,其主要逻辑是从recvQueue中获取其他节点的选票信息,让后来和其他的节点的选票进行比较,从而来决定是否变更自己的选票,而再变更自己选票时则也会将自己变更后的选票广播给其它的节点,当当前的节点收到了对同一个节点的过半选票后,则会将对应的节点设置为候选leader,并退出选举阶段,进行后续的恢复阶段。
对于选票的比较方式则主要时通过epoch,zxid和sid来进行比较的,这种比较方式保证了最终选出的leader包含了所有的commit了的事务。
//这里的选举主要是服务器之间交换数据,这其中可能会有节点最终选出的leader不是最终选出的leader
//但是其选出的候选leader最终会由于没有过半的follower连接而退出leader,重新开始选举
//但是这会等待initLimit*tickTime时间
public Vote lookForLeader() throws InterruptedException {
//jmx相关
self.start_fle = Time.currentElapsedTime();
try {
Map<Long, Vote> recvset = new HashMap<Long, Vote>();//当前logicalClock下的其他的节点的投票
//此处存储的是对于没有logicalClock验证的消息,因为对于在leading和following状态的节点
//其logicalClock是不会变的,但是对于断线的新的节点,其logicalClock会加1,从而比稳定的leader和follower节点的
//electionEpoch要大,故而如果加上了logicalClock的判断,则其可能一直都无法选举成功
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = minNotificationInterval;
//这里的logicalclock从最开始的版本就存在了,那时选举时
synchronized (this) {
logicalclock.incrementAndGet();//增加electionEpoch的周期数量
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());//最开始的投票时投给自己
}
sendNotifications();//广播消息给其他的节点
SyncedLearnerTracker voteSet;
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {//交换notification知道发现leader
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);//从recvqueue中获取notification
if (n == null) {
if (manager.haveDelivered()) {//至少有一个节点已经被发送消息了
sendNotifications();//发送消息给配置中的集群的所有的节点
} else {
manager.connectAll();//一条消息都没发送出去,尝试重新连接所有的连接
}
//这里等待时间指数上升 这段代码是在https://issues.apache.org/jira/browse/ZOOKEEPER-230加入的
int tmpTimeOut = notTimeout * 2;
notTimeout = Math.min(tmpTimeOut, maxNotificationInterval);
} else if (validVoter(n.sid) && validVoter(n.leader)) {
switch (n.state) {
case LOOKING:
//这里-1表示的是当前的节点可能丢失了持久化的数据,不应该让其参与选举
//这里的代码时https://issues.apache.org/jira/browse/ZOOKEEPER-261中加入的
if (getInitLastLoggedZxid() == -1) {
break;
}
if (n.zxid == -1) {
break;
}
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);
} else {//当前节点的
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
sendNotifications();//广播节点给其他节点
} else if (n.electionEpoch < logicalclock.get()) {
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {//选举周期相同则看其他节点的选票是否比当前节点的选票要更符合,从而更新当前节点的选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));//更新对应节点的选票信息
voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));
if (voteSet.hasAllQuorums()) {//检查这个选票是否过半
while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
//等待当前收到的其他信息的节点的队列finalizeWait时间来保证其他节点的选票不会影响当前结果
//个人认为主要是避免这样的情况,A,B,C,B发送节点给A,然后A将自己的选票改为B,此时A节点的选票中B节点已
//经有两张选票,此时A中认为的leader是B,但是对于B,它在收到A的选票改变之前收到了C的选票,因此其将自己
//的选票改位C,并广播出去,然后C收到B的选票为C,此时B和C都认为C为leader。
//如果不处理后续的选票,则A认为的leader为B,B和C认为的leader则为C.
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
recvqueue.put(n);
break;
}
}
if (n == null) {//此处表示等待了finalizeWait时间没有其他节点来影响选举结果,则退出选举并返回选举结果
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;
}
}
//这里没有考虑logicalClock,因为对于leading和following状态的节点其对应的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;
default:
LOG.warn("Notification state unrecoginized: {} (n.state), {}(n.sid)", n.state, n.sid);
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
}
}