1. 找到zk的入口类
在zkServer.sh里面我们看到这里是启动类,那么我们就好好的看看这个类
2. Zookeeper的入口类QuorumPeerMain的main方法
/**
* To start the replicated server specify the configuration file name on the command line.
* @param args path to the configfile
*/
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
/**
* 代码结构中:try中的是最重要的代码
* 如果看 exception,都是容错和异常处理的代码
* 如果 finnly 里面有代码,一定要看,一定很重要。大部分代码都是 stop() close()
*/
try {
/**
* TODO_MA 启动入口
*/
main.initializeAndRun(args);
} catch (IllegalArgumentException e) {
LOG.error("Invalid arguments, exiting abnormally", e);
LOG.info(USAGE);
System.err.println(USAGE);
System.exit(2);
} catch (ConfigException e) {
LOG.error("Invalid config, exiting abnormally", e);
System.err.println("Invalid config, exiting abnormally");
System.exit(2);
} catch (Exception e) {
LOG.error("Unexpected exception, exiting abnormally", e);
System.exit(1);
}
LOG.info("Exiting normally");
System.exit(0);
}
2.1 main.initializeAndRun(args)
/**
* TODO QuorumPeer 管理和代表了一台服务器,那么 QuorumPeerConfig 就一定是用来管理 配置的
* 启动的之后, zkserver 会去加载 zoo.cfg , 这里面的配置,都会读取进来保存在 QuorumPeerConfig
* @param args
* @throws ConfigException
* @throws IOException
*/
protected void initializeAndRun(String[] args) throws ConfigException, IOException {
// 解析配置,如果传入的是配置文件(参数只有一个),解析配置文件并初始化QuorumPeerConfig
// 集群模式,会解析配置参数
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) { // "zoo.cfg的路径"
// TODO 解析参数,通过 Properties 方式来进行。
config.parse(args[0]);
}
/**
* DatadirCleanupManager 线程,由于 ZooKeeper 的任何一个变更操作都产生事务,事务日志需要持久化到硬盘,
* 同时当写操作达到一定量或者一定时间间隔后,会对内存中的数据进行一次快照并写入到硬盘上的 snapshop 中,
* 快照为了缩短启动时加载数据的时间从而加快整个系统启动。
* 而随着运行时间的增长生成的 transaction log 和 snapshot 将越来越多,所以要定期清理,
* DatadirCleanupManager 就是启动一个 TimeTask 定时任务用于清理 DataDir 中的 snapshot 及对应的 transaction log。
*
* DatadirCleanupManager主要有两个参数:
* snapRetainCount:清理后保留的snapshot的个数,对应配置:autopurge.snapRetainCount,大于等于3,默认3
* purgeInterval:清理任务TimeTask执行周期,即几个小时清理一次,对应配置:autopurge.purgeInterval,单位:小时
*/
// Start and schedule the the purge task
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config.getDataDir(), config.getDataLogDir(),
config.getSnapRetainCount(), config.getPurgeInterval());
// 启动清理文件的线程
purgeMgr.start();
// zk 的数据会不断的从该内存快照到磁盘。快照文件越来越多!
// DatadirCleanupManager 就是用来定期清理多余的快照文件。 每次清理,最少会依然保存3个最近的快照
/**
* 根据配置中的 servers 数量判断是集群环境还是单机环境,如果单机环境以 standalone 模式运行
* 直接调用 ZooKeeperServerMain.main()方法,否则进入集群模式中。
* 集群命令格式:zkServer.sh start
*/
// 集群模式 zkServer.sh start servers.size = 4
if (args.length == 1 && config.servers.size() > 0) {
/**
* 集群模式,生产环境,毫无疑问,都是集群模式,所以重点关注集群模式
*/
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running in standalone mode");
// there is only server in the quorum -- run as standalone
/**
* 单机模式
*/
ZooKeeperServerMain.main(args);
}
}
2.2 runFromConfig
/**
* TODO_MA 集群模式启动
* @param config
* @throws IOException
*/
public void runFromConfig(QuorumPeerConfig config) throws IOException {
// 加载 日志 组件的
try {
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
/**
* QuorumPeer : 代表 一台服务器
*/
LOG.info("Starting quorum peer");
try {
/**
* 创建 ServerCnxnFactory 实例, ServerCnxnFactory 从名字就可以看出其是一个工厂类,负责管理 ServerCnxn,
* ServerCnxn 这个类代表了一个客户端与一个 server 的连接,每个客户端连接过来都会被封装成一个 ServerCnxn 实例用
* 来维护了服务器与客户端之间的 Socket 通道。
*
* 首先要有监听端口,客户端连接才能过来,ServerCnxnFactory.configure()方法的核心就是启动监听端口供客户端连接进来,
* 端口号由配置文件中clientPort属性进行配置,默认是2181
*
* ServerCnxnFactory 有 NIOServerCnxnFactory 和 NettyServerCnxnFactory 两种。
*
* NIOServerCnxnFactory = cnxnFactory
*
* NIOServerCnxnFactory 的作用:? 监听 2181 端口 如果有客户端发送请求过来,则由这个组件进行处理
* 如果有一个 客户端发送链接请求过来,NIOServerCnxnFactory 处理了之后,就会生成一个 ServerCnxn 来负责管理
*/
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns());
// 到这为止,只是初始化,并没有启动
/**
* 获取 QuorumPeer, zk的逻辑主线程,负责选举、投票
* QuorumPeer 是一个线程,注意它的 start 和 run 方法。
* 里头有一个内部类:QuorumServer
*
* QuorumPeer quorumPeer = getQuorumPeer();
* quorumPeer.setXXXX(XXXX);
*/
quorumPeer = getQuorumPeer();
quorumPeer.setQuorumPeers(config.getServers());
//FileTxnSnapLog主要用于snap和transaction log的IO工具类
quorumPeer.setTxnFactory(new FileTxnSnapLog(new File(config.getDataLogDir()), new File(config.getDataDir())));
//选举类型,用于确定选举算法
quorumPeer.setElectionType(config.getElectionAlg());
//设置myid
quorumPeer.setMyid(config.getServerId());
//设置心跳时间
quorumPeer.setTickTime(config.getTickTime());
//设置初始化同步时间
quorumPeer.setInitLimit(config.getInitLimit());
//设置节点间状态同步时间
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
//ServerCnxnFactory客户端请求管理工厂类 NIOServerCnxnFactory
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
quorumPeer.setClientPortAddress(config.getClientPortAddress());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
//ZKDatabase维护ZK在内存中的数据结构, ZKDatabase 就是用来管理 DataTree 的
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
//服务节点的角色类型:两种类型()
// zk 服务器的类型:PARTICIPANT, OBSERVER; 只是在 zoo.cfg 中用来进行配置的。
// PARTICIPANT: LEADER, FOLLOWER
// 只有服务器被配置成 PARTICIPANT 类型,才有资格 有选举权 和 被选举权
// learner: OBSERVER + FOLLOWER
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
// sets quorum sasl authentication configurations
quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
if (quorumPeer.isQuorumSaslAuthEnabled()) {
quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);
quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);
quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);
quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);
quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);
}
quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
quorumPeer.initialize();
/**
* 请注意:这个方法的调用完毕之后,会跳转到 QuorumPeer 的 run() 方法
* 启动主线程,QuorumPeer重写了Thread.start方法
*/
quorumPeer.start(); // 既有可能是线程。
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
3. quorumPeer.start()
@Override
public synchronized void start() {
/**
* 刚启动的时候,需要加载数据库, 从磁盘中的 数据快照文件 和 comitted logs 中恢复
* 涉及到的核心类是 ZKDatabase,并借助于 FileTxnSnapLog 工具类将 snap 和 transaction log
* 反序列化到内存中,最终构建出内存数据结构 DataTree。
*
* 总结:从事务日志目录dataLogDir和数据快照目录dataDir中恢复出DataTree数据
* 涉及到的核心类是ZKDatabase,并借助于FileTxnSnapLog工具类将snap和transaction log反序列化到内存中,最终构建出内存数据结构DataTree
*/
loadDataBase();
/**
* 服务连接:开启对客户端的连接端口,启动ServerCnxnFactory主线程
*
* ServerCnxnFactory的作用:构建reactor模型的EventLoop,Selector每隔1秒执行一次select方法来处理IO请求,
* 并分发到对应的代表该客户端的ServerCnxn中并利用doIO进行处理
*
* NIOServerCnxnFactory 负责 创建 ServerCnxn 负责和客户端进行数据读写通信的
*
* 调用这句代码的时候,跳转到 new ZooKeeperThread(this).start()
* this = cnxnFactory
* 跳转到自己的 run 方法
*/
cnxnFactory.start(); // 卡住了
/**
* 开始选举, 这儿并不是真正的选举,而是只是初始化选举需要的各种组件
*/
startLeaderElection();
/**
* TODO_MA 到此为止,一定要记得:
* 1、FastLeaderElection 的 WorkerReceiver 和 WorkerSender 都启动好了。
* 2、QuorumCnxManager 的 Listen监听 也启动好了,等待有其他 Server 的链接的话,就会创建成对的 RecvWorker 和 SendWorker
*/
/**
* 线程启动 跳转到 run() 方法。
* 记得,上面的代码执行完了之后,会跳转到 run() 方法。 因为 QuorumPeer 是一个线程!
* 启动QuorumPeer线程,在该线程中进行服务器状态的检查
*/
super.start();
}
4. loadDataBase();
File updating = new File(getTxnFactory().getSnapDir(), UPDATING_EPOCH_FILENAME);
try {
/**
* TODO_MA 加载数据 ZKDataBase zkDb
* 最重要的两件事:
* 1、真的加载了磁盘中的数据(快照文件数据 + CommitedLog)到内存(DataTree)
* 2、返回了当前这个 server节点中的 最大的 zxid
*/
zkDb.loadDataBase();
/**
* load the epochs 这是已得到的 lastProcessedZxid
* 从最新的 zxid 恢复 epoch 变量、zxid 64位,前32位是 epoch 的值,后 32 位是zxid
* 这段代码的目的是寻找到 acceptEpoch
*/
long lastProcessedZxid = zkDb.getDataTree().lastProcessedZxid;
long epochOfZxid = ZxidUtils.getEpochFromZxid(lastProcessedZxid);
try {
//从文件中读取当前的epoch
currentEpoch = readLongFromFile(CURRENT_EPOCH_FILENAME);
if (epochOfZxid > currentEpoch && updating.exists()) {
LOG.info("{} found. The server was terminated after " + "taking a snapshot but before updating current " + "epoch. Setting " +
"current epoch to {}.", UPDATING_EPOCH_FILENAME, epochOfZxid);
setCurrentEpoch(epochOfZxid);
if (!updating.delete()) {
throw new IOException("Failed to delete " + updating.toString());
}
}
} catch (FileNotFoundException e) {
// pick a reasonable epoch number
// this should only happen once when moving to a
// new code version
currentEpoch = epochOfZxid;
LOG.info(CURRENT_EPOCH_FILENAME + " not found! Creating with a reasonable default of {}. This should only happen when you are " +
"upgrading your installation", currentEpoch);
writeLongToFile(CURRENT_EPOCH_FILENAME, currentEpoch);
}
if (epochOfZxid > currentEpoch) {
throw new IOException("The current epoch, " + ZxidUtils
.zxidToString(currentEpoch) + ", is older than the last zxid, " + lastProcessedZxid);
}
try {
/**
* TODO 从配置文件读 acceptedEpoch
*/
acceptedEpoch = readLongFromFile(ACCEPTED_EPOCH_FILENAME);
} catch (FileNotFoundException e) {
// pick a reasonable epoch number
// this should only happen once when moving to a
// new code version
acceptedEpoch = epochOfZxid;
LOG.info(ACCEPTED_EPOCH_FILENAME + " not found! Creating with a reasonable default of {}. This should only happen when you are " +
"upgrading your installation", acceptedEpoch);
writeLongToFile(ACCEPTED_EPOCH_FILENAME, acceptedEpoch);
}
if (acceptedEpoch < currentEpoch) {
throw new IOException("The accepted epoch, " + ZxidUtils.zxidToString(acceptedEpoch) + " is less than the current epoch, " + ZxidUtils
.zxidToString(currentEpoch));
}
} catch (IOException ie) {
LOG.error("Unable to load database on disk", ie);
throw new RuntimeException("Unable to run quorum server ", ie);
}
}
ZXID是什么?
我们应该知道我们古代有一些朝代,或者我们想美国总统,例如美国总统有好几任,是吧
其实可以就是这么想,例如朝代就是康熙11年,就是康熙当皇帝的第11年,或者道光3年,类似这样的一种计数方式
1、epoch:就相当于康熙 道光 民国 ,就是某个server上位之后就会生成一个新的epoch
2、txid:每个server不一样那么txid就会从头开始即 例如 a01 a02 a03
zk集群里面每一次事务操作,都会生成一个全局唯一的zxid
1、选举
2、广播
4.1 loadDataBase
/**
* load the database from the disk onto memory and 从磁盘加载数据到内存
* also add the transactions to the committedlog in memory. 在内存中完成已提交日志的事务操作,得到下一个 zxid
* @return the last valid zxid on disk
* @throws IOException
*/
public long loadDataBase() throws IOException {
// 从磁盘加载数据, 从快照恢复数据
// FileSnapTxnLog snapLog
long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
// 已经初始化好
initialized = true;
// 返回 zxid
return zxid;
}
4.2 restore
public long restore(DataTree dt, Map<Long, Integer> sessions, PlayBackListener listener) throws IOException {
/**
* 第一个操作:先把 已经持久化到磁盘的 快照数据反序列化到内存中。
*/
snapLog.deserialize(dt, sessions);
/**
* 第二个操作:从 ComittedLogs 去加载数据。
* 在最后一次快照之后,也有一些新的 事务被提交了,这些事务的日志记录在日志文件。
* 冷启动,也需要加载这个数据
* 也会返回这些日志中的, maxZXID
*/
return fastForwardFromEdits(dt, sessions, listener);
}
这一行代码的意思就是从磁盘恢复日志到内存里面的DataTree里面,然后把最大的zxid返回就是最大事务id也就是最后执行的事务id,其实也就是WAL机制的冷启动问题
5. cnxnFactory.start()
@Override
public void start() {
// ensure thread is started once and only once
if (thread.getState() == Thread.State.NEW) {
/**
* TODO_MA 去找 ZooKeeperThread 的 run 方法!
* 事实上,去找 当前这个类的 run() 方法。因为 ZooKeeperThread 这个类的第一个参数是 this
*/
thread.start();
// 事实上,跳转到 当前这个 工厂类的 run 方法
}
}
5.1 NIOServerCnxnFactory
/**
* TODO_MA 等待用户的请求过来,进行处理。
*/
public void run() {
// ServerSocket
while (!ss.socket().isClosed()) {
try {
/**
* TODO 等待用户连接
* NIO 的 核心API:
* Buffer
* Channel(InputStream + OutputStream Buffer)
* Selector(从所有的channle找找 IO就绪的 chanel )
* Reactor
*/
selector.select(1000); // 阻塞方法
Set<SelectionKey> selected;
synchronized (this) {
// 拿到的结果
selected = selector.selectedKeys();
}
ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(selected);
Collections.shuffle(selectedList);
for (SelectionKey k : selectedList) {
/**
* TODO 建立连接部分 OP_ACCEPT
*/
if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();
InetAddress ia = sc.socket().getInetAddress();
int cnxncount = getClientCnxnCount(ia);
if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns) {
LOG.warn("Too many connections from " + ia + " - max is " + maxClientCnxns);
sc.close();
} else {
LOG.info("Accepted socket connection from " + sc.socket().getRemoteSocketAddress());
sc.configureBlocking(false);
SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
/**
* TODO_MA 处理连接请求
* 每个客户端请求过来,都会相应的创建一个 NIOServerCnxn 来进行处理
*/
NIOServerCnxn cnxn = createConnection(sc, sk);
sk.attach(cnxn);
addCnxn(cnxn);
}
/**
* TODO 读数据进行处理部分 OP_READ OP_WRITE
*/
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
NIOServerCnxn c = (NIOServerCnxn) k.attachment();
/**
* TODO_MA 读数据进行处理!
*/
c.doIO(k);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected ops in select " + k.readyOps());
}
}
}
selected.clear();
} catch (RuntimeException e) {
LOG.warn("Ignoring unexpected runtime exception", e);
} catch (Exception e) {
LOG.warn("Ignoring exception", e);
}
}
closeAll();
LOG.info("NIOServerCnxn factory exited run method");
}
这个时候并没有请求进来所以,这里是没有任何处理的,只是在不停的轮训
6. startLeaderElection
/**
* Leader 选举涉及到节点间的网络 IO,QuorumCnxManager 就是负责集群中各节点的网络 IO,
* QuorumCnxManager 包含一个内部类 Listener,Listener 是一个线程,这里启动 Listener 线程,
* 主要启动选举监听端口并处理连接进来的 Socket;
* FastLeaderElection 就是封装了具体选举算法的实现。
*/
synchronized public void startLeaderElection() {
try {
/**
* TODO 投票是投给自己的哟: 一个 vote 包含 myid, zxid, epoch 三个重要的信息
* 选票!
* 1、myid config.getMyID(zoo.cfg)
* 2、zxid epoch ====> loadDataBase()
*/
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 {
/**
* TODO_MA 通过 UDP 协议往所有 LOOKING 状态的服务器广播选票
*/
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
/**
* 这里是初始化选举算法,从 zoo.cfg 中可以得知,默认的选举算法是: FastPaxos(FastLeaderElection) 算法
* 初始化网络通信组件:QuorumCnxManager,当中会初始化一个 Listener,监听客户端的链接
* 初始化选举算法实例:FastLeaderElection, 负责选举流程的正常进行
*
* 负责具体选举的 那个 算法实例, 现在才开始创建!
*
* electionAlg = FastLeaderElection
*/
this.electionAlg = createElectionAlgorithm(electionType);
}
选举的机制
选举神奇吗,其实一点都不神奇,我们可以看到,每一张选票都带有epoch、zxid、myid。
1、首先选举的就是epoch大的,只要是epoch小的都不要
2、当epoch一样大的时候,选择zxid大的。
3、如果zxid也一样,选serverid大的,也就是myid
总体的原则:
选票: 发送给其他人用来赞成或者反对的提案
投票: 对选票投赞成或者反对票
1、每个server一启动都是把自己生成的epoch、zxid、myid构建成自己的选票,广播给所有其他节点,通俗来讲,就是每个server启动之后就去找leader找不到,就会构建自己的选票,然后广播给所有的server
2、每次接收到一个选票,就会进行一次epoch、zxid、myid的比较,如果自己的不满足要求,就把自己的选票更新成别人的,然后再次广播。
3、如果每次接受到的投票中,有一个投票是赞成自己的,那么执行判断方法,来判断,迄今为止接收到的所有投票,是否有超过半数是同意自己的
4、如果有,那么就更改自己的状态(LOOKING变成LEADING),因为有可能会出现,其他的server还在投票,而且还有大于自己的epoch的,把自己的选票再广播一次
5、再获取一个投票,看看是否有获取到的投票的epoch zxid是否比自己的大, 如果不比自己大,则称为leader
6.1 createElectionAlgorithm
/**
* TODO_MA Leader 选举涉及到两个核心类:QuorumCnxManager 和 FastLeaderElection。
* @param electionAlgorithm
* @return
*/
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_MA 选举过程中的 IO 负责类 qcm = QuorumCnxManager
* 选举过程中的,一切网络通信,都由这个组件负责
* QuorumCnxManager : 负责选举过程中的网络通信 3888
* ServerCnxnFactory: 负责客户端和 server段进行通信的组件 2181
*
* 在这个 createCnxnManager(); 代码中,创建了 Listener
*/
qcm = createCnxnManager();
/**
* QuorumCnxManager 有一个内部类 Listener,初始化一个 ServerSocket,然后在一个 while 循环中调用 accept 接收客户端
* (注意:这里的客户端指的是集群中其它服务器)连接。
* 当有客户端连接进来后,会将该客户端 Socket 封装成 RecvWorker 和 SendWorker,它们都是线程,分别负责和该 Socket 所代表的客户端进行读写。
* 其中,RecvWorker 和 SendWorker 是成对出现的,每对负责维护和集群中的一台服务器进行网络 IO 通信。
*/
QuorumCnxManager.Listener listener = qcm.listener;
if (listener != null) {
/**
* 启动监听! Listener 监听选举端口进来的 client 请求。
* 把进行选举锁需要的各种网络通信组件,都启动好了。
*
* 启动了 QuorumCnxnManager 中的俩个线程 SendWorker(广播选票) RecvWorker(从外界接受投票结果)(三个map联合管理接收结果的)
* QuorumCnxnManager 这个对象中, 也有一个队列: recvQueue
*/
listener.start();
/**
* TODO 选举算法具体实现类!
* 启动了 选举算法实例: 启动了两个线程: WorkerReceiver(构建选票,发送选票,唱票) WorkerSender(负责发送)
* 这个选举算法中,还有两个队列: sendqueue<ToSend>, recvqueue<Notification>
*/
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
6.2 Listener的run方法
/**
* Thread to listen on some port
* 负责各个server之间的通信,维护了和各个server之间的连接,下面的线程负责与其他server建立连接
*/
public class Listener extends ZooKeeperThread {
volatile ServerSocket ss = null;
public Listener() {
// During startup of thread, thread name will be overridden to
// specific election address
super("ListenerThread");
}
/**
* Sleeps on accept().
*/
@Override
public void run() {
int numRetries = 0;
InetSocketAddress addr;
while ((!shutdown) && (numRetries < 3)) {
try {
/**
* TODO 初始化服务器
*/
ss = new ServerSocket();
ss.setReuseAddress(true);
if (listenOnAllIPs) {
int port = view.get(QuorumCnxManager.this.mySid).electionAddr.getPort();
addr = new InetSocketAddress(port);
} else {
addr = view.get(QuorumCnxManager.this.mySid).electionAddr;
}
/**
* 绑定选举端口 3888
*/
LOG.info("My election bind port: " + addr.toString());
setName(view.get(QuorumCnxManager.this.mySid).electionAddr.toString());
ss.bind(addr);
/**
* 等待客户端(其他服务器)的链接!
*/
while (!shutdown) {
/**
* 客户端 Socket 链接管理对象。 卡在这儿。
*/
Socket client = ss.accept(); // 阻塞的方法
setSockOpts(client);
LOG.info("Received connection request " + client.getRemoteSocketAddress());
// Receive and handle the connection request asynchronously if the quorum sasl authentication is enabled.
// This is required because sasl server authentication process may take few seconds to finish,
// this may delay next peer connection requests.
if (quorumSaslAuthEnabled) {
receiveConnectionAsync(client);
} else {
/**
* TODO_MA 接受链接
*/
receiveConnection(client);
}
numRetries = 0;
}
} catch (IOException e) {
LOG.error("Exception while listening", e);
numRetries++;
try {
ss.close();
Thread.sleep(1000);
} catch (IOException ie) {
LOG.error("Error closing server socket", ie);
} catch (InterruptedException ie) {
LOG.error("Interrupted while sleeping. " + "Ignoring exception", ie);
}
}
}
LOG.info("Leaving listener");
if (!shutdown) {
LOG.error("As I'm leaving the listener thread, " + "I won't be able to participate in leader " + "election any longer: " + view
.get(QuorumCnxManager.this.mySid).electionAddr);
}
}
6.3 FastLeaderElection类的注释
/**
* TODO_MA FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader方法和shutdown方法,
* 其是标准的Fast Paxos算法的实现,各服务器之间基于TCP协议进行选举。
*
* TODO_MA 使用TCP实现 leader 选举。它使用 QuorumCnxManager 类的对象来管理连接。
* 否则,与其他UDP实现一样,该算法基于推送。
* Implementation of leader election using TCP. It uses an object of the class QuorumCnxManager to manage connections.
* Otherwise, the algorithm is push-based as with the other UDP implementations.
*
* TODO_MA 这里有一些参数可以调整以更改其行为。首先,finalizeWait 确定等待直到决定领导者的时间。
* 这是领导者选举算法的一部分。
* finalizeWait = 200ms
* There are a few parameters that can be tuned to change its behavior. First,
* finalizeWait determines the amount of time to wait until deciding upon a leader.
* This is part of the leader election algorithm.
*
* TODO_MA Messenger中维护了一个 WorkerSender和 WorkerReceiver,分别表示选票发送器和选票接收器。
*
*
* ZooKeeper 的底层分布式一致性算法: paxos (basic paxos, fast paxos, multi paxos)
* ZooKeeper 的底层选举算法实现:fast paxo
* 具体的实现类就是:FastLeaderElection
*/
6.4 FastLeaderElection的starter
/**
* This method is invoked by the constructor. Because it is a
* part of the starting procedure of the object that must be on
* any constructor of this class, it is probably best to keep as
* a separate method. As we have a single constructor currently,
* it is not strictly necessary to have it separate.
*
* @param self QuorumPeer that created this object
* @param manager Connection manager
*
*
* TODO_MA 其完成在构造函数中未完成的部分,如会初始化FastLeaderElection的sendqueue和recvqueue,并且启动接收器和发送器线程。
*/
private void starter(QuorumPeer self, QuorumCnxManager manager) {
// 赋值,对Leader和投票者的ID进行初始化操作
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
// 初始化发送队列( 发起选举者 发送的选票信息,就被封装成 ToSend 放在一个同步队列里面)
sendqueue = new LinkedBlockingQueue<ToSend>();
// 操作 sendqueue 的就是 workerSender
// 初始化接收队列(发起选举者 接受的投票结果信息,也被放在一个队列里面等待处理 )
recvqueue = new LinkedBlockingQueue<Notification>();
// 操作 recvqueue 就是 workerReceiver
// 创建Messenger,会启动接收器和发送器线程
this.messenger = new Messenger(manager);
}
其实我们看到这里应该就已经看到了一些眉目了,其实看zk的源码就是看他的选举机制因为paxos算法,其他的也很重要但是可以慢慢看 并不是很着急
6.5 总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e76liuMD-1597328279286)(https://gitee.com/wuqingzhi128/blogImg/raw/master//未命名文件 (2)].png)
7. QuorumPeer的run方法
public void run() {
setName("QuorumPeer" + "[myid=" + getId() + "]" + cnxnFactory.getLocalAddress());
LOG.debug("Starting quorum peer");
try {
jmxQuorumBean = new QuorumBean(this);
MBeanRegistry.getInstance().register(jmxQuorumBean, null);
for (QuorumServer s : getView().values()) {
ZKMBeanInfo p;
if (getId() == s.id) {
p = jmxLocalPeerBean = new LocalPeerBean(this);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxLocalPeerBean = null;
}
} else {
p = new RemotePeerBean(s);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
}
}
}
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxQuorumBean = null;
}
/**
* QuorumPeer 线程进入到一个无限循环模式,不停的通过 getPeerState 方法获取当前节点状态,然后执行相应的分支逻辑
*/
try {
/**
* TODO_MA 主要逻辑
* Main loop
* 只要 当前这台服务器一直为 looking 状态, 停止选举的开关也一直开着。
* 一直进行选举,直到选出来leader为止!
*
* 重点
* 1、lookForLeader() 选举的具体实现(只是通过唱票发现自己能成为leader,并不代表自己一定是leader)
* 2、follower.followLeader(); 数据同步的具体实现
* 3、leader.lead(); 先校验!我再广播一次我的选票给所有其他服务器,然后再次验证是否超过半数同意自己
*
* 特例:
* 1、我是刚启动的server, 这个集群的leader早就存在了。
* 我一上线,也会调用lookForLeader找leader。(构建自己的选票,广播给所有用户)
*
* 2、因为集群已经启动,leader, follower角色已经确定。所有的角色都有保存现在的合法选票!
* 这个选票其实保存的信息就是:leader的:ephoch serverid
*
* 3、所有角色都会在收到 刚上线的server的选票的时候,都会把现在的合法选票 发回给这个server
* 刚启动的这个server应该要进行唱票,最终发现大家返回的都是leader的选票。 唱票之后,发现这个leader获得的投票已经
* 超过半数,也就意味着,能确认这个集群中已经有leader了, 自己立即成为 learner(根据配置来决定到底是 follower 还是 observer)
*/
while (running) {
switch(getPeerState()){
case LOOKING:
// 先找到选举算法实例,然后调用他的 lookForLeader() 进行选举
// 在 lookForLeader() 里面有唱票和改变状态的操作,下一次可能就不是looking状态
// lookForLeader 找leader, 找不到就举行选举
setCurrentVote(makeLEStrategy().lookForLeader());
break;
case FOLLOWING:
setFollower(makeFollower(logFactory));
follower.followLeader(); // 数据同步!
break;
case OBSERVING:
setObserver(makeObserver(logFactory));
observer.observeLeader();
break;
case LEADING:
setLeader(makeLeader(logFactory));
leader.lead(); // 在校验过程中,还需要确认有超过半数节点存活
break;
}
/**
* 根据当前节点自己的状态,去做响应的处理。
*/
switch (getPeerState()) {
/**
* 首先系统刚启动时 serverState 默认是 LOOKING,表示需要进行 Leader 选举,这时进入 Leader 选举状态中,
* 会调用 FastLeaderElection.lookForLeader 方法,lookForLeader 方法内部也包含了一个循环逻辑,
* 直到选举出 Leader 才会跳出 lookForLeader 方法,如果选举出的 Leader 就是本节点,
* 则将 serverState=LEADING 赋值,否则设置成 FOLLOWING 或 OBSERVING。
*/
case LOOKING: //如果是looking,则进入选举流程
LOG.info("LOOKING");
if (Boolean.getBoolean("readonlymode.enabled")) {
LOG.info("Attempting to start ReadOnlyZooKeeperServer");
// Create read-only server but don't start it immediately
final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(logFactory, this,
new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb);
// Instead of starting roZk immediately, wait some grace period before we decide we're partitioned.
//
// Thread is used here because otherwise it would require changes in each of election strategy classes which is
// unnecessary code coupling.
Thread roZkMgr = new Thread() {
public void run() {
try {
// lower-bound grace period to 2 secs
sleep(Math.max(2000, tickTime));
if (ServerState.LOOKING.equals(getPeerState())) {
/**
* TODO_MA 启动服务器
*/
roZk.startup();
}
} catch (InterruptedException e) {
LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
} catch (Exception e) {
LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
}
}
};
try {
/**
* TODO_MA 启动服务器
*/
roZkMgr.start();
setBCVote(null);
/**
* TODO_MA 此处通过策略模式来决定当前哪个选举算法来进行领导选举
*/
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
} finally {
// If the thread is in the the grace period, interrupt
// to come out of waiting.
roZkMgr.interrupt();
roZk.shutdown();
}
} else {
try {
/**
* TODO_MA 选举
*/
setBCVote(null);
// TODO 重要: 调用 FastLeaderElection.lookForLeader();
// makeLEStrategy().lookForLeader() 返回的就是 leader 的选票
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
/**
* 如果选举报错,则继续设置自己的状态为 LOOKING 然后继续选举
*/
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
/**
* 对于 Follower 和 Observer 而言,主要的初始化工作是要建立与 Leader 的连接并同步 epoch 信息,最后完成与 Leader 的数据同步
*/
case OBSERVING:
try {
LOG.info("OBSERVING");
setObserver(makeObserver(logFactory));
/**
* TODO_MA observer 和 Leader 做数据同步
*/
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
observer.shutdown();
setObserver(null);
setPeerState(ServerState.LOOKING);
}
break;
/**
* 对于 Follower 和 Observer 而言,主要的初始化工作是要建立与 Leader 的连接并同步 epoch 信息,最后完成与 Leader 的数据同步
*/
case FOLLOWING:
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));
/**
* TODO_MA
*/
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
/**
* 然后 QuorumPeer.run 进行下一轮次循环,通过 getPeerState 获取当前 serverState 状态,
* 如果是 LEADING,则表示当前节点当选为 LEADER,则进入 Leader 角色分支流程,执行作为一个 Leader 该干的任务;
* 如果是 FOLLOWING 或 OBSERVING,则进入 Follower 或 Observer 角色,并执行其相应的任务。
* 注意:进入分支路程会一直阻塞在其分支中,直到角色转变才会重新进行下一轮次循环,
* 比如 Follower 监控到无法与 Leader 保持通信了,会将 serverState 赋值为 LOOKING,跳出分支并进行下一轮次循环,
* 这时就会进入 LOOKING 分支中重新进行 Leader 选举。
*
* Leader 会启动 LearnerCnxAcceptor 线程,该线程会接受来自 Follower 和 Observer(统称为 Learner)的连接请求
* 并为每个连接创建一个 LearnerHandler 线程,该线程会负责包括数据同步在内的与 learner 的一切通信。
*/
case LEADING:
LOG.info("LEADING");
try {
// Leader leader
// 改角色
setLeader(makeLeader(logFactory));
/**
* TODO_MA ZooKeeper 就会进入集群同步阶段,集群同步主要完成集群中各节点状态信息和数据信息的一致。
* 选出新的 Leader 后的流程大致分为:计算 epoch、统一 epoch、同步数据、广播模式等四个阶段。
* 其中其前三个阶段:计算 epoch、统一 epoch、同步数据就是这一节主要介绍的集群同步阶段的主要内容,
* 这三个阶段主要完成新 Leader 与集群中的节点完成同步工作,
* 处于这个阶段的 zk 集群还没有真正做好对外提供服务的能力,可以看着是新 leader
* 上任后进行的内部沟通、前期准备工作等,只有等这三个阶段全部完成,新 leader 才会真正的成为 leader,
* 这时 zookeeper 集群会恢复正常可运行状态并对外提供服务。
*/
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
} finally {
LOG.warn("QuorumPeer main thread exited");
try {
MBeanRegistry.getInstance().unregisterAll();
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
}
}
8. lookForLeader
/**
* TODO_MA 该函数用于开始新一轮的Leader选举,其首先会将逻辑时钟自增,(epoch)
* 然后更新本服务器的选票信息(初始化选票),之后将选票信息放入 sendqueue 等待发送给其他服务器
*
* Starts a new round of leader election. Whenever our QuorumPeer changes its state to LOOKING, this method is invoked, and it
* sends notifications to all other peers.
*/
public Vote lookForLeader() throws InterruptedException {
// 一些准备操作
try {
self.jmxLeaderElectionBean = new LeaderElectionBean();
MBeanRegistry.getInstance().register(self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
self.jmxLeaderElectionBean = null;
}
if (self.start_fle == 0) {
self.start_fle = Time.currentElapsedTime();
}
try {
// recvset 存储合法投票
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
// 存储选举之外的投票结果
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized (this) {
// 更新逻辑时钟,每进行一轮选举,都需要更新逻辑时钟
// logicalclock = epoch
logicalclock.incrementAndGet(); // +===> epoch + 1
// 更新选票(serverid, zxid, epoch)
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 向其他服务器发送自己的选票
LOG.info("New election. My id = " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
sendNotifications();
/**
* Loop in which we exchange notifications until we find a leader
* 之后每台服务器会不断地从 recvqueue 队列中获取外部选票。如果服务器发现无法获取到任何外部投票,
* 就立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,
* 如果已经建立了连接,则再次发送自己当前的内部投票
*/
// 本服务器状态为LOOKING并且还未选出leader
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
/**
* Remove next notification from queue, times out after 2 times the termination time
*
* ToSend: 选票
* Notification: 投票
*/
// 从recvqueue接收队列中取出投票
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
/**
* Sends more notifications if haven't received enough. Otherwise processes new notification.
*/
// 如果没有收到足够多的选票,则发送选票
if (n == null) {
// manager已经发送了所有选票消息
if (manager.haveDelivered()) {
// 向所有其他服务器发送消息
sendNotifications();
// 还未发送所有消息
} else {
// 连接其他每个服务器
manager.connectAll();
}
/**
* Exponential backoff
* 如果第一进行投票的时候,没有收到选票的话,会重新发起投票,然后等待更长的超时时间
* 这个超时更新机制就是 指数增长,2的倍数。
*/
int tmpTimeOut = notTimeout * 2;
notTimeout = (tmpTimeOut < maxNotificationInterval ? tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
// 接收到一个投票结果
// 投票者集合中包含接收到消息中的服务器id
// n.leader 推举成leader 的SID
// n.sid 哪个服务器发过来的
} else if (validVoter(n.sid) && validVoter(n.leader)) {
/**
* Only proceed if the vote comes from a replica in the voting view for a replica in the voting view.
* 确定接收消息中的服务器状态
* 对面发送投票回来的那个server是 follwer还是 leader 还是?
*/
switch (n.state) {
/**
* 如果对端发过来的 electionEpoch 大于自己,则表明重置自己的 electionEpoch,
* 然后清空之前获取到的所有投票 recvset,因为之前获取的投票轮次落后于当前则代表之前的投票已经无效了,
* 然后调用 totalOrderPredicate()将当前期望的投票和对端投票进行 PK,用胜出者更新当前期望投票,
* 然后调用 sendNotifications()将自己期望头破广播出去。
*/
case LOOKING:
// If notification > current, replace and send messages out
// TODO_MA 其选举周期大于逻辑时钟,外部投票的选举轮次大于内部投票。
// 若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),
// 并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。
if (n.electionEpoch > logicalclock.get()) {
// 重新赋值逻辑时钟
logicalclock.set(n.electionEpoch);
// 清空选票
recvset.clear();
// 进行PK,选出较优的服务器( epoch zxid serverid )
// 前面三个参数是对方的,后面三个参数是自己的。按顺序进行比较
// updateProposal 更新选票
// totalOrderPredicate 怎么更新选票的逻辑
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 无法选出较优的服务器
} else {
// 更新选票
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 广播消息
// 注意:这里不管哪一方胜出,都需要广播出去
// 这是因为由于 electionEpoch 落后导致之前发出的所有投票都是无效的,所以这里需要重新发送
sendNotifications();
// TODO_MA 选举周期小于逻辑时钟,不做处理,外部投票的选举轮次小于内部投票。
// 若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理。
} else if (n.electionEpoch < logicalclock.get()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long
.toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
}
break;
// TODO_MA 逻辑时钟相等,并且能选出较优的服务器.外部投票的选举轮次等于内部投票。
// 此时可以开始进行选票PK,如果消息中的选票更优,则需要更新本服务器内部选票,再发送给其他服务器。
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
// 更新选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 发送消息
sendNotifications();
}
if (LOG.isDebugEnabled()) {
LOG.debug("Adding vote: from=" + n.sid + ", proposed leader=" + n.leader + ", proposed zxid=0x" + Long
.toHexString(n.zxid) + ", proposed election epoch=0x" + Long.toHexString(n.electionEpoch));
}
// recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 判断是否能选出 leader
// 唱票
// recvset所有的投票结果集,new Vote() 当前自己持有的合法选票
if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) {
// 遍历已经接收的投票集合
// Verify if there is any change in the proposed leader
while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
// 能够选出较优的服务器。选票有变更,比之前提议的Leader有更好的选票加入
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
// 将更优的选票放在recvset中
recvqueue.put(n);
break;
}
}
/**
* This predicate is true once we don't read any new relevant message from the reception queue
*/
// 表示之前提议的Leader已经是最优的
if (n == null) {
// 设置服务器状态
self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING : learningState());
// 最终的选票
// endVote 就是 最终的选票 就是leader的选票
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
// 清空recvqueue队列的选票
leaveInstance(endVote);
// 返回选票
return endVote;
}
}
break;
case OBSERVING:
LOG.debug("Notification from observer: " + n.sid);
break;
case FOLLOWING:
// 处于LEADING状态
/**
* 若选票中的服务器状态为FOLLOWING或者LEADING时,其大致步骤会判断选举周期是否等于逻辑时钟,归档选票,
* 是否已经完成了Leader选举,设置服务器状态,修改逻辑时钟等于选举周期,返回最终选票
*/
case LEADING:
/**
* Consider all notifications from the same epoch together.
*/
// 与逻辑时钟相等
if (n.electionEpoch == logicalclock.get()) {
// 将该服务器和选票信息放入recvset中
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 判断是否完成了leader选举
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);
// 清空recvqueue队列的选票
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));
// 已经完成了leader选举
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);
// 清空recvqueue队列的选票
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());
}
}
到此选举算法就结束了,那么我们来回顾一下这个流程
9. 选举算法的总结
假定三台机器来进行选举,第一台机器先启动,然后启动其他的每个myid依次为1,2,3,并且选举轮次也是
- 还没有选举出来leader之前,所有的节点都是LOOKING的状态,也就是找leader的状态
- 首先会构造自己的选票,然后把自己选票发送给所有的参与选举的机器,然后就会一直去接收外部队列的里面获取外部选票,因为是场景驱动,所以此时没有机器获取到选票,只能获取到自己的
- 然后依次进行判断,当前选票和外部接收到的选票的epoch是否一样,此时肯定是一样,所以会进行判断zxid,发现zxid还是一样的,这个时候就判断myid,发现还是一样的,这个时候发现就是我自己的,然后就把自己的选票加入到了合法队列中
- 然后就判断合法队列中的这个选票是否大于半数,如果不大于继续循环,但是现在就一台机器,并且还发现,别的机器一直连不上,就处于了等待
现在启动第二台机器
- 第二台机器启动的时候还是LOOKING的状态,也就是找leader的状态
- 还是会构造自己的选票,发送给所有参与选举的机器
- 这个时候第一台机器发现第二台机器启动起来了,然后又发送了自己认为正确的选票给其他机器,在进行判断
- 第二台机器也会发送自己的选票,第二台机器先收到的是自己的选票,然后把自己的选票更新为最优的,然后发现唱票,发现不足半数
- 这个时候,第二台机器收到了第一台机器发来的选票依次判断epoch、zxid、myid发现myid不如我啊,就什么都不做
- 第一台机器发现,有新选票了,我要看看,然后进行对比,发现,我靠,我没它厉害,然后我认为他就是leader,然后把自己认为对的选票更新为他的选票
- 然后发送给所有机器,这个时候,第二台机器收到了这个选票,然后对于epoch、zxid、myid,然后把这条消息,添加到选票集合中
- 发现唱票成功,这个时候又会将这个选票进行发送一次,看看是否还是认为他是leader
- 如果是那么就开始更新状态,为leader,然后第一台启动的机器也发现,第二台机器是leader这个时候就开始更新自己的状态follower开始同步数据
- leader也会开始开放端口对外进行服务
现在启动第三台机器
- 先开始找leader发现找到了leader然后就更新自己的状态为follower