Zookeeper-网络IO管理器ServerCnxnFactory

背景

Zookeeper作为一个服务器,自然要与客户端进行网络通信,如何高效的与客户端进行通信,让网络IO不成为ZooKeeper的瓶颈是ZooKeeper急需解决的问题,ZooKeeper中使用ServerCnxnFactory管理与客户端的连接,其有两个实现,一个是NIOServerCnxnFactory,使用Java原生NIO实现;一个是NettyServerCnxnFactory,使用netty实现;使用ServerCnxn代表一个客户端与服务端的连接.
这里写图片描述

ServerCnxnFactory

注:下文或注释中的连接就是客户端发起的TCP连接,也即SocketChannel
ZooKeeper可以通过设置系统属性zookeeper.serverCnxnFactory配置ServerCnxnFactory的实现类,默认使用NIOServerCnxnFactory

NIOServerCnxnFactory

实现思路

一般使用Java NIO的思路为使用1个线程组监听OP_ACCEPT事件,负责处理客户端的连接;使用1个线程组监听客户端连接的OP_READOP_WRITE事件,处理IO事件(netty便是如此实现).
但ZooKeeper并不是如此划分线程功能的,NIOServerCnxnFactory启动时会启动四类线程

  1. accept thread:该线程接收来自客户端的连接,并将其分配给selector thread(启动一个线程)
  2. selector thread:该线程执行select(),由于在处理大量连接时,select()会成为性能瓶颈,因此启动多个selector thread,使用系统属性zookeeper.nio.numSelectorThreads配置该类线程数,默认个数为 /2 核 心 数 / 2 (至少一个)
  3. worker thread:该线程执行基本的套接字读写,使用系统属性zookeeper.nio.numWorkerThreads配置该类线程数,默认为 2 核 心 数 ∗ 2 .如果该类线程数为0,则另外启动一线程进行IO处理,见下文worker thread介绍
  4. connection expiration thread:若连接上的session已过期,则关闭该连接

可以看出,ZooKeeper中对线程需要处理的工作做了更细的拆分.其认为在有大量客户端连接的情况下,selector.select()会成为性能瓶颈,因此其将selector.select()拆分出来,交由selector thread处理.

线程间通信

上述各类线程之间通过同步队列通信.这一小节我们看下各类线程通信使用哪几个同步队列?各有什么用处

SelectorThread.acceptedQueue

acceptedQueueLinkedBlockingQueue<SocketChannel>类型的,在selector thread中.其中包含了accept thread接收的客户端连接,由selector thread负责将客户端连接注册到selector上,监听OP_READOP_WRITE.

SelectorThread.updateQueue

updateQueueacceptedQueue一样,也是LinkedBlockingQueue<SocketChannel>类型的,在selector thread中.但是要说明白该队列的作用,就要对Java NIO的实现非常了解了.
Java NIO使用epoll系统调用,且是水平触发,也即若selector.select()发现socketChannel中有事件发生,比如有数据可读,只要没有将这些数据从socketChannel读取完毕,下一次selector.select()还是会检测到有事件发生,直至数据被读取完毕.
ZooKeeper一直认为selector.select()是性能的瓶颈,为了提高selector.select()的性能,避免上述水平触发模式的缺陷,ZooKeeper在处理IO的过程中,会让socketChannel不再监听OP_READOP_WRITE事件,这样就可以减轻selector.select()的负担.
此时便出现一个问题,IO处理完毕后,如何让socketChannel再监听OP_READOP_WRITE事件?
有的小伙伴可能认为这件事情非常容易,worker thread处理IO结束后,直接调用key.interestOps(OP_READ & OP_WRITE)不就可以了吗?事情并没有这简单,是因为selector.select()是在selector thread中执行的,若在selector.select()的过程中,worker thread调用了key.interestOps(OP_READ & OP_WRITE),可能会阻塞selector.select().ZooKeeper为了追求性能的极致,设计为由selector thread调用key.interestOps(OP_READ & OP_WRITE),因此worker thread就需在IO处理完毕后告诉selector thread该socketChannel可以去监听OP_READOP_WRITE事件了,updateQueue就是存放那些需要监听OP_READOP_WRITE事件的socketChannel.

NIOServerCnxn.outgoingBuffers

outgoingBuffers存放待发送给客户端的响应数据.

注:个人推测,既然key.interestOps(OP_READ & OP_WRITE)会阻塞selector.select(),那么accepted.register(selector, SelectionKey.OP_READ)也会阻塞selector.select(),因此接收到的客户端连接注册到selector上也要在selector thread上执行,这也是acceptedQueue存在的理由

accept thread

 /**
         * 接收新的socket连接,每个IP地址有其连接个数上限.使用轮询为新连接分配selector thread
         * @return whether was able to accept a connection or not
         */
        private boolean doAccept() {
            boolean accepted = false;
            SocketChannel sc = null;
            try {
                sc = acceptSocket.accept();
                accepted = true;
                //防止来自一个IP地址的连接过多
                InetAddress ia = sc.socket().getInetAddress();
                int cnxncount = getClientCnxnCount(ia);
                //判断是否最大客户端连接的限制
                if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns) {
                    throw new IOException("Too many connections from " + ia
                            + " - max is " + maxClientCnxns);
                }

                LOG.info("Accepted socket connection from "
                        + sc.socket().getRemoteSocketAddress());
                sc.configureBlocking(false);

                // Round-robin assign this connection to a selector thread
                //使用轮询将连接分派给某个selector线程
                if (!selectorIterator.hasNext()) {
                    selectorIterator = selectorThreads.iterator();
                }
                SelectorThread selectorThread = selectorIterator.next();
                //将新连接加入selector thread 的acceptedQueue中
                if (!selectorThread.addAcceptedConnection(sc)) {
                    throw new IOException(
                            "Unable to add connection to selector queue"
                                    + (stopped ? " (shutdown in progress)" : ""));
                }
                acceptErrorLogger.flush();
            } catch (IOException e) {
                // accept, maxClientCnxns, configureBlocking
                acceptErrorLogger.rateLimitLog(
                        "Error accepting new connection: " + e.getMessage());
                fastCloseSock(sc);
            }
            return accepted;
        }
    }

在accept thread 的run()中,其执行selector.select(),并调用doAccept()接收客户端连接,将其添加至SelectorThread.acceptedQueue()

selector thread

         @Override
        public void run() {
            try {
                while (!stopped) {
                    try {
                        //1.调用select()读取就绪的IO事件,交由worker thread处理
                        select();
                        //2.处理accept线程新分派的连接,
                        // (1)将新连接注册到selector上;(2)包装为NIOServerCnxn后注册到NIOServerCnxnFactory中
                        processAcceptedConnections();
                        //3.更新updateQueue中连接的监听事件
                        processInterestOpsUpdateRequests();
                    } catch (RuntimeException e) {
                        LOG.warn("Ignoring unexpected runtime exception", e);
                    } catch (Exception e) {
                        LOG.warn("Ignoring unexpected exception", e);
                    }
                }
                //执行清理操作,关闭所有在selector上等待的连接
                ...
            } finally {
                ...
                //清理工作
            }
        }

在selector thread的run()中,主要执行3件事情

  1. 调用select()读取就绪的IO事件,交由worker thread处理(在交由worker thread 处理之前会调用key.interestOps(0))
  2. 处理accept线程新分派的连接,
    (1)将新连接注册到selector上;
    (2)包装为NIOServerCnxn后注册到NIOServerCnxnFactory中
  3. 更新updateQueue中连接的监听事件

worker thread

ZooKeeper中通过WorkerService管理一组worker thread线程,其有两种管理模式:

模式名解释使用场景实现
可指定线程模式将任务指定由某一线程完成,若一系列任务需有序完成,可使用此种模式,将需按序完成的任务指定到同一线程同一会话下的一系列请求生成N个ExecutorService,每个ExecutorService只包含一个线程
不可指定线程模式任务提交后,由WorkerService随机指定线程完成,任务之间无顺序要求则使用该模式执行网络IO生成1个ExecutorService,其中有N个线程

由于各连接的网络IO任务之间无顺序要求,NIOServerCnxnFactory使用的WorkerService采用不可指定线程模式.

    /**
     * Schedule work to be done by the thread assigned to this id. Thread
     * assignment is a single mod operation on the number of threads.  If a
     * worker thread pool is not being used, work is done directly by
     * this thread.
     * 根据id取模将workRequest分配给对应的线程.如果没有使用worker thread
     * (即numWorkerThreads=0),则启动ScheduledWorkRequest线程完成任务,当前
     * 线程阻塞到任务完成.
     *
     * @param workRequest 待处理的IO请求
     * @param id          根据此值选择使用哪一个thread处理workRequest
     */
    public void schedule(WorkRequest workRequest, long id) {
        if (stopped) {
            workRequest.cleanup();
            return;
        }

        ScheduledWorkRequest scheduledWorkRequest =
                new ScheduledWorkRequest(workRequest);

        // If we have a worker thread pool, use that; 
        // otherwise, do the work directly.
        int size = workers.size();
        if (size > 0) {
            try {
                // make sure to map negative ids as well to [0, size-1]
                int workerNum = ((int) (id % size) + size) % size;
                ExecutorService worker = workers.get(workerNum);
                worker.execute(scheduledWorkRequest);
            } catch (RejectedExecutionException e) {
                LOG.warn("ExecutorService rejected execution", e);
                workRequest.cleanup();
            }
        } else {
            // When there is no worker thread pool, do the work directly
            // and wait for its completion
            scheduledWorkRequest.start();
            try {
                scheduledWorkRequest.join();
            } catch (InterruptedException e) {
                LOG.warn("Unexpected exception", e);
                Thread.currentThread().interrupt();
            }
        }
    }

在上文介绍worker thread时,说”如果该类线程数为0,则使用selector thread 直接执行IO读写”,但从上面源码可以看出,若worker thread个数为0,为每个网络IO启动一个线程去执行,且主线程阻塞都到网络IO执行完毕,这简直是浪费资源,既然要阻塞到网络IO执行完毕,为何还要单独启动一个线程?个人认为可能是遗留代码或为日后扩展做准备,才会有如此不合理的代码.因此一定不能将worker thread的个数设置为0.
我们继续看ScheduledWorkRequest是如何处理网络IO的

        @Override
        public void run() {
            try {
                // Check if stopped while request was on queue
                if (stopped) {
                    workRequest.cleanup();
                    return;
                }
                workRequest.doWork();
            } catch (Exception e) {
                LOG.warn("Unexpected exception", e);
                workRequest.cleanup();
            }
        }
        @Override
        public void doWork() throws InterruptedException {
            //如果Channel已经关闭则清理该SelectionKey
            if (!key.isValid()) {
                selectorThread.cleanupSelectionKey(key);
                return;
            }
            //1.如果可读或可写,则调用NIOServerCnxn.doIO()方法,通知NIOServerCnxn连接对象进行IO读写及处理
            if (key.isReadable() || key.isWritable()) {
                //调用NIOServerCnxn的doIO()完成IO处理
                cnxn.doIO(key);

                // Check if we shutdown or doIO() closed this connection
                //如果已经shutdown则关闭连接
                if (stopped) {
                    cnxn.close();
                    return;
                }
                //如果Channel已经关闭则清理该SelectionKey
                if (!key.isValid()) {
                    selectorThread.cleanupSelectionKey(key);
                    return;
                }
                //2.更新该会话的过期时间
                touchCnxn(cnxn);
            }

            //3.已经处理完读写,重新标记该连接已准备好新的select事件监听
            cnxn.enableSelectable();
            //把该连接重新放到selectThread的updateQueue中,selectThread会在处理处理完所有Channel的读写和新连接后,更新此Channel的注册监听事件
            if (!selectorThread.addInterestOpsUpdateRequest(key)) {
                cnxn.close();
            }
        }

除去一些健壮性代码,主要完成3件事:

  1. NIOServerCnxn.doIO()方法,通知NIOServerCnxn连接对象进行IO读写及处理
  2. 更新该连接的过期时间
  3. 网络IO已处理完毕,修改selectable标志位和将socketChannel添加至selector thread的updateQueue中,其作用已在前文说明.

在selector thread处理accept thread接收的连接时,除了将新连接注册到selector上之外,还将连接包装为NIOServerCnxn后注册到NIOServerCnxnFactory中.NIOServerCnxn是对客户端连接的封装,worker thread中调用NIOServerCnxn.doIO()处理网络IO.详见ZooKeeper-客户端连接ServerCnxn之NIOServerCnxn

connection expiration thread

此线程用于清理过期的连接,主要方法如下:

        @Override
        public void run() {
            try {
                while (!stopped) {
                    long waitTime = cnxnExpiryQueue.getWaitTime();
                    if (waitTime > 0) {
                        Thread.sleep(waitTime);
                        continue;
                    }
                    for (NIOServerCnxn conn : cnxnExpiryQueue.poll()) {
                        conn.close();
                    }
                }

            } catch (InterruptedException e) {
                LOG.info("ConnnectionExpirerThread interrupted");
            }
        }

此线程的工作原理详见Zookeeper-连接和会话的过期清理策略(ExpiryQueue)

NettyServerCnxnFactory

NettyServerCnxnFactory使用netty进行网络IO,但是其使用netty3.*版本,与4.*版本的实现思路虽然一致,但API差别很大,为此不再深入研究NettyServerCnxnFactory,简单介绍下其与NIOServerCnxnFactory的不同.

不同点NIONetty
accept事件启动1个accept threadboss group处理accept事件,默认启动1个线程
select()启动select thread添加handler时调用addLast(EventExecutorGroup, ChannelHandler…),则handler处理IO事件会在EventExecutorGroup中进行
网络IO启动worker thread启动work group处理网络IO,默认启动 2 核 心 数 ∗ 2 个线程
处理读事件在worker thread中调用NIOServerCnxn.doIO()处理在handler中处理读事件
粘包拆包通过lenBuffer和incomingBuffer解决该问题,代码很复杂插入处理粘包拆包的handler即可
处理写事件执行FinalRP.processRequest()的线程与worker thread通过NIOServerCnxn.outgoingBuffers进行通信,由worker thread批量写netty天生支持异步写,若当前线程为EventLoop线程,则将待写入数据存放到ChannelOutboundBuffer中.若当前线程不是EventLoop线程,构造写任务添加至EventLoop任务队列中
直接内存使用ThreadLocal的直接内存记不太清楚netty中如何使用直接内存了,但netty支持直接内存,且使用较为方便
处理连接关闭启动connection expiration thread管理连接在handler中处理连接

注:上述区别是将netty4.*版本与NIOServerCnxnFactory的对比,由于ZooKeeper使用netty3.*,因此其NettyServerCnxnFactory中存在一些无用代码,比如处理粘包拆包的代码

从上述的比较中可以看出使用netty处理网络IO比基于Java NIO自己编码方便太多了,netty大法好~~

总结

总结下线程通信所用的三个队列:

  1. SelectorThread.acceptedQueue:accept thread和selector thread通信
  2. SelectorThread.updateQueue:worker thread和selector thread通信
  3. NIOServerCnxn.outgoingBuffers:worker thread和请求处理线程通信

参考

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值