前言
ZooKeeper对Zab协议的实现有自己的主备模型,即Leader和learner(Observer + Follower),有如下几种情况需要进行领导者的选举工作
情形1: 集群在启动的过程中,需要选举Leader
情形2: 集群正常启动后,leader因故障挂掉了,需要选举Leader
情形3: 集群中的Follower数量不足以通过半数检验,Leader会挂掉自己,选举新leader
情景4: 集群正常运行,新增加1个Follower
本篇博文,从这四个方面进行源码的追踪阅读
程序入口
QuorumPeer.java相当于集群中的每一个节点server,在它的start()方法中,完成当前节点的启动工作,源码如下:
// todo 进入了 QuorumPeer(意为仲裁人数)类中,可以把这个类理解成集群中的某一个点
@Override
public synchronized void start() {
// todo 从磁盘中加载数据到内存中
loadDataBase();
// todo 启动上下文的这个工厂,他是个线程类, 接受客户端的请求
cnxnFactory.start();
// todo 开启leader的选举工作
startLeaderElection();
// todo 确定服务器的角色, 启动的就是当前类的run方法在900行
super.start();
}
第一个loadDataBase();目的是将数据从集群中恢复到内存中
第二个cnxnFactory.start();是当前的节点可以接受来自客户端(java代码,或者控制台)发送过来的连接请求
第三个startLeaderElection();开启leader的选举工作, 但是其实他是初始化了一系列的辅助类,用来辅助leader的选举,并非真正在选举
当前类,quorumPeer继承了ZKThread,它本身就是一个线程类,super.start();就是启动它的run方法,在他的Run方法中有一个while循环,一开始在程序启动的阶段,所有的节点的默认值都是Looking,于是会进入这个分支中,在这个分之中会进行真正的leader选举工作
小结
从程序的入口介绍中,可以看出本篇文章在会着重看下startLeaderElection();做了哪些工作? 以及在looking分支中如何选举leader
情形1:集群在启动的过程中,选举新Leader
进入startLeaderElection();方法,源码如下, 他主要做了两件事
对本类QuorumPeer.java维护的变量(volatile private Vote currentVote;)初始化
createElectionAlgorithm()创建一个leader选举的方法
其实到现在,就剩下一个算法没过期了,就是fastLeaderElection
// TODO 开启投票选举Leader的工作
synchronized public void startLeaderElection() {
try {
// todo 创建了一个封装了投票结果对象 包含myid 最大的zxid 第几轮Leader
// todo 先投票给自己
// todo 跟进它的构造函数
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
for (QuorumServer p : getView().values()) {
if (p.id == myid) {
myQuorumAddr = p.addr;
break;
}
}
if (myQuorumAddr == null) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
// todo 创建一个领导者选举的算法,这个算法还剩下一个唯一的实现 快速选举
this.electionAlg = createElectionAlgorithm(electionType);
}
继续跟进createElectionAlgorithm(electionType), 在这个方法中做了如下三件大事
创建了QuorumCnxnManager
创建Listenner
创建FastLeaderElection
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
// todo 创建CnxnManager 上下文的管理器
qcm = createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
// todo 在这里将listener 开启
listener.start();
// todo 实例化领导者选举的算法
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
准备选举环境
QuorumManager
上图是QuorumCnxManager的类图,看一下,它有6个内部类, 其中的除了Message外其他都是可以单独运行的线程类
这个类有着举足轻重的作用,它是集群中全体节点共享辅助类, 那到底有什么作用呢? 我不卖关子直接说,因为leader的选举是通过投票决议出来的,既然要相互投票,那集群中的各个点就得两两之间建立连接,这个QuorumCnxManager就负责维护集群中的各个点的通信
它维护了两种队列,源码在下面,第一个队列被存入了ConcurrentHashMap中 key就是节点的myid(或者说是serverId),值可以理解成存储它往其他服务器发送投票的队列
第二个队列是收到的其他服务器发送过来的msg
// todo key=serverId(myid) value = 保存着当前服务器向其他服务器发送消息的队列
final ConcurrentHashMap> queueSendMap;
// todo 接收到的所有数据都在这个队列中
public final ArrayBlockingQueue recvQueue;
如上图是手绘的QuorumCnxManager.java的体系图,最直观的可以看到它内部的三条线程类,那三条线程类的run()方法又分别做了什么呢?
SendWorker的run(), 可以看到它根据sid取出了当前节点对应的队列,然后将队列中的数据往外发送
public void run() {
threadCnt.incrementAndGet();
try {
ArrayBlockingQueue bq = queueSendMap.get(sid);
if (bq == null || isSendQueueEmpty(bq)) {
ByteBuffer b = lastMessageSent.get(sid);
if (b != null) {
LOG.debug("Attempting to send lastMessage to sid=" + sid);
send(b);
}
}
} catch (IOException e) {
LOG.error("Failed to send last message. Shutting down thread.", e);
this.finish();
}
try {
while (running && !shutdown && sock != null) {
ByteBuffer b = null;
try {
// todo 取出任务所在的队列
ArrayBlockingQueue bq = queueSendMap.get(sid);
if (bq != null) {