目录说明
- 节点状态说明
- Leader选举流程
- Leader选举源码解析
- Leader流程总述
节点状态说明
在ZK集群中服务器有participant和observer两种类型(区别在于前者参与选举而后者不参与),Leader、Follower、Observer三种角色及以下四种状态
- LOOKING:寻找状态,表明当前集群环境中没有Leader,该状态下节点间将发起投票选举。
- FOLLOWING:跟随者状态。表明当前节点为Follower角色
- LEADING:领导者状态,表明当前节点为Leader角色
- OBSERVING:观察者状态,表明当前节点为observer角色
Leader选举流程
前面的博客中说过在ZK集群中,所有事务请求都由Leader进行处理,然后发起事务提议,进行数据同步。所以关于Leader的选举是Zookeeper中保证分布式数据一致性的关键所在,通常有两种情况会触发选举:集群启动、Leader宕机,虽然触发场景不一样,但选举的流程基本一致:
0)变更状态:宕机场景下,所有Follower会先将自己的状态变更为LOOKING,以便开始新一轮的选举。
1)每个Server广播自己的投票:每个Server默认都会投自己当Leader,并且将该投票广播到整个集群(包括自己)。投票信息中包含自己的myid、最大事务ID、当前的选举轮次等信息,具体的投票结构体如下
id | sid(也就是myid) |
zxid | 最大事务ID |
electionEpoch | 当前的选举轮次 |
peerEpoch | 被投票的Leader的选举轮次,在首次投票时与上面的electionEpoch应该保持一致 |
state | 节点状态 |
2)接收其它Server的投票:判断投票的有效性,如对比投票轮次、是否来自LOOKING服务器、Observer不允许参与投票等。
3)投票PK:源码如下,由于epoch是zxid的高32位,所以可以理解成先比较zxid,zxid大的胜出,相等的情况下sid大的胜出。
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
...
return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}
4)更新投票并广播:更新自己的投票,然后再次广播最新投票。
5)统计投票:每收到一次投票,服务器都会进行统计,判断是否已经有过半机器支持该投票。在已过半支持的情况下,仍然每200ms<最多200ms>检查一次投票接收队列,如果没有新投票到来,则认定该投票为Leader。如果有新的更大的投票到来,则将新的投票重新放入投票接收队列,然后终止本轮选举,进行下一轮的选举(选举轮次+1),在下一轮选举中再与新的投票进行比较....依次循环,直到选出Leader。如果新的投票比已过半支持投票小,则忽略该投票,继续下一次检查。这样一个延迟检查,更好的处理了启动延迟和网络不稳定这种情况。判断投票是否结束的相关源码如下
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
....
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
// 确认所提议的leader是否有变化
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
//有变化,且新投票比所提议的Leader要大,则重新入队
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
if (n == null) {
//没有新投票到来,更新自己的状态
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
//并将endVote设置成自己的currVote
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock.get(),
proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break; //退出循环
}
6)改变服务器状态:如上源码,在确定了Leader后,每个Server将更新自己的状态,要么为Leading或FOLLOWING
最后这里贴一张从启动到选举结束的内部通信流程图,如果对生产-消费者模式很熟练的话,对于下图及源码的理解应该很容易
Leader选举源码解析
在列出源码之前,先简单介绍一下与zookeeper选举相关的几个核心类
- QuorumPeerMain:Zookeeper选举入口类(主函数入口)
- QuorumPeer:选举真正的启动类,Thread的子类,负责恢复zk数据、创建QuorumCnxManager对象、选举对象、启动Leader选举等
- QuorumCnxManager:负责选举网络通信的I/O
- FastLeaderElection:选举类
QuorumPeerMain
篇幅关系源码暂略,在QuorumPeerMain中主要是读取zoo.cfg配置,封装成QuorunPeerConfig对象,根据配置创建一个ServerCnxnFactory对象管理客户端连接(默认监听2181端口),关于连接的处理,这里是典型的NIO应用,本篇暂不讨论。最终构造一个QuorumPeer对象并启动(QuorunPeer是Thread的子类)。
QuorumPeer
在QuorumPeer的start()方法中将会产生多个流程分支并发运行,主要有加载ZK数据、创建QuorumCnxManager对象并启动监听、创建选举对象并启动读写线程、开始选举,这里我们对与选举相关后三条分支作一个详细的介绍,相关源码整理如下
public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider {
private InetSocketAddress myQuorumAddr; //当前服务器地址
volatile private Vote currentVote; //当前投票
private ZKDatabase zkDb; //DB
private int electionType; //采用的选举算法,默认为3
Election electionAlg; //选举算法对应的选举对象
private long currentEpoch = -1; //当前选举周期,每选举一次该值递增
private long myid; //myid
public enum ServerState { //节点状态
LOOKING, FOLLOWING, LEADING, OBSERVING;
}
public enum LearnerType { //除Leader外,其余节点类型,要么是参与者,要么是观察者
PARTICIPANT, OBSERVER;
}
//重写Thread的start()方法
@Override
public synchronized void start() {
loadDataBase(); //加载数据
cnxnFactory.start();
startLeaderElection(); //开始选举
super.start(); //真正启动线程,调起run()
}
synchronized public void startLeaderElection() {
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); //设置投票
...
this.electionAlg = createElectionAlgorithm(electionType); //创建选举对象
}
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
switch (electionAlgorithm) {
...
case 3:
qcm = createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
listener.start(); //启动监听:监听其它服务器过来的连接
le = new FastLeaderElection(this, qcm); //创建选举对象
}
break;...
}
return le;
}
@Override
public void run() {
//...JMS注册相关代码,略...
try {
while (running) {
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
try {
setBCVote(null);
setCurrentVote(makeLEStrategy().lookForLeader());
}...
}
}
}...
}
}
分支一:QuorumCnxManager
从上方源码中可以看出QuorumCnxManager对象的创建、监听的启动、FastLeaderElection选举对象的创建都位于createElectionAlgorithm()方法中。从下方QuorumCnxManager源码中可以看出相出QuorumCnxManager维护的一系列成员:
- QuorumCnxManager.Listener:负责监听选举端口(默认3888),为了避免重复建立TCP连接,只允许SID大的节点向SID小的节点建立连接;连接建立完成后,针对每个连接创建对应的RecvWoeker与SendWorker。
- QuorumCnxManager$RecvWorker:负责从Socket中接收数据,转存到recvQueue队列中
- QuorumCnxManager$SendWorker:负责从queueSendMap中读取数据,然后通过Socket发出
- QuorumCnxManager.senderWorkerMap:该Map负责维护sid与其Sendworker映射关系,即Sid与其对应的Socket连接。
- QuorumCnxManager.queueSendMap:存储sid与其发送队列的映射
- QuorumCnxManager.recvQueue:用来存储从Socket接收到的数据。
public class QuorumCnxManager {
final long mySid;
final int socketTimeout;
final Map<Long, QuorumPeer.QuorumServer> view; //key=myid,value=服务器节点对象
//Mapping from Peer to Thread number
final ConcurrentHashMap<Long, SendWorker> senderWorkerMap; //k=myid,v=对应的Sender线程
final ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap; //k==myid,v=对应的发送队列,该队列则Sender线程消费
final ConcurrentHashMap<Long, ByteBuffer> lastMessageSent; //k=myid,v=最后发送的消息
//Reception queue
public final ArrayBlockingQueue<Message> recvQueue; //默认capacity为100
private final Object recvQLock = new Object(); //recvQueue锁对象
volatile boolean shutdown = false;
public final Listener listener;
public class Listener extends ZooKeeperThread {
volatile ServerSocket ss = null;
@Override
public void run() {
int numRetries = 0;
InetSocketAddress addr;
while((!shutdown) && (numRetries < 3)){
try {
ss = new ServerSocket();
...
addr = view.get(QuorumCnxManager.this.mySid).electionAddr; //默认监听3888端口上的连接
LOG.info("My election bind port: " + addr.toString());
ss.bind(addr);
while (!shutdown) {
Socket client = ss.accept(); //等待连接,传统的阻塞式IO
setSockOpts(client);
LOG.info("Received connection request "+ client.getRemoteSocketAddress());
receiveConnection(client); //连接处理
numRetries = 0;
}
} ...
}
}
...
}
public void receiveConnection(final Socket sock) {
DataInputStream din = null;
try {
din = new DataInputStream(new BufferedInputStream(sock.getInputStream()));
handleConnection(sock, din);
} ...
}
private void handleConnection(Socket sock, DataInputStream din) throws IOException {
Long sid = null;
try {
// Read server id
sid = din.readLong();
} ...
if (sid < this.mySid) {
//如果请求建立连接的服务器id < 当前节点myid,则断开连接,并由当前节点发起重新建立连接
SendWorker sw = senderWorkerMap.get(sid);
if (sw != null) {
sw.finish();
}
LOG.debug("Create new connection to server: " + sid);
closeSocket(sock);
connectOne(sid);
} else {
//否则启动SendWorker和RecvWorker两个线程,用来发送和接收信息
SendWorker sw = new SendWorker(sock, sid);
RecvWorker rw = new RecvWorker(sock, din, sid, sw);
sw.setRecv(rw);
SendWorker vsw = senderWorkerMap.get(sid);
if(vsw != null)
vsw.finish();
senderWorkerMap.put(sid, sw);
queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
sw.start();
rw.start();
return;
}
}
//该线程主要是当前节点对应的发送队列中读取数据,然后通过socket发出
class SendWorker extends ZooKeeperThread {
Long sid;
Socket sock;
RecvWorker recvWorker; //与SendWorker配对的RecvWorker
volatile boolean running = true;
DataOutputStream dout; // = new DataOutputStream(sock.getOutputStream());
...
synchronized void send(ByteBuffer b) throws IOException {
byte[] msgBytes = new byte[b.capacity()];
try {
b.position(0);
b.get(msgBytes);
} catch (BufferUnderflowException be) {
LOG.error("BufferUnderflowException ", be);
return;
}
dout.writeInt(b.capacity());
dout.write(b.array());
dout.flush();
}
@Override
public void run() {
threadCnt.incrementAndGet();
...
try {
while (running && !shutdown && sock != null) {
ByteBuffer b = null;
try {
ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
if (bq != null) { //消费队列
b = pollSendQueue(bq, 1000, TimeUnit.MILLISECONDS);
}...
if(b != null){
lastMessageSent.put(sid, b);
send(b);
}
} ...
}
}...
}
}
//该线程主要是从socket中读取数据,然后封装成Message对象,加入RecvQueue队列中
class RecvWorker extends ZooKeeperThread {
Long sid;
Socket sock;
volatile boolean running = true;
final DataInputStream din; // = new DataInputStream(new BufferedInputStream(sock.getInputStream()))
final SendWorker sw; //与Recvwoeker对应的SendWorker
...
@Override
public void run() {
threadCnt.incrementAndGet();
try {
while (running && !shutdown && sock != null) {
int length = din.readInt();
...
byte[] msgArray = new byte[length];
din.readFully(msgArray, 0, length);
ByteBuffer message = ByteBuffer.wrap(msgArray);
addToRecvQueue(new Message(message.duplicate(), sid));
}
}...
}
}
}
流程综述:
- Listener启动后,监听Leader选举端口(默认3888),等待集群中其它服务器建立连接。在收到连接后会交给receiveConnection()进行处理,为了避免两台机器之间重复创建TCP连接,在receiveConnection()中会对比申请连接的服务器SID与自身SID,如果发现自身的SID大,则断开连接,然后自己主动去和远程服务器建立连接。也就是说只允许SID大的节点向SID小的节点建立连接。
- 连接建立成功之后,Listener线程内部会启动两个线程:SendWorker与RecvWorker,分别负责消息的发送与接收。其中SendWorker线程负责从当前节点对应的消息发送队列中读取数据,然后通过socket发出;RecvWorker线程负责从从socket流中读取数据,然后封装成Message对象,加入RecvQueue消息接收队列中。
那么消息发送队列中的数据是何时写入的?而recvQueue消息接收队列的由谁消费?接下来看分支二
分支二:FastLeaderElection的创建
同样从下方FastLeaderElection创建的源码中可以看出相出FastLeaderElection维护的一系列成员
- FastLeaderElection.manager: 负责网络I/O的QuorumCnxManager通信对象
- FastLeaderElection$Messenge.messager:通信对象的包装类,内部维护了一对读写线程
- FastLeaderElection$Messenge$WorkerReceiver:选票接收器,该线程会不断的从QuorumCnxManager.RecvQueue队列中取出其它服务器发送过来的数据,并转换成一个Notification通知对象,然后转存到FastLeaderElection.recvqueue队列中。
- FastLeaderElection$Messenge$WorkerSender:选票发送器,该线程主要负责将FastLeaderElection.sendqueue中的数据取出,放到QuorumCnxManager.queueSendMap中当前节点对应的发送队列中。
- LinkedBlockingQueue<ToSend> sendqueue:选票发送队列
- LinkedBlockingQueue<Notification> recvqueue:选票接收队列
public class FastLeaderElection implements Election {
QuorumCnxManager manager; //负责网络I/O的通信对象
Messenger messenger; //通信对象的包装内,内部维护了一对读写线程
LinkedBlockingQueue<ToSend> sendqueue; //选票发送队列
LinkedBlockingQueue<Notification> recvqueue; //选票接收队列
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);
}
protected class Messenger
WorkerSender ws; //发送线程
WorkerReceiver wr; //接收线程
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();
}
//选票接收器,该线程会不断的从QuorumCnxManager.RecvQueue队列中取出其它服务器发送过来的数据,并转换成一个Notification通知对象,然后保存到FastLeaderElection.recvqueue队列中。
class WorkerReceiver extends ZooKeeperThread {
volatile boolean stop;
QuorumCnxManager manager;
public void run() {
Message response;
while (!stop) {
// Sleeps on receive
try{
response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
if(response == null) continue;
if(!validVoter(response.sid)){ //If it is from an observer
...
} else {
// 读取响应数据封装成一个Notification对象
Notification n = new Notification();
...
n.leader = response.buffer.getLong();
n.zxid = response.buffer.getLong();
n.electionEpoch = response.buffer.getLong();
n.state = ackstate;
n.sid = response.sid;
if(!backCompatibility){
n.peerEpoch = response.buffer.getLong();
} else {
n.peerEpoch = ZxidUtils.getEpochFromZxid(n.zxid);
}
if(LOG.isInfoEnabled()){
printNotification(n);
}
//如果当前节点状态也处于Looking状态
if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
recvqueue.offer(n);
//如果发送方节点状态也为Looking,则比较两个投票
//如果发送方其选举轮次<当前服务器,则忽略,并向其发起自己的投票
if((ackstate == QuorumPeer.ServerState.LOOKING)
&& (n.electionEpoch < logicalclock.get())){
Vote v = getVote();
ToSend notmsg = new ToSend(ToSend.mType.notification,
v.getId(),
v.getZxid(),
logicalclock.get(),
self.getPeerState(),
response.sid,
v.getPeerEpoch());
sendqueue.offer(notmsg);
}
} else {
//如果当前节点不是Looking,而发送方为Looking,则认为当前节点为Leader,则向其发送Leader信息。
Vote current = self.getCurrentVote();
if(ackstate == QuorumPeer.ServerState.LOOKING){
....
ToSend notmsg = new ToSend(
ToSend.mType.notification,
current.getId(),
current.getZxid(),
current.getElectionEpoch(),
self.getPeerState(),
response.sid,
current.getPeerEpoch());
sendqueue.offer(notmsg);
}
}
}
} ...
}
}
}
//sender线程主要负责将FastLeaderElection.sendqueue中的数据取出,放到QuorumCnxManager.queueSendMap中当前节点对应的发送队列中。
class WorkerSender extends ZooKeeperThread {
volatile boolean stop;
QuorumCnxManager manager;
...
public void run() {
while (!stop) {
try {
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
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);
}
}
...
}
}
流程综述:
- 创建FastLeaderElection()对象,其内部Manager对象在初始化的同时启动一个WorkerSender与WorkerReceiver线程。
- WorkerSender线程为选票发送器,负责将FastLeaderElection.sendqueue选票发送队列中的数据提取到QuorumCnxManager. queueSendMap中当前节点对应的消息发送队列中。所以它是生产者的同时也是消费者。
- WorkerReceiver为选票接收器,该线程不断的从QuorumCnxManager.recvQueue消息接收队列中取出其它服务器发送过来的数据,并转换成一个Notification通知对象,然后保存到FastLeaderElection.recvqueue选票接收队列中。
同样,那么FastLeaderElection.sendqueue选票发送队列的数据由谁生产?其FastLeaderElection.recvqueue选票接收队列又由谁消费?接下来我们看下剩下的一条业务线,QuorumPeer线程自己的任务
分支三:QuorumPeer.run()
public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider {
...
@Override
public void run() {
//...JMS注册相关代码,略...
try {
while (running) {
switch (getPeerState()) {
case LOOKING: //如果Looking状态,则发起投票
LOG.info("LOOKING");
try {
setBCVote(null);
setCurrentVote(makeLEStrategy().lookForLeader());
}...
}
}
}...
}
}
public class FastLeaderElection implements Election {
public Vote lookForLeader() throws InterruptedException {
...
try {
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
logicalclock.incrementAndGet();
//设置投票信息,投自己
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
LOG.info("New election. My id = " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
//广播自己的投票
sendNotifications();
//在没有选出Leader之前,将一直循环
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)) {
switch (n.state) {
case LOOKING:
// 接收到的节点epoch大于logicalclock,则表示已经远程节点已经开始新一轮选举了
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));
//本轮次中,是否对于某个节点的投票数>server.size/2
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
// Verify if there is any change in the proposed leader
//如果超过半数投票,仍然不会马上确定其为最终的Leader,如果在200ms内没有更适合的leader的话,才算选举成功。为什么这么设计?比如同时启动1,2,3三台机器,但是当3启动完成后,可能12已经全启动并完成了选举,但其实3应该成为Leader,这里增加200ms的延迟,就是为了解决类似延迟的情况。
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
//更新节点状态
if (n == null) {
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
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:
/*
* Consider all notifications from the same epoch
* together.
*/
if(n.electionEpoch == logicalclock.get()){
recvset.put(n.sid, new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch));
if(ooePredicate(recvset, outofelection, n)) {
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
/*
* Before joining an established ensemble, verify
* a majority is following the same leader.
*/
outofelection.put(n.sid, new Vote(n.version,
n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch,
n.state));
if(ooePredicate(outofelection, outofelection, n)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
LOG.warn("Notification state unrecognized: {} (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 {
try {
if(self.jmxLeaderElectionBean != null){
MBeanRegistry.getInstance().unregister(
self.jmxLeaderElectionBean);
}
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
self.jmxLeaderElectionBean = null;
LOG.debug("Number of connection processing threads: {}",
manager.getConnectionThreadCount());
}
}
//广播投票信息(包括发送给自己),注意这里会针对每个节点创建一个ToSend对象,而不是广播一个ToSend,原因在于目标主机的ID是封装在ToSend中的。
private void sendNotifications() {
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch);
sendqueue.offer(notmsg);
}
}
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
...
//如果投票节点的选举轮次>当前节点轮次,或者投票节点的Zxid>curZxid,事务ID的情况下比较Sid,由于epoch是zxid的高32位,所以也就是说优先选举zxid最大的节点为Leader,zxid相同的情况下,选举sid最大的节点。
return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}
}
从上方源码可以看出,发起投票是在QuorumPeer.run()中,该分支的主要流程是发起投票->接收投票->投票PK->投票归档->投票统计->根据统计结果来决定是继续投票还是投票结束。下面对投票流程从源码层面做一个总述
投票流程总述
- 选举启动后,首先创建一个QuorumCnxManager网络通信对象,并启动其监听线程Listener:负责监听选举端口(默认3888),针对每个连接创建对应的RecvWoeker与SendWorker。
- 创建选举对象,并启动其WorkerReceiver选票接收器及WorkerSender选票发送器,负责接收和发送投票信息。
- 分支三开始投票,在QuorumPeer.run()的updateProposal()方法将投票对象设为自己,并广播自己的投票,通过sendNotifications(),将投票信息及sid封装成一个ToSend对象,注意这里针对每一个集群节点都会生成一个ToSend对象(而不是一个投票发给多个节点),放入FastLeaderElection.sendqueue选票发送队列中。
- 分支二中FastLeaderElection的WorkerSender选票发送器将消费该队列,将其中的ToSend对象根据sid进行拆分,生成Message对象,再放入QuorumCnxManager. queueSendMap中对应节点的对应消息发送队列中。
- 分支一的QuorumCnxManager.SendWorker线程将消费消息发送队列,通过socket发送出去。
- 分支三广播投票后,将等待其它节点投票信息的到来,如果一定时间内未收到投票,则检测连接。连接正常的情况下继续广播一次自己的投票,否则尝试建立连接。
- 若接收到的投票信息,则进行投票PK。
4.1)先比较选举轮次,若远程节点的选举轮次 > 当前节点的选举轮次,则表示远程节点已经开始新一轮选举了,自己的选举轮次是落后的,则更新自己的选举轮次,并清空选票统计列表,然后拿自己与接收到的投票PK。
4.2)PK之后 ,更新自己投票为胜出方,再次广播 - 选票归档,无论是否变更投票,都将接收到的投票进行归档,以便进行选举统计
- 选票统计,如果归档列表中某投票存在的数量大于server.size/2,则每200ms<最多200ms>检查一次投票接收队列,如果没有新投票到来,则认定该投票为Leader。如果有新的更大的投票到来,则将新的投票重新放入投票接收队列,然后终止本轮选举,进行下一轮的选举(选举轮次+1),在下一轮选举中再与新的投票进行比较....依次循环,直到选出Leader。如果新的投票比已过半支持投票小,则忽略该投票,继续下一次检查。这样一个延迟检查,更好的处理了启动延迟和网络不稳定这种情况。
- 状态变更,选举结束后,变更自己状态,并记录Leader信息。