学习目标
-
理清Zookeeper的Session创建、刷新和过期流程分析
-
明确Zookeeper的核心业务调用链
第1章 Session创建
上文给大家讲过Zookeeper的应用了,实际上Zookeeper分为两个模块,server端和client端,server端实现了所有的zookeeper业务逻辑,而client端就是封装了server端的一些方法调用。既然存在两个模块,那肯定涉及到了网络通信,ZooKeeper中使用ServerCnxnFactory管理与客户端的连接,其有两个实现,一个是NIOServerCnxnFactory,使用Java原生NIO实现;一个是NettyServerCnxnFactory,使用netty实现;使用ServerCnxn代表一个客户端与服务端的连接。从单机版启动中可以发现Zookeeper默认通信组件为NIOServerCnxnFactory。接下来我们先来看看Zookeeper中通信的流程图,然后再详细分析源码。
下面给出了我们客户端发起连接请求及服务端建立会话的全流程图
1.1 客户端发送请求
上文中已经讲过,建立连接是通过new ZooKeeper方法完成的,在ZooKeeper的构造方法中会创建一个ClientCnxn对象,并调用该对象的start方法,在该方法中会启动两个线程任务:sendThread和eventThread。
而sendThread线程就是我们去建立连接的核心线程,在该线程的run方法中实际上是通过一个while循环,不断的执行,如果是第一次进来会去创建连接,如果连接状态是CONNECTED的话,则会最大不超过10秒去发送一次Ping请求保证连接不断开。
源码比较长,有些不重要的代码就直接省略了。
public void run() {
//发送Ping的间隔
final int MAX_SEND_PING_INTERVAL = 10000; //10 seconds
while (state.isAlive()) {
try {
//如果状态是CONNECTING的话就去创建连接
if (!clientCnxnSocket.isConnected()) {
startConnect(serverAddress);
}
//如果已经连接成功,则最大不超过10秒发送一次心跳
if (state.isConnected()) {
//这段逻辑实际上就是控制心跳的是发送间隔,避免过多的发送
int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend() -
((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0);
if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
sendPing();
clientCnxnSocket.updateLastSend();
} else {
if (timeToNextPing < to) {
to = timeToNextPing;
}
}
}
}
}
void connect(InetSocketAddress addr) throws IOException {
SocketChannel sock = createSock();
try {
//会调用ZK的服务端完成会话创建
registerAndConnect(sock, addr);
} catch (IOException e) {
}
}
void registerAndConnect(SocketChannel sock, InetSocketAddress addr)
throws IOException {
sockKey = sock.register(selector, SelectionKey.OP_CONNECT);
//调用NIO开启会话
boolean immediateConnect = sock.connect(addr);
}
1.2 服务端接收连接
服务端由NIOServerCnxnFactory启动线程去接收请求,NIOServerCnxnFactory启动时会启动四类线程:
AcceptThread:该线程接收来自客户端的连接,并将其分配给SelectorThread(启动一个线程)。
SelectorThread:该线程执行select(),由于在处理大量连接时,select()会成为性能瓶颈,因此启动多个SelectorThread,使用系统属性zookeeper.nio.numSelectorThreads配置该类线程数,默认个数为 核心数/2。
WorkerThread:该线程执行基本的套接字读写,使用系统属性zookeeper.nio.numWorkerThreads配置该类线程数,默认为核心数∗2核心数∗2.如果该类线程数为0,则另外启动一线程进行IO处理,见下文worker thread介绍。
ConnectionExpirationThread:若连接上的session已过期,则关闭该连接。
1.2.1 AcceptThread
该线程会接收客户端的请求
public void run() {
while (!stopped && !acceptSocket.socket().isClosed()) {
select();
}
}
private void select() {
try {
//查找就绪的连接
selector.select();
Iterator<SelectionKey> selectedKeys =
selector.selectedKeys().iterator();
while (!stopped && selectedKeys.hasNext()) {
if (key.isAcceptable()) {
//1:和当前服务建立链接。
//2:获取远程客户端计算机地址信息。
//3:判断当前链接是否超出最大限制。
//4:调整为非阻塞模式。
//5:轮询获取一个SelectorThread,将当前链接分配给该SelectorThread。
//6:将当前请求添加到该SelectorThread的acceptedQueue中,并唤醒该SelectorThread。
if (!doAccept()) {
pauseAccept(10);
}
}
}
}
}
进入到doAccept方法中
private boolean doAccept() {
...
try {
//建立连接
sc = acceptSocket.accept();
accepted = true;
//获取远程计算机地址信息
InetAddress ia = sc.socket().getInetAddress();
int cnxncount = getClientCnxnCount(ia);
//判断是否超出最大客户端连接的限制
if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){
...
}
LOG.debug("Accepted socket connection from "
+ sc.socket().getRemoteSocketAddress());
//调整此通道的阻塞模式
sc.configureBlocking(false);
//轮询将此连接分配给一个SelectorThread
if (!selectorIterator.hasNext()) {
selectorIterator = selectorThreads.iterator();
}
SelectorThread selectorThread = selectorIterator.next();
//将新连接加入SelectorThread的acceptedQueue中,并唤醒SelectorThread
if (!selectorThread.addAcceptedConnection(sc)) {
...
}
acceptErrorLogger.flush();
} catch (IOException e) {
...
}
return accepted;
}
}
public boolean addAcceptedConnection(SocketChannel accepted) {
//将accepted添加到acceptedQueue
if (stopped || !acceptedQueue.offer(accepted)) {
return false;
}
//唤醒SelectorThread
wakeupSelector();
return true;
}
在addAcceptedConnection方法中会唤醒SelectorThread,所以,接下来,逻辑会进入到SelectorThread.run方法中
1.2.2 SelectorThread
该线程的主要作用是从Socket读取数据,并封装成workRequest,并将workRequest交给workerPool工作线程池处理,同时将acceptedQueue中未处理的连接取出,并未每个连接绑定OP_READ读事件,并封装对应的上下文对象NIOServerCnxn。SelectorThread的run方法如下:
public void run() {
//读取就绪的IO事件,交由worker thread处理,在ZookeeperServer的processPacket()中处理数据
select();
//把acceptedQueue队列中接收的连接,取出来注册OP_READ事件,
//并添加NIOServerCnxn对象与当前key绑定
//相当于给每个连接添加附加对象NIOServerCnxn(上下文对象)
processAcceptedConnections();
//遍历所有updateQueue,更新updateQueue中连接的监听事件
processInterestOpsUpdateRequests();
}
先来看看proces