开始启动——定时清理任务
话不多说,找到我们的启动类org.apache.zookeeper.server.quorum.QuorumPeerMain,查看他的main方法
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
main.initializeAndRun(args);
}
...
}
其实就是调用了自己的initializeAndRun方法
protected void initializeAndRun(String[] args)
throws ConfigException, IOException, AdminServerException
{
//解析配置
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
// Start and schedule the the purge task
//定时清理任务
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
// 当配置了多节点信息,config.isDistributed()=true
if (args.length == 1 && config.isDistributed()) {
// 集群模式
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);
}
}
通过传递进来的路径参数解析zoo.cfg文件,创建定时清理的任务DatadirCleanupManager#start
public void start() {
timer = new Timer("PurgeTask", true);
TimerTask task = new PurgeTask(dataLogDir, snapDir, snapRetainCount);
timer.scheduleAtFixedRate(task, 0, TimeUnit.HOURS.toMillis(purgeInterval));
purgeTaskStatus = PurgeTaskStatus.STARTED;
}
跟进PurgeTask#run方法,查看具体任务内容
public void run() {
LOG.info("Purge task started.");
try {
PurgeTxnLog.purge(logsDir, snapsDir, snapRetainCount);
} catch (Exception e) {
LOG.error("Error occurred while purging.", e);
}
LOG.info("Purge task completed.");
}
public static void purge(File dataDir, File snapDir, int num) throws IOException {
if (num < 3) {
throw new IllegalArgumentException(COUNT_ERR_MSG);
}
FileTxnSnapLog txnLog = new FileTxnSnapLog(dataDir, snapDir);
List<File> snaps = txnLog.findNRecentSnapshots(num);
int numSnaps = snaps.size();
if (numSnaps > 0) {
purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
}
}
主要清理的逻辑在purgeOlderSnapshots方法,主要清楚最近以及之前zxid的快照文件,不包括文件中有包含比最近的zxid版本更新的事务的文件。
集群启动
接下来就是通过参数数量与配置判断是集群启动还是单机启动,这里以集群启动为例调用QuorumPeerMain#runFromConfig
public void runFromConfig(QuorumPeerConfig config)
throws IOException, AdminServerException
{
try {
// 注册jmx
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer");
try {
ServerCnxnFactory cnxnFactory = null;
ServerCnxnFactory secureCnxnFactory = null;
if (config.getClientPortAddress() != null) {
cnxnFactory = ServerCnxnFactory.createFactory();
// 配置客户端连接端口
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns(),
false);
}
if (config.getSecureClientPortAddress() != null) {
secureCnxnFactory = ServerCnxnFactory.createFactory();
// 配置安全连接端口
secureCnxnFactory.configure(config.getSecureClientPortAddress(),
config.getMaxClientCnxns(),
true);
}
// ------------初始化当前zk服务节点的配置----------------
// 设置数据和快照操作
quorumPeer = getQuorumPeer();
quorumPeer.setTxnFactory(new FileTxnSnapLog(
config.getDataLogDir(),
config.getDataDir()));
quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());
quorumPeer.enableLocalSessionsUpgrading(
config.isLocalSessionsUpgradingEnabled());
//quorumPeer.setQuorumPeers(config.getAllMembers());
// 选举类型
quorumPeer.setElectionType(config.getElectionAlg());
// server Id
quorumPeer.setMyid(config.getServerId());
quorumPeer.setTickTime(config.getTickTime());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
quorumPeer.setInitLimit(config.getInitLimit());
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setConfigFileName(config.getConfigFilename());
// 设置zk的节点数据库
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);
if (config.getLastSeenQuorumVerifier()!=null) {
quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false);
}
// 初始化zk数据库
quorumPeer.initConfigInZKDatabase();
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setSecureCnxnFactory(secureCnxnFactory);
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
// 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);
// -------------初始化当前zk服务节点的配置---------------
quorumPeer.initialize();
//重点
quorumPeer.start();
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
首先对log4j注册 JMX MBean ,实现监控、管理能力,创建ServerCnxnFactory工厂,默认是NIOServerCnxnFactory,初始化 QuorumPeer,设置各种参数,其中会创建初始化zk数据库也就是ZKDatabase,ZKDatabase中会对DataTree进行初始化,创建root节点,以及/zookeeper/config配置的节点。 quorumPeer.setElectionType(config.getElectionAlg());为设置选举的类型,默认是3.接着会调用quorumPeer.start方法这是整个启动逻辑的重点方法
public synchronized void start() {
// 校验serverid如果不在peer列表中,抛异常
if (!getView().containsKey(myid)) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
// 加载zk数据库:载入之前持久化的一些信息
loadDataBase();
// 启动连接服务端
startServerCnxnFactory();
try {
adminServer.start();
} catch (AdminServerException e) {
LOG.warn("Problem starting AdminServer", e);
System.out.println(e);
}
// 启动之后马上进行选举,主要是创建选举必须的环境,比如:启动相关线程
startLeaderElection();
// 执行选举逻辑
super.start();
}
首先会去调用loadDataBase方法加载快照文件中的数据到内存中,主要是重启、故障恢复时快速恢复数据的。这个具体的实现过程后面再讲。然后就是调用startServerCnxnFactory方法去启动ServerCnxnFactory服务
private void startServerCnxnFactory() {
if (cnxnFactory != null) {
cnxnFactory.start();
}
if (secureCnxnFactory != null) {
secureCnxnFactory.start();
}
}
调用ServerCnxnFactory的start方法,默认是使用NIOServerCnxnFactory实现的
public void start() {
stopped = false;
//初始化worker线程池
if (workerPool == null) {
workerPool = new WorkerService(
"NIOWorker", numWorkerThreads, false);
}
//挨个启动Selector线程(处理客户端请求线程),
for(SelectorThread thread : selectorThreads) {
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
// ensure thread is started once and only once
//启动acceptThread线程(处理接收连接进行事件)
if (acceptThread.getState() == Thread.State.NEW) {
acceptThread.start();
}
// ExpirerThread(处理过期连接)
if (expirerThread.getState() == Thread.State.NEW) {
expirerThread.start();
}
}
- workerPool为空时,创建一个WorkerService,他是一个worker线程池,处理工作任务,里面包含若干ExecutorService,可以设置工作线程数,shutDown超时时间。
- 挨个启动Selector线程,这是zk封装的thread,继承自Thread,封装了NIO的 Selector、SocketChannel,SelectionKey,用于完成nio网络通信。
- 启动acceptThread线程,根据状态确保只启动一次。处理注册监听ACCEPT事件.
- 启动ExpirerThread线程,循环判断连接是否过期,处理清除过期连接。
选举准备工作
线程启动完后,会去调用startLeaderElection方法去准备选举相关环境
synchronized public void startLeaderElection() {
try {
// 所有节点启动的初始状态都是LOOKING,因此这里都会是创建一张投自己为Leader的票
if (getPeerState() == ServerState.LOOKING) {
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
}
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//初始化选举算法,electionType默认为3
this.electionAlg = createElectionAlgorithm(electionType);
}
所有节点启动的初始状态都是LOOKING,因此这里都会是创建一个投票,设置到currentVote中,就是说创建一张投给自己的票。Vote属性传入myid,最近的事务版本zxid,peerEpoch三个参数,然后前面看到过electionType默认是设置为3的。所以会调用createElectionAlgorithm方法创建选举算法。
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:
//electionAlgorithm默认是3,直接走到这里
qcm = createCnxnManager();
//监听选举事件的listener
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
//开启监听器
listener.start();
//初始化选举算法
FastLeaderElection fle = new FastLeaderElection(this, qcm);
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
因为传进来的electionAlgorithm是3,所以会创建一个QuorumCnxManager,然后开启里面的监听器(这个监听器主要就是根据配置信息创建选举地址的套接字,bind绑定监听选举端口),创建初始化具体的选举算法FastLeaderElection。然后FastLeaderElection 执行start方法,其实就是启动两个线程WorkerSender、WorkerReceiver,分别用于向其他所有节点发送自己的投票信息、接收并处理其他节点发送给自己的投票信息的线程,具体的可以放在之后在说明。最后,会调用QuorumPeer父类的start方法执行具体选举逻辑。QuorumPeer类继承自ZooKeeperThread类,而ZooKeeperThread类继承自Thread。所以调用QuorumPeer父类的start方法就是在调用线程run方法。这个在后面再具体分析。
小结
回顾一下zookeeper集群的启动过程:
- QuorumPeerMain启动,通过传入的配置文件路径地址解析zoo.cfg配置文件。
- 创建并启动定时清理任务的线程
- 创建ServerCnxnFactory工厂,默认是NIOServerCnxnFactory,初始化 QuorumPeer,设置各种参数,其中会创建初始化zk数据库也就是ZKDatabase(存放节点信息的)
- 加载快照日志文件恢复数据
- 开启各种工作 连接 清理的线程
- 监听选举端口等待接收信息,创建具体选举算法,启动WorkerSender、WorkerReceiver,分别用于向其他所有节点发送自己的信息、接收并处理其他节点发送给自己的信息的线程。
- 开始具体选举。