上一篇已经说过了zookeeper源码怎么启动的了。那么就直接步入正题。
本篇所有的十一NIO通信为例,不涉及到其它的,实际上zookeeper不止可以使用NIO通信。看下图:
zookeeper单机流程
其实都不用想单机逻辑肯定非常简单,毕竟一台服务器,很多都很好实现。
整个流程图如下:
其中箭头中的数字是调用的顺序,横向表示在同一个方法中,而黄色区域为该方法的注释。单机启动的整个体系就是这个展开的。
zookeeper单机配置文件
从启动项就能明白,我们指定了配置文件启动,所以肯定是把配置文件的参数解析出来,然后载入到内存中,最后初始化好server即可。因为解析配置都很简单,所以就不说了,主要是后面放到对应的配置类之后做了什么事?怎么初始化的?当一个client发送请求应该怎么处理的?这个才是重点需要了解的。
zookeeper怎么初始化的?
我们直接定位到org.apache.zookeeper.server.ZooKeeperServerMain#runFromConfig这个方法。你可以在这里打个断点,直接就粗暴的认为前面就是读取文件,实际还有判断判断集群单机,快照和事务的定时策略等。
public void runFromConfig(ServerConfig config) throws IOException {
LOG.info("这是单机模式!!!!");
LOG.info("Starting server");
//事务日志结构体
FileTxnSnapLog txnLog = null;
try {
// Note that this thread isn't going to be doing anything else,
// so rather than spawning another thread, we will just call
// run() in this thread.
// create a file logger url from the command line args
final ZooKeeperServer zkServer = new ZooKeeperServer();
// Registers shutdown handler which will be used to know the
// server error or shutdown state changes.
final CountDownLatch shutdownLatch = new CountDownLatch(1);
zkServer.registerServerShutdownHandler(
new ZooKeeperServerShutdownHandler(shutdownLatch));
//server的相关属性设置
txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(
config.dataDir));
txnLog.setServerStats(zkServer.serverStats());
zkServer.setTxnLogFactory(txnLog);
zkServer.setTickTime(config.tickTime);
zkServer.setMinSessionTimeout(config.minSessionTimeout);
zkServer.setMaxSessionTimeout(config.maxSessionTimeout);
//通过工厂模式去生产一个实际的zkserver的工厂。
cnxnFactory = ServerCnxnFactory.createFactory();
//生产了一个NIO的cnxn的工厂出来
//配置文件中配置maxClientCnxns,config.getMaxClientCnxns()就是它的值。默认60
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
cnxnFactory.startup(zkServer);
// Watch status of ZooKeeper server. It will do a graceful shutdown
// if the server is not running or hits an internal error.
//执行完await方法就能操作了。为什么这个时候就可以了?首先它肯定是等待一个子线程结束。当我们debug到这的时候,这里的主线程就会阻塞到这里。
//阻塞到这里为什么client执行命令会卡住呢?
//首先我们注释掉shutdownLatch.await();那么一启动就会结束,Exiting normally。那么这个方法的作用就很明显了,就是为了让子线程一直执行。
//为什么一定要执行await()方法之后命令才会处理呢?能连上说明2181端口是开启的。请求处理链的线程也是开启了的那么调试的时候就是处理链到端口阻塞或者返回的时候阻塞了。
//通过在PrepRequestProcessor的run方法前加上打印符发现是执行了await后才打印的,说明这个时候是阻塞了处理链到端口的地方。
//com.xq.test.ServerSocketChannelTest大概模拟了一下,也许不对,可能是执行await方法之后才去处理端口接受的数据的,那我怎么证明呢?
//首先肯定有方法接受并处理数据。然后定位到org.apache.zookeeper.server.NIOServerCnxnFactory.run方法中。
//我在await方法加了打印,也在NIOServerCnxnFactory.run加了打印。从多次执行打印看就是先接受,处理,所以线程是没有被阻塞的,说明我想错了。
//实际情况应该是我dedug的当断点到这的时候程序已经是暂停状态,它是不会跑代码的,所以是个错误的感觉,白写这么多!
//TimeUnit.MINUTES.sleep(2);照样能用。。。注释不删,就当是教训吧。
System.out.println("await方法");
shutdownLatch.await();
// TimeUnit.MINUTES.sleep(2);
shutdown();
cnxnFactory.join();
if (zkServer.canShutdown()) {
zkServer.shutdown(true);
}
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Server interrupted", e);
} finally {
if (txnLog != null) {
txnLog.close();
}
}
}
这个方法的注释我也写出来了。其实重点还是cnxnFactory.startup(zkServer);这行代码。前面的代码就认为是为整个启动做准备。
跟踪到这个代码下面:
@Override
public void startup(ZooKeeperServer zks) throws IOException,
InterruptedException {
//启动thread线程。
start();//1
//设置zkserver的属性
setZooKeeperServer(zks);//2
//加载数据。如果有数据,直接返回。否则根据事务和快照进行快速恢复。
zks.startdata();//3
//启动ZooKeeperServer服务器
zks.startup();//4
}
这个就很重要了。可以说是单机最核心的启动流程,针对这四步一个一个的说。
1、start()方法:它启动了一个线程,调用线程的start()方法,所以直接定位到run()方法中。
public void run() {
System.out.println("接受并处理socket数据!!");
while (!ss.socket().isClosed()) {
try {
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) {
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);
NIOServerCnxn cnxn = createConnection(sc, sk);
sk.attach(cnxn);
addCnxn(cnxn);
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
NIOServerCnxn c = (NIOServerCnxn) k.attachment();
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");
}
也就不详细说了,其实就是不断的获取客户端的数据。
2、setZooKeeperServer(zks)比较简单,就是把配置设置到单机的的启动类中。
3、zks.startdata():这个方法就比较重要了,因为比如zookeeper启动过一段时间,然后停止了,但是要再次启动这些数据再哪里呢?zookeeper也是做了持久化了的,再次启动之前就需要把这些配置读取数据到内存。
public void startdata()
throws IOException, InterruptedException {
//check to see if zkDb is not null
if (zkDb == null) {//如果没有数据,先new出一个ZKDatabase
zkDb = new ZKDatabase(this.txnLogFactory);
}
if (!zkDb.isInitialized()) {//如果没有被初始化
loadData();
}
}
public void loadData() throws IOException, InterruptedException {
/*
* When a new leader starts executing Leader#lead, it
* invokes this method. The database, however, has been
* initialized before running leader election so that
* the server could pick its zxid for its initial vote.
* It does it by invoking QuorumPeer#getLastLoggedZxid.
* Consequently, we don't need to initialize it once more
* and avoid the penalty of loading it a second time. Not
* reloading it is particularly important for applications
* that host a large database.
*
* The following if block checks whether the database has
* been initialized or not. Note that this method is
* invoked by at least one other method:
* ZooKeeperServer#startdata.
*
* See ZOOKEEPER-1642 for more detail.
*/
if(zkDb.isInitialized()){//如果被初始化了。证明已经有数据库。就加载datatree中的最新的zxid。这样就能快速恢复。
setZxid(zkDb.getDataTreeLastProcessedZxid());
}
else {
//如果没有被初始化。根据事务和快照进行快速恢复。
setZxid(zkDb.loadDataBase());
}
// Clean up dead sessions
LinkedList<Long> deadSessions = new LinkedList<Long>();
for (Long session : zkDb.getSessions()) {
if (zkDb.getSessionWithTimeOuts().get(session) == null) {
deadSessions.add(session);
}
}
zkDb.setDataTreeInit(true);
for (long session : deadSessions) {
// XXX: Is lastProcessedZxid really the best thing to use?
killSession(session, zkDb.getDataTreeLastProcessedZxid());
}
}
我们看看setZxid(zkDb.loadDataBase());这个方法,从头初始化。
public long loadDataBase() throws IOException {
反序列化事务和快照。zxid再加一,实现了快速加载
//怎么恢复的呢?
//首先进入到这里就说明内存不存在任何数据,那么就需要把当前的server快速恢复到上次关闭的状态。
//1.它首先加载最新的备份的快照文件,将其反序列化加载到内存中。
//2.快照文件中存在zxid,根据记录到dataLogDir中的事务依次执行到最新的zxid,就能恢复到上次关闭的状态了。
//DataTree就是一个树形结构,就是我们在zkCli中看到的类似linux目录的结构。
long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
initialized = true;
return zxid;
}
因为很多其实都在注释中了,就不多说了。
4、zks.startup()我们在前面做了什么事呢?再来回想一下。
1、启动线程:为了能够接收客户端的数据。
2、配置出一个zookeeperserver。
3、把zookeeper恢复到上次关闭的状态。
所以第四步应该做什么呢?仔细想想前3步。可有什么没有做的?
是的。我们还没有去处理并返回客户端的请求。所以我们最后启动是做什么呢?肯定是初始化请求处理啦!来来来,我们看看这个方法做了什么。
public synchronized void startup() {
if (sessionTracker == null) {
createSessionTracker();
}
//开启一个session跟踪
startSessionTracker();
//设置一个请求处理器。传说中的责任链模式。这个有点绕。不过理解了这个模式就好懂。
//流程就是PrepRequestProcessor->SyncRequestProcessor->FinalRequestProcessor
setupRequestProcessors();
//干嘛的不知道
registerJMX();
//设置状态为运行
setState(State.RUNNING);
//启动完成。开始唤醒所有线程。这里不知道有什么作用,我就算是注释掉操作照样能运行。
//但是上一步setState(State.RUNNING);注释掉客户端就能连证明至少要到setState后server才初始化完成。
notifyAll();
}
其它不说,我认为最重要的代码是:
setupRequestProcessors();
我们看看这个方法里面是什么。
protected void setupRequestProcessors() {
//构造一个FinalRequestProcessor---是一个线程
//FinalRequestProcessor:用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理还会负责将事务应用到内存数据库中去。
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
//构造一个SyncRequestProcessor---是一个线程。SyncRequestProcessor:事务日志记录处理器
RequestProcessor syncProcessor = new SyncRequestProcessor(this,
finalProcessor);
//启动这个线程。
((SyncRequestProcessor)syncProcessor).start();
//PrepRequestProcessor: 请求预处理器
firstProcessor = new PrepRequestProcessor(this, syncProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
因为这个相当于使用了3个线程去处理,每一步都有自己的负责的事情,故而称之为责任链模式(也可以认为是流水线)。至于每个线程中的run()方法就不带进去看了。这个相信用了一段时间zookeeper的人里面的run()方法应该能看懂。后面这里补个请求处理链的流程图。