先来看下ZooKeeper服务端的整体构架。
ZooKeeper服务器的启动,大体分为以下5个步骤:配置文件解析、初始化数据管理器、初始化网络I/O管理器、数据恢复与对外服务。下图为单机版的服务器启动流程。
预启动步骤如下。
(1)统一由QuorumPeerMain作为启动类。
无论是单机版还是集群版,在启动ZooKeeper服务器时,在zkServer.cmd和zkServer.sh两个脚本中,都配置了使用org.apach.zookeeper.server.quorum.QuorumPeerMain作为启动类入口。
public class QuorumPeerMain {
private static final Logger LOG = LoggerFactory.getLogger(QuorumPeerMain.class);
private static final String USAGE = "Usage: QuorumPeerMain configfile";
protected QuorumPeer quorumPeer;
/**
* 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 {
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);
}
protected void initializeAndRun(String[] args)
throws ConfigException, IOException
{
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]); //解析配置文件zoo.cfg
}
// Start and schedule the the purge task //创建并启动历史文件清理器
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
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)解析配置文件zoo.cfg。
ZooKeeper首先会进行配置文件的解析,配置文件的解析其实就是对zoo.cfg文件的解析,该文件配置了ZooKeeper运行时的基本参数,包括ticketTime、DataDir和clientPort等参数。
(3)创建并启动历史文件清理器DatadirCleanupManager。
从3.4.0版本开始,ZooKeeper增加了自动清理历史数据文件的机制,包括对事务日志和快照文件进行定时清理。
(4)判断当前是集群模式还是单机模式的启动。
从上面步骤(2)解析出的集群服务器地址列表来判断当前是集群模式还是单机模式,如果是单机模式,就委托给ZooKeeperServerMain进行启动处理。
(5)再次进行配置文件zoo.cfg的解析。
(6)创建服务器实例ZooKeeperServer。
org.apach.zookeeper.server.ZooKeeperServer是单机版ZooKeeper服务端最核心的实体类。ZooKeeper服务器首先会进行服务器实例的创建,接下去的步骤是则都是对该服务器实例的初始化工作,包括连接器、内存数据库和请求处理器等组件的初始化。
public class ZooKeeperServerMain {
private static final Logger LOG =
LoggerFactory.getLogger(ZooKeeperServerMain.class);
private static final String USAGE =
"Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]";
private ServerCnxnFactory cnxnFactory;
/*
* Start up the ZooKeeper server.
*
* @param args the configfile or the port datadir [ticktime]
*/
public static void main(String[] args) {
ZooKeeperServerMain main = new ZooKeeperServerMain();
try {
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);
}
protected void initializeAndRun(String[] args)
throws ConfigException, IOException
{
try {
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
//再次进行配置文件zoo.cfg的解析
ServerConfig config = new ServerConfig();
if (args.length == 1) {
config.parse(args[0]);
} else {
config.parse(args);
}
//创建服务器实例ZooKeeperServer。
runFromConfig(config);
}
/**
* Run from a ServerConfig.
* @param config ServerConfig to use.
* @throws IOException
*/
public void runFromConfig(ServerConfig config) throws IOException {
LOG.info("Starting server");
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
ZooKeeperServer zkServer = new ZooKeeperServer(); //创建服务器实例ZooKeeperServer。
//创建ZooKeeper数据管理器FileTxnSnapLog
FileTxnSnapLog ftxn = new FileTxnSnapLog(new
File(config.dataLogDir), new File(config.dataDir));
zkServer.setTxnLogFactory(ftxn);
//设置服务器tickTime和会话超时时间限制
zkServer.setTickTime(config.tickTime);
zkServer.setMinSessionTimeout(config.minSessionTimeout);
zkServer.setMaxSessionTimeout(config.maxSessionTimeout);
//创建ServerCnxnFactory
cnxnFactory = ServerCnxnFactory.createFactory();
//初始化ServerCnxnFactory
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//启动ServerCnxnFactory主线程 以及后续操作。。
cnxnFactory.startup(zkServer);
cnxnFactory.join();
if (zkServer.isRunning()) {
zkServer.shutdown();
}
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Server interrupted", e);
}
}
/**
* Shutdown the serving instance
*/
protected void shutdown() {
cnxnFactory.shutdown();
}
}
初始化的步骤如下。
(1)创建服务器统计器ServerStats。
ServerStats是ZooKeeper服务器运行时的统计器,包含了最基本的运行时通信。ZooKeeper服务器基本统计信息如下表。
(2)创建ZooKeeper数据管理器FileTxnSnapLog。
FileTxnSnapLog是ZooKeeper上层服务与底层数据存储之间的对接层,提供了一系列操作数据文件的接口,包括事务日志文件和快照文件。ZooKeeper根据zoo.cfg文件解析出的快照数据目录dataDIr和事务日志目录dataLodDir来创建FIleTxnSnapLog。
(3)设置服务器tickTime和会话超时时间限制。
(4)创建ServerCnxnFactory。
从3.4.0之后,使用者可以通过设置系统属性zookeeper.serverCnxnFactory 来指定使用ZooKeeper自己实现的NIO还是使用Netty框架来作为ZooKeeper服务端网络连接工厂。
public abstract class ServerCnxnFactory {
public static final String ZOOKEEPER_SERVER_CNXN_FACTORY = "zookeeper.serverCnxnFactory";
public abstract int getLocalPort();
public abstract Iterable getConnections();
public abstract void closeSession(long sessionId);
public abstract void configure(InetSocketAddress addr,
int maxClientCnxns) throws IOException;
protected SaslServerCallbackHandler saslServerCallbackHandler;
public Login login;
/** Maximum number of connections allowed from particular host (ip) */
public abstract int getMaxClientCnxnsPerHost();
/** Maximum number of connections allowed from particular host (ip) */
public abstract void setMaxClientCnxnsPerHost(int max);
public abstract void startup(ZooKeeperServer zkServer)
throws IOException, InterruptedException;
public abstract void join() throws InterruptedException;
public abstract void shutdown();
public abstract void start();
protected ZooKeeperServer zkServer;
//注册ZooKeeper服务器实例。
final public void setZooKeeperServer(ZooKeeperServer zk) {
this.zkServer = zk;
if (zk != null) {
zk.setServerCnxnFactory(this);
}
}
//创建ServerCnxnFactory。
static public ServerCnxnFactory createFactory() throws IOException {
String serverCnxnFactoryName =
System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
if (serverCnxnFactoryName == null) {
serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
}
try {
return (ServerCnxnFactory) Class.forName(serverCnxnFactoryName)
.newInstance();
} catch (Exception e) {
IOException ioe = new IOException("Couldn't instantiate "
+ serverCnxnFactoryName);
ioe.initCause(e);
throw ioe;
}
}
}
public class NIOServerCnxnFactory extends ServerCnxnFactory implements Runnable {
... ...
Thread thread;
@Override
//初始化ServerCnxnFactory
//ZooKeeper首先会初始化一个Thread,作为整个ServerCnxnFactory的主线程,然后再初始化NIO服务器
public void configure(InetSocketAddress addr, int maxcc) throws IOException {
configureSaslLogin();
thread = new Thread(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);
}
@Override
//启动ServerCnxnFactory主线程
public void start() {
// ensure thread is started once and only once
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
@Override
public void startup(ZooKeeperServer zks) throws IOException,
InterruptedException {
start(); //启动ServerCnxnFactory主线
zks.startdata(); //恢复本地数据。
zks.startup();
setZooKeeperServer(zks);
}
// 启动主线程执行 run方法
public void run() {
while (!ss.socket().isClosed()) {
try {
selector.select(1000);
Set selected;
synchronized (this) {
selected = selector.selectedKeys();
}
ArrayList selectedList = new ArrayList(
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");
}
}
(5)初始化ServerCnxnFactory。
ZooKeeper首先会初始化一个Thread,作为整个ServerCnxnFactory的主线程,然后再初始化NIO服务器。
(6)启动ServerCnxnFactory主线程。
启动(5)中已经初始化的主线程ServerCnxnFactory的主逻辑(run方法)。需要注意的是,虽然ZooKeeper的NIO服务器已经对外开放,客户端能够访问到ZooKeeper的客户端服务端口2181,但此时ZooKeeper服务器是无法正常处理客户端请求的。
//恢复本地数据。
public void startdata()
throws IOException, InterruptedException {
//check to see if zkDb is not null
if (zkDb == null) {
zkDb = new ZKDatabase(this.txnLogFactory);
}
if (!zkDb.isInitialized()) {
loadData();
}
}
public void startup() {
if (sessionTracker == null) {
createSessionTracker(); //创建并启动会话管理
}
startSessionTracker(); //开启会话管理器
setupRequestProcessors(); //初始化ZooKeeper的请求处理链
registerJMX(); //注册JMX服务。
synchronized (this) {
running = true;
notifyAll();
}
}
//创建并启动会话管理
protected void createSessionTracker() {
sessionTracker = new SessionTrackerImpl(this, zkDb.getSessionWithTimeOuts(),
tickTime, 1);
}
//开启会话管理器
protected void startSessionTracker() {
((SessionTrackerImpl)sessionTracker).start();
}
//初始化ZooKeeper的请求处理链
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor syncProcessor = new SyncRequestProcessor(this,
finalProcessor);
((SyncRequestProcessor)syncProcessor).start();
firstProcessor = new PrepRequestProcessor(this, syncProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
(7)恢复本地数据。
每次在ZooKeeper启动的时候,都需要从本地快照文件和事务日志文件中进行数据恢复。
(8)创建并启动会话管理。
在ZooKeeper启动阶段,会创建一个会话管理器SessionTracker.SessionTracker主要负责ZooKeeper服务端的会话管理。创建SessionTracker的时候,会初始化expirationInterval、nextExpirationTime和SessionWithTimeout,同时还会计算出一个初始化的sessionId。
(9)初始化ZooKeeper的请求处理链。
ZooKeeper的请求处理方式是典型的责任链模式的实现,在ZooKeeper服务器上,会有多个请求处理器依次来处理一个客户端请求。在服务器启动的时候,会将这些请求处理器串联起来形成一个请求处理链。单机版服务器的请求处理链包括PrepRequestProcessor、syncRequestProcessor和FinalRequestProcessor单个请求处理器,如下如所示。
(10) 注册JMX服务。
ZooKeeper会将服务器运行的一些信息以JML的方式暴露给外部。
(11)注册ZooKeeper服务器实例。
在(6)中,ZooKeeper已经将ServerCnxnFactory主线程启动,但是同时我们提到此时ZooKeeper依旧无法处理客户端请求,原因就是此时网络层尚不能够访问ZooKeeper服务器实例。在经过后续步骤的初始化后,ZooKeeper服务器实例已经初始化完毕,只需注册给ServerCnxnFactory即可,之后,ZooKeeper就可以对外提供正常服务了。