zookeeper使用及源码分析(一)

        扑街前言:前端时间的文章都是在说RPC框架的网络通讯方式,这段时间我们了解一个金典的rpc框架zookeeper的使用及源码,zookeeper目前最常使用的就是作为注册中心,和dubbo结合就是一个实用的分布式架构,所以本篇已经后续的文章也是根据注册中心的思路来了解zookeeper。


        关于zookeeper的安装不在这里赘述了,这个十分简单,网上很多教程跟着做就行了,我们这次主要说的是zookeeper的一些使用和基本概念。这里提一下zookeeper的数据存储目录是放在了conf文件夹下的zoo文件(zoo文件是复制的zoo_sample文件)中,比如下面文件截图就是:D:/tools/zookeeper-3.4.9/data路径。


 zookeeper的基础概念

        zookeeper 是Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。

        zookeeper 的架构通过冗余服务实现高可用行(CP)。

        zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封住起来,构成一个高效可靠的原语集,并以一系列简单 易用的接口提供给用户使用。

        一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式 协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。


zookeeper数据结构

        zookeeper本身是一个树形目录服务(名称空间),非常类似于标准文件系统,key-value 的形式存储。名称 key 由斜线 / 分 割的一系列路径元素,zookeeper 名称空间中的每个节点都是由一个路径来标识的。

        它的每一个节点可以存储数据,每一个节点还有对应的状态信息。zookeeper 的key 可以理解为:节点的完整路径;value 就是:节点中的数据。


zookeeper节点类型


zookeeper客户端

        zookeeper 对Java提供了三种客户端:zookeeper 原生的API,Curator,zkClient,zookeeper 原生的API偏底层不是很好用,一般是用的就是Curator,而Curator 也就是封装了这些API。

        我们这里就不对客户端的源码进行分析了,我们主要讨论的zookeeper 本身的源码。


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个。

zookeeper线程模型

        上面说了在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服务端启动的代码。

        首先我们需要先确认找到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);
			}
		}
	}
}

ServerCnxnFactory创建

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;
	}
}

ServerCnxnFactory配置

        这个方法主要就是对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<NIOServerCnxn>(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);
}

ServerCnxnFactory启动

        当上面的配置完成之后,也就是该创建的也都创建好了,那么下一步就是启动。

        其实这个也相对比较简单,就是启动上面创建的每一个线程,然后将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 对于业务处理的流程,这个还是比较复杂,一篇文章还是说不完的,就到这,结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值