文章目录
zookeeper Leader选举源码分析
一、包含的知识点
二、zookeeper选举的入口
2.1 zookeeper服务启动入口main
要了解zookeeper选举流程, 首先需要了解程序的入口, 那怎么知道程序的入口呢? 使用过zookeeper服务知道启动命令 zkServer.sh、zkServer.cmd, 查看zkServer.sh shell脚本, 对ZOOMAIN处理都包含QuorumPeerMain类的全路径声明, 如:
ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"
显然QuorumPeerMain类是服务启动入口, 通过Idea查询QuorumPeerMain类, main函数包含下面的内容
// 根据指定的配置文件, 启动Server服务
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
main.initializeAndRun(args);
} catch(Exception e) {
//省略部分异常处理代码
}
LOG.info("Exiting normally");
System.exit(0);
}
2.2 触发选举时机
Leader在zookeeper集群环境中只存在一个, 那什么时候会触发Leader选举呢 ? 主要包含下面场景
- 服务集群启动时需要选举Leader
- Leader服务不可用时需要重新选举
三、initializeAndRun 初始化并启动服务
从2.1节代码可以知道, main方法内部调用了initializeAndRun方法进行服务启动, 下面是具体代码逻辑
//QuoRumPeerMain
protected void initializeAndRun(String[] args)
throws ConfigException, IOException{
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
//1. 解析配置文件, 获得配置参数
config.parse(args[0]);
}
// Start and schedule the the purge task
//2. 创建并启动线程, 用于清理日志文件
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
//3. 启动服务
if (args.length == 1 && config.servers.size() > 0) {
//3.1 如果存在配置文件, 按照配置文件启动服务
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
//3.2 如果没有给定配置文件, 按照单例模式启动, 即args=null
ZooKeeperServerMain.main(args);
}
}
服务启动如果存在配置文件, 会对配置文件进行解析, args[0] 其实就是配置文件**zoo.cfg**路径信息, 比如下面zookeeper启动命令
# zkServer.sh ../conf/zoo.cfg
initializeAndRun方法的主要逻辑是
-
创建配置对象QuorumPeerConfig解析配置文件 (配置解析流程比较简单, 这里不分析, 使用到的地方会提一下)
-
创建对象DatadirCleanupManager用于定期清理日志信息
-
从config中获取必要的参数: dataDir(数据目录)、dataLogDir(日志目录)、snapRetainCount(快照保留数量)、purgeInterval(清理间隔)
-
参数对应zoo.cfg配置信息如下
dataDir=../data dataLogDir=../log snapRetainCount=5 purgeInterval=3600
-
-
服务启动
- 如果存在zoo.cfg配置参数信息,按照配置文件进行服务启动
- 如果没有配置zoo.cfg, 按照单例的方式启动服务
四、runFromConfig 按照配置文件启动服务
生产环境肯定使用zookeeper集群方式启动服务, 也就是肯定存在配置文件信息, 因此这里只分析runFromConfig启动服务进行选举流程, ZooKeeperServerMain.main(args)按照单例的方式启动服务暂时不分析。
//QuoRumPeerMain
public void runFromConfig(QuorumPeerConfig config) throws IOException {
try {
//1. 注册log4j JMX mbeans信息, 可以通过设置zookeeper.jmx.log4j.disable=true来禁用
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer");
try {
//2. 创建ServerCnxnFactory用于服务连接
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//3. 创建线程 QuorumPeer, 并根据config进行属性信息设置
quorumPeer = getQuorumPeer();
//... 省略从config获取配置参数, 设置quorumPeer属性的代码
quorumPeer.initialize();
//4. 启动线程 quorumPeer
quorumPeer.start();
//5. join住线程quorumPeer, 让主线程等待子线程执行完
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
从代码逻辑可以看出runFromConfig方法主要是创建QuorumPeer线程, 并进行启动。方法的主要流程如下:
-
注册log4j JMX mbeans信息, 用于日志输出, 可以通过设置**zookeeper.jmx.log4j.disable=true**来禁用
-
创建ServerCnxnFactory对象用于服务连接, 这里主要是 NIOServerCnxnFactory
-
创建线程 QuorumPeer, 并根据config进行属性信息设置
- config信息解析在第三节中提到
config.parse(args[0])
-
调用start方法进行服务启动, 并执行join方法保证子线程能够正常执行结束
五、QuorumPeer启动线程
QuorumPeer其实是一个线程, 它内部继承了Thread, 下面是类继承关系图, 因此QuorumPeer类启动其实就是线程的启动, 只是包含了部分其它逻辑
下面是QuorumPeer启动入口
//QuorumPeer
public synchronized void start() {
//1. 加载数据, 进行快照信息回复
loadDataBase();
//2. 启动服务连接线程, 其实就是NIOServerCnxnFactory内线程启动
cnxnFactory.start();
//3. 进行Leader选举
startLeaderElection();
//4. 调用父类进行线程启动
super.start();
}
从start方法看出在进行服务启动前会进行必要信息处理, 之后会调用父类start方法进行线程启动, 主要流程是
-
加载数据进行快照信息恢复, 这里通过 FileTxnSnapLog 进行数据加载, 主要加载dataDir、dataLogDir目录中信息, 这两个目录是在下面代码处配置
//QuoRumPeerMain#runFromConfig方法 quorumPeer.setTxnFactory(new FileTxnSnapLog( new File(config.getDataLogDir()), new File(config.getDataDir())));
-
通过 NIOServerCnxnFactory 启动服务连接线程
-
在第四节中创建了 cnxnFactory 对象, 并创建了 ZookeeperThread线程对象
//QuoRumPeerMain#runFromConfig ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns()); //NioServerCnxnFactory#configure public void configure(InetSocketAddress addr, int maxcc) throws IOException { configureSaslLogin(); thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr); thread.setDaemon(true); maxClientCnxns = maxcc; this.ss = ServerSocketChannel.open(); ss.socket().setReuseAddress(true); LOG.info("binding to port " + addr); ss.socket().bind(addr); ss.configureBlocking(false); ss.register(selector, SelectionKey.OP_ACCEPT); }
-
进行服务启动, 实际就是ZookeeperThread线程的启动
public void start() { // 线程是NEW状态, 还没有启动 if (thread.getState() == Thread.State.NEW) { thread.start(); } }
-
覆写run方法, 采用多路复用技术, 根据SelectKey来分别处理OP_ACCEPT、OP_READ、OP_WRITE
-
-
执行节点Leader选举(细节后面分析)
-
调用父类进行线程启动, 即调用Thread的start方法
六、startLeaderElection 进行Leader选举参数配置
6.1 QuorumPeerMain服务启动主要流程
在进入startLeaderElection方法之前, QuorumPeerMain为服务启动做了相应处理, 简要概括如下:
- parse(args[0]), 解析配置文件zoo.cfg中相关配置信息
- 基于解析后的配置config, 创建启动线程类QuorumPeer
- 创建server服务之间连接工厂 ServerCnxnFactory, 默认是NIOServerCnxnFactory
- 加载文件(dataDir、dataLogDir)进行数据恢复
- 启动用于服务连接的线程, 实际是ServerCnxnFactory内部包含的ZookeeperThread
- startLeaderElection 进行Leader选举
- 调用super启动QuorumPeer线程
6.2 startLeaderElection代码具体分析
现在我们具体分析 startLeaderElection 方法, 首先看下代码
//QuorumPeer
synchronized public void startLeaderElection() {
try {
//1. 创建投票, 票据Vote包含myid, zxid, epoch三个主要信息
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
//2. 从所有view中找到当前节点信息, 设置myQuorumAddr信息
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");
}
//3. 如果选举方式为0, 设置相关信息
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//设置选举类型, 默认是3
this.electionAlg = createElectionAlgorithm(electionType);
}
从代码执行流程可以看出, 虽然方法名称叫startLeaderElection, 实际上是进行Leader选举前相关信息创建,具体包含下面内容
-
创建选举使用的投票信息Vote, 主要包含: myid、zxid、epoch 三个主要信息
-
查找当前服务节点 myQuorumAddr 信息, 通过比较QuorumServer.id 、myid来识别是否属于同一节点
-
getView信息来源于解析zoo.cfg时. 会解析下面内容获取QuorumServer信息
server.1=IP1:2888:3888 server.2=IP2.2888:3888 server.3=IP3.2888:3888 // 集群节点配置如下 server.A=B:C:D A 数字, 表示服务器编号, 和myid文件内容相对应 B 服务器节点IP C 当前服务器节点和Leader服务器进行信息交换的端口 D 选举时, 服务器相互通信的端口
-
myid信息来源于, data目录下myid文件配置的内容
-
-
创建选举算法, 默认情况下electionType=3
//QuorumPeer protected Election createElectionAlgorithm(int electionAlgorithm){ Election le=null; switch (electionAlgorithm) { //... 省略部分代码 case 3: //1. 创建QuorumCnxManager, 以TCP的方式为每一对Server维护一个连接connection qcm = createCnxnManager(); QuorumCnxManager.Listener listener = qcm.listener; if(listener != null){ //2. 启动listener, 用于处理连接(connection)请求 listener.start(); //3. 创建Leader选举使用的算法, 这里是FastLeaderElection le = new FastLeaderElection(this, qcm); } else { LOG.error("Null listener when initializing cnx manager"); } break; default: assert false; } return le; }
6.3 创建Leader选举算法
Leader选举存在多种算法, 主要包括LeaderElection
、AuthFastLeaderElection
、AuthFastLeaderElection
、FastLeaderElection
, 默认情况下electionType=3(FastLeaderElection
), 其实通过源码也可以看出前三种方式都添加了 @Deprecated
注解, 表示这种选举方式已经废弃。这里只讲解 electionType=3创建选举方法。分析上面的代码其主要逻辑如下
- 创建连接管理器 createCnxnManager() , 为每对服务器之间维护一个连接, 用于接收消息进行投票
- 启动listener线程用于监听投票请求, Listener是ZookeeperThread子类, 那listenre也是线程, 启动后会不断监听服务请求
- 创建投票使用的算法FastLeaderElection
执行createElectionAlgorithm方法后, 选择使用FastLeaderElection算法进行投票选举, 这个算法主要做了什么呢?
//FastLeaderElection
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this