扑街前言:前端时间的文章都是在说RPC框架的网络通讯方式,这段时间我们了解一个金典的rpc框架zookeeper的使用及源码,zookeeper目前最常使用的就是作为注册中心,和dubbo结合就是一个实用的分布式架构,所以本篇已经后续的文章也是根据注册中心的思路来了解zookeeper。
关于zookeeper的安装不在这里赘述了,这个十分简单,网上很多教程跟着做就行了,我们这次主要说的是zookeeper的一些使用和基本概念。这里提一下zookeeper的数据存储目录是放在了conf文件夹下的zoo文件(zoo文件是复制的zoo_sample文件)中,比如下面文件截图就是:D:/tools/zookeeper-3.4.9/data路径。
zookeeper 是Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。
zookeeper 的架构通过冗余服务实现高可用行(CP)。
zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封住起来,构成一个高效可靠的原语集,并以一系列简单 易用的接口提供给用户使用。
一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式 协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
zookeeper本身是一个树形目录服务(名称空间),非常类似于标准文件系统,key-value 的形式存储。名称 key 由斜线 / 分 割的一系列路径元素,zookeeper 名称空间中的每个节点都是由一个路径来标识的。
它的每一个节点可以存储数据,每一个节点还有对应的状态信息。zookeeper 的key 可以理解为:节点的完整路径;value 就是:节点中的数据。
zookeeper 对Java提供了三种客户端:zookeeper 原生的API,Curator,zkClient,zookeeper 原生的API偏底层不是很好用,一般是用的就是Curator,而Curator 也就是封装了这些API。
我们这里就不对客户端的源码进行分析了,我们主要讨论的zookeeper 本身的源码。
zookeeper 的网络通信默认基于Java NIO,也可以基于netty。我们这里再复习一下NIO的三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)。具体的可以看下我写的关于NIO的文章,还有关于netty的文章。
还要再说一下主从Reactor多线程模型,也就是1 + M + N线程模式,当client 请求过来时,会有一个主线程Main Reactor对象,用于监听连接,然后通过Acceptor 处理客户端连接事件并注册给subReactor 线程socketChannel,subReactor 对象会创建Handle 进行各个事件的处理,当有事件发生时,subReactor 会调用对应的handle 进行处理,handle 通过read 获取相应的请求,然后分发给worker 线程池,worker 线程池业务会分配真正的线程进行业务处理,当处理完成之后,又会返回对应的响应对象给 handle,handle 会将响应的结果send 发送client。注意的是请求连接的线程是1个,读写处理的线程是M个,业务逻辑处理的线程是N个。
上面说了在netty中学习到的主从Reactor多线程模型,zookeeper其实也对主从Reactor多线程模型有自己的封装,AcceptThread:负责处理连接的建立;SelectorThread:负责处理监听客户端连接的IO事件,检测到IO事件后封装事件信息交由worker thread支持IO操作,线程个数为:sqrt(numCores / 2),至少一个;ConnectionExpirerThread:负责监听连接会话是否超时;RreRequestProcessor/SyncRreRequestProcessor:跟请求处理相关的处理器线程。
下面我们看下具体IO模型,具体流程就是:当有client 请求过来时,会先在acceptThread 中通过selector 注册连接并写入SelectorThread 中的acceptedQueue 中,然后再通过selector轮询将具体的读写事件封装到IOWorkRequrst 中,然后再是WorkService线程池中的scheduleWorkRequset 找到具体现线程来处理读写事件。其中在注册监听连接的时候还会为NIOServerCnxnFactory 对象添加ExpiryQueue 对象,然后ConnectionExpireThread线程不断的检测连接是否过期。
那么我们根据上面的图中的内容,跟一下zookeeper服务端启动的代码。
首先我们需要先确认找到zookeeper中服务端项目的具体位置,然后我们可以在bin文件夹中找到具体启动文件,而启动文件中描述了主方法的文件位置以及名称。
这样我们就找到了server子项目中的QuorumPeerMain类,其中的主方法能让我们一步一步debug 往下走,主方法我就不展示了,主方法后的调用代码如下。
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包含一个Timer定时器和PurgeTask清理任务。PurgeTask 是基于TimerTask实现的;
* 首先认知下zookeeper主要存放了两类文件,snapshot和log,snapshot是数据的快照,log是与snapshot关联一致的事务日志
*/
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(
config.getDataDir(),
config.getDataLogDir(),
config.getSnapRetainCount(),
config.getPurgeInterval());
purgeMgr.start();
/**
* 判断是否是集群启动还是单机启动
* 初次运行到这如果报错:
* 1、添加必要的依赖
* 2、重新编译zookeeper-jute
* 必要情况下编译打包安装整个项目(跳过测试)
*/
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);
}
}
从上面代码中的单个启动一步一步往下跟会找到一个initializeAndRun方法,其中runFromConfig方法的调用就是zk 真正的启动内容了。
这里面有一大段的内容,其中需要关注的ServerCnxnFactory 对象的创建、配置以及对应的服务启动,下面我们一点一点的分析。
/**
* Run from a ServerConfig.
* @param config ServerConfig to use.
* @throws IOException
* @throws AdminServerException
*/
public void runFromConfig(ServerConfig config) throws IOException, AdminServerException {
LOG.info("Starting server");
/**
* 事务日志和数据快照文件操作
*/
FileTxnSnapLog txnLog = null;
try {
/**
* 开启指标度量
*/
try {
metricsProvider = MetricsProviderBootstrap.startMetricsProvider(
config.getMetricsProviderClassName(),
config.getMetricsProviderConfiguration());
} catch (MetricsProviderLifeCycleException error) {
throw new IOException("Cannot boot MetricsProvider " + config.getMetricsProviderClassName(), error);
}
ServerMetrics.metricsProviderInitialized(metricsProvider);
ProviderRegistry.initialize();
// 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
/**
* dataDir:事务日志目录---config.dataLogDir
* snapDir:数据快照目录---config.dataDir
* 主要目的是存储内存数据库序列化后的快照路径。
* 如果没有配置事务日志(即dataLogDir配置项)的路径,那么ZooKeeper的事务日志也存放在数据目录中
*/
txnLog = new FileTxnSnapLog(config.dataLogDir, config.dataDir);
/**
* in 3.6 is very handy to identify JVM pauses caused by either GC or other host related (OS/networking/disk/kernel/...) issues.
*/
JvmPauseMonitor jvmPauseMonitor = null;
if (config.jvmPauseMonitorToRun) {
jvmPauseMonitor = new JvmPauseMonitor(config);
}
/**
* 创建真正的zookeeper Server对象
*/
final ZooKeeperServer zkServer = new ZooKeeperServer(jvmPauseMonitor, txnLog, config.tickTime, config.minSessionTimeout, config.maxSessionTimeout, config.listenBacklog, null, config.initialConfig);
txnLog.setServerStats(zkServer.serverStats());
// Registers shutdown handler which will be used to know the
// server error or shutdown state changes.
/**
* 注册zookeeper服务关闭监听处理
*/
final CountDownLatch shutdownLatch = new CountDownLatch(1);
zkServer.registerServerShutdownHandler(new ZooKeeperServerShutdownHandler(shutdownLatch));
// Start Admin server
/**
* The AdminServer
* New in 3.5.0: The AdminServer is an embedded Jetty server that provides an HTTP interface to the four letter word commands.
* By default, the server is started on port 8080, and commands are issued by going to the URL “/commands/[command name]”
* http://localhost:8080/commands
*/
adminServer = AdminServerFactory.createAdminServer();
adminServer.setZooKeeperServer(zkServer);
adminServer.start();
boolean needStartZKServer = true;
if (config.getClientPortAddress() != null) {
/**
* 网络IO管理器ServerCnxnFactory
* Zookeeper作为一个服务器,自然要与客户端进行网络通信,如何高效的与客户端进行通信,让网络IO不成为ZooKeeper的瓶颈是ZooKeeper急需解决的问题
* ZooKeeper中使用ServerCnxnFactory管理与客户端的连接,有两个实现
* 1、NIOServerCnxnFactory 封装的是java原生 NIO
* 2、NettyServerCnxnFactory 使用netty作为网络通信模块
*
* 使用ServerCnxn代表一个客户端与服务端的连接,有两个实现:
* 1、NIOServerCnxn:封装了SocketChannel操作
* 2、NettyServerCnxn
*/
cnxnFactory = ServerCnxnFactory.createFactory();
/**
* addr:主机地址及端口等信息,源自zoo.cfg中的clientPort等配置项,服务端绑定该地址端口启动
* maxcc:最大连接数量,源自zoo.cfg中的maxClientCnxns配置项
* backlog: tcp backlog
* secure:ssl
*
* 在configure中做了如下几件事
* 1、初始化ExpiryQueue和expirerThread
* 2、按需创建SelectorThread,添加到集合
* 3、打开一个ServerSocketChannel,绑定上地址端口,
* 4、创建一个AcceptThread,将ServerSocketChannel注册到AcceptThread
*/
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), false);
/**
* 启动zk服务:
*/
cnxnFactory.startup(zkServer);
// zkServer has been started. So we don't need to start it again in secureCnxnFactory.
needStartZKServer = false;
}
//基于ssl 安全连接
if (config.getSecureClientPortAddress() != null) {
secureCnxnFactory = ServerCnxnFactory.createFactory();
secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);
secureCnxnFactory.startup(zkServer, needStartZKServer);
}
/**
* 容器节点管理器
*/
containerManager = new ContainerManager(
zkServer.getZKDatabase(),
zkServer.firstProcessor,
Integer.getInteger("znode.container.checkIntervalMs", (int) TimeUnit.MINUTES.toMillis(1)),
Integer.getInteger("znode.container.maxPerMinute", 10000),
Long.getLong("znode.container.maxNeverUsedIntervalMs", 0)
);
containerManager.start();
ZKAuditProvider.addZKStartStopAuditLog();
// Watch status of ZooKeeper server. It will do a graceful shutdown
// if the server is not running or hits an internal error.
shutdownLatch.await();//阻塞等待zookeeper服务关闭或出现内部错误
shutdown();
if (cnxnFactory != null) {
cnxnFactory.join();
}
if (secureCnxnFactory != null) {
secureCnxnFactory.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();
}
if (metricsProvider != null) {
try {
metricsProvider.stop();
} catch (Throwable error) {
LOG.warn("Error while stopping metrics", error);
}
}
}
}
public static ServerCnxnFactory createFactory() throws IOException {
/**
* 获取系统参数:zookeeper.serverCnxnFactory
*/
String serverCnxnFactoryName = System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
if (serverCnxnFactoryName == null) {
//如果没有配置zookeeper网络IO管理器 则默认采用JAVA NIO
serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
/**
* 可以配置netty,通过系统参数zookeeper.serverCnxnFactory配置如下值:
* org.apache.zookeeper.server.NettyServerCnxnFactory
*/
}
try {
ServerCnxnFactory serverCnxnFactory = (ServerCnxnFactory) Class.forName(serverCnxnFactoryName)
.getDeclaredConstructor()
.newInstance();
LOG.info("Using {} as server connection factory", serverCnxnFactoryName);
return serverCnxnFactory;
} catch (Exception e) {
IOException ioe = new IOException("Couldn't instantiate " + serverCnxnFactoryName, e);
throw ioe;
}
}
这个方法主要就是对NIOServerCnxnFactory 对象的配置(因为如果没有指定netty的话,zk默认就是NIO所以这里跟的时候就是选择了NIO的实现)。
其中上面流程图中所说的ExpiryQueue 对象(用于存储连接)、ConnectionExpirerThread 线程(用于监听ExpiryQueue 对象中的连接是否已经失效)都是在这里创建的(注意只是创建,这个方法里面所有的线程都只是创建,并没有启动)。
这里还有一个numSelectorThreads 属性(这个是用于下面创建线程时使用的),这个属性的值就是当前系统核数除以2后再开根号的整数,最少1个,也就是说我的电脑如果是8核,那么就是8 / 2 = 4 在开根号 = 2个,如果是4核,那么numSelectorThreads 的大小就是1。除了这个之外,还有一个就是numWorkerThreads 属性,这个也是用于下面创建线程时使用的,这个的值就是当前系统核数的2倍。
上面的流程图也可以看出,SelectorThread 线程池就是用于轮询检测并分发读写事件的,这个区别于Netty 因为Netty 的业务操作是没有单独再开线程处理的,所以Netty 的读写处理线程池大小是当前系统核数的两倍,而zk 是根据上面的numSelectorThreads 属性进行的创建,但是zk 中用于业务处理的线程池,也就是WorkService 线程池的大小就是跟Netty 一样的,是通过numWorkerThreads 属性进行的创建。
当上面的两个线程池创建完成之后,zk 又去创建了一个ServerSocketChannel,然后进行了绑定和设置阻塞,最后用这个ServerSocketChannel和selectorThreads 属性(这就是存储selectorThread 线程的线程池)还有配置文件中的客户端端口地址创建了AcceptThread 线程(注意:这个处理请求连接的线程池,线程数只有1个)。
/**
*
* @param addr 主机地址及端口等信息,源自zoo.cfg中的clientPort等配置项,服务端绑定该地址端口启动
* @param maxcc 最大连接数量,源自zoo.cfg中的maxClientCnxns配置项
* @param backlog tcp backlog
* @param secure SSL
* @throws IOException
*/
@Override
public void configure(InetSocketAddress addr, int maxcc, int backlog, boolean secure) throws IOException {
if (secure) {
throw new UnsupportedOperationException("SSL isn't supported in NIOServerCnxn");
}
configureSaslLogin();
maxClientCnxns = maxcc;
initMaxCnxns();
sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
// We also use the sessionlessCnxnTimeout as expiring interval for
// cnxnExpiryQueue. These don't need to be the same, but the expiring
// interval passed into the ExpiryQueue() constructor below should be
// less than or equal to the timeout.
/**
* 连接会话过期队列,连接被建立后也会被添加到该队列,由expirerThread不断去检测该队列中的连接是否过期
* 虽然交ExpiryQueue,但内部维护的是两个Map:elemMap 和 expiryMap
*/
cnxnExpiryQueue = new ExpiryQueue(sessionlessCnxnTimeout);
expirerThread = new ConnectionExpirerThread();//创建expirerThread 用于检测连接会话是否过期
/**
* 求SelectorThread:selector thread,使用系统属性zookeeper.nio.numSelectorThreads配置该类线程数,
* 默认个数为 Math.sqrt(核心数/2)(至少一个)
*/
int numCores = Runtime.getRuntime().availableProcessors();
// 32 cores sweet spot seems to be 4 selector threads
numSelectorThreads = Integer.getInteger(
ZOOKEEPER_NIO_NUM_SELECTOR_THREADS,
Math.max((int) Math.sqrt((float) numCores / 2), 1));
if (numSelectorThreads < 1) {
throw new IOException("numSelectorThreads must be at least 1");
}
/**
* WorkerThread:执行基本的套接字读写(IO操作)
* 使用系统属性zookeeper.nio.numWorkerThreads配置该类线程数,默认为核心数∗2
*/
numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
workerShutdownTimeoutMS = Long.getLong(ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT, 5000);
String logMsg = "Configuring NIO connection handler with "
+ (sessionlessCnxnTimeout / 1000) + "s sessionless connection timeout, "
+ numSelectorThreads + " selector thread(s), "
+ (numWorkerThreads > 0 ? numWorkerThreads : "no") + " worker threads, and "
+ (directBufferBytes == 0 ? "gathered writes." : ("" + (directBufferBytes / 1024) + " kB direct buffers."));
LOG.info(logMsg);
/**
* selectorThreads是一个HashsSet,创建所有的SelectorThread 添加到该Set集合中
* 譬如唐僧老师的电脑是8核数,就会创建2个SelectorThread
*/
for (int i = 0; i < numSelectorThreads; ++i) {
selectorThreads.add(new SelectorThread(i));
}
listenBacklog = backlog;
/**
* 打开一个ServerSocketChannel
* 绑定端口,设置非阻塞
*/
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
LOG.info("binding to port {}", addr);
if (listenBacklog == -1) {
ss.socket().bind(addr);
} else {
ss.socket().bind(addr, listenBacklog);
}
ss.configureBlocking(false);
/**
* 创建AcceptThread,将ServerSocketChannel 注册到该 acceptThread,并监听它的OP_ACCEPT事件
* acceptThread 线程只用于接收客户端的连接
*/
acceptThread = new AcceptThread(ss, addr, selectorThreads);
}
当上面的配置完成之后,也就是该创建的也都创建好了,那么下一步就是启动。
其实这个也相对比较简单,就是启动上面创建的每一个线程,然后将zookeeperServer 对象和ServerCnxnFactory 对象进行一个绑定,然后再确认数据库的是否正常,最后再启动各个组件就结束了。
当启动成功之后,我们对于主流程关注的就不多了,后面的代码就是一些监听之类的,还有就是CountDownLatch 的阻塞和释放主流程的内容。
@Override
public void startup(ZooKeeperServer zks, boolean startServer) throws IOException, InterruptedException {
/**
* 启动zookeeper服务
* 1、动所有的SelectorThread
* 2、启动acceptThread开始接收连接
* 3、启动expirerThread
* 4、创建workerPool
*/
start();
/**
* 将ServerCnxnFactory 和 ZooKeeperServer 绑定
*/
setZooKeeperServer(zks);
if (startServer) {
/**
* 创建zookeeper数据库,恢复会话和数据
*/
zks.startdata();
/**
* 启动各组件开始工作: 各组件基本都独占一个线程
* 譬如:
* 1、会话跟踪
* 2、构建请求处理器链并启动 (重要-关系到后续请求处理的逻辑)
* 3、启动请求限流器
* ......其他......
*/
zks.startup();
}
}
@Override
public void start() {
stopped = false;
if (workerPool == null) {
/**
* 创建workerPool,默认是创建了一个线程池,核心线程数是numWorkerThreads=cup核数*2
*/
workerPool = new WorkerService("NIOWorker", numWorkerThreads, false);
}
/**
* 启动所有的SelectorThread
*/
for (SelectorThread thread : selectorThreads) {
/**
* State.NEW:hread state for a thread which has not yet started.
*/
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();
}
}
总结一下,zookeeper 的服务端启动,相对于来说还是比较简单的,后续的文章再讨论一下zk 对于业务处理的流程,这个还是比较复杂,一篇文章还是说不完的,就到这,结束。