【深入剖析Tomcat笔记】第四篇 默认连接器

回顾

前一篇中,我们将ServerSocket拆分为 Connector、 Processor、 Request、 Response 四个部分。实现了对于简单ServerSocket主要功能拆分。Connector负责ServerSocket创建和socket接入,Processor负责请求解析,Request和Resposne分别对应输入输出。此篇通过研究Tomcat 4中 默认连接器,进一步优化服务容器。
【深入剖析Tomcat笔记】第三篇 基本容器模型
此篇Git源码:默认连接器

连接器分析

Tomcat连接器包含Connector和Processor两个部分。在上一个项目中Connector和Processor是 1:1关系,这样的形式在实际处理过程中请求数只能为1。这种方式进行解析,整体处理时间必然取决于time(Connecter) + time(Processor)。

整体解析时间
解析时间

首先可以想到的解决方案就是采用多线程方式处理Connector。由于Connector负责接入ServerSocket的Socket链接,所以可以创建多个Connector线程,分别映射多个端口,则需要进行端口映射,微博早期使用过这种技术,称为MPSS(MultiPort Single Server单服务多接口),现在的虚拟化技术是对这种技术的发展。

多Connector解析
多Connector解析

第二种方案,多线程处理Processor。不难发现,在整个解析过程中消耗时间比较多的是Processor解析过程。

Processor解析分为:
1.RequestHead处理,包括RequestMethod、URL和HTTP协议版本号;
2.转发对应的解析器(静态资源/Servlet);
3.RequestParams解析;
4.静态资源加载/动态解析,RequestBody解析;
5.Response封装

Processor解析过程中,虽然我们已经采用了LazyLoad的思想,将解析过程拆分三步解析(上述1、3、 4),但IO相较Socket接入及其他解析流程,占用了很大比重的时间消耗。因此通过多线程优化Processor可以节约大量解析时间。

多Processor解析
多Processor解析

方案 优势 劣势
多Connector 实现简单 通用性 多端口性能更高 鲁棒性 资源利用率低 额外配置端口映射
多Processor 资源利用率高 灵活配置 健壮性 实现成本高 单端口瓶颈 性能优化调试成本

连接器模型设计

Tomcat主要采用了第二种方案进行优化。使用第二种方案,首先要对Processor实例进行管理,要完成的工作包括:(1)在设计方面,Processor是无状态的,因此可以多启动多个Processor线程进行解析,Processor解析结束后不释放实例,实现复用。针对多个Processor进行管理,很容易想到通过池化模型来解决多个实例管理问题。(2)池化中的Processor分为两种状态,运行时/等待,等待状态可以接入请求,运行时不可以接入请求,运行时完成后可以准确返回结果

Tomcat配置文件中 < Connector >标记,Tomcat6以后已经此采用maxThreads,minSpareThreads进行池化模型管理,针对Processor模块会有统一调度优化。在老版本的Tomcat 4 时,使用minProcessors,maxProcessors进行Processor池化模型控制调度。

Connector->minSpareThreads
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.

Connector->maxThreads
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.

From Tomcat 7 Config

连接器池化模型

Tomcat默认连接器中使用 java.util.Stack进行池化模型管理,主要有实例化,回收,获取。

Conncter.java

    protected int minProcessors = 5;//最小Processors
    protected int maxProcessors = 20;//最大Processors

    protected int curProcessors ;//当前Processors

    protected Stack<HttpProcessor> processors;

    /**
    * 初识化
    * 启动Connector线程
    * 初始化 Processor线程池
    */
    public void start() {
        Thread thread = new Thread(this);
        thread.start();
        //实例化
        while(curProcessors < minProcessors) {
            if((maxProcessors > 0) && (curProcessors >= maxProcessors))
                break;
            HttpProcessor processor = new HttpProcessor(this);
            recycle(processor);
            curProcessors ++;
        }
    }

    /**
     * processor回收
     * processor实例压栈
     */
    void recycle(HttpProcessor processor) {
        processors.push(processor);
    }

    /**
     * 创建Processor
     * 若对象池中有Processor,则获取
     * 若当先实例数小于最大实例数,则创建新实例
     * 返回空
     * @return
     */
    private HttpProcessor createProcessor() {
        synchronized (processors) {
            if (processors.size() > 0) {
                return ((HttpProcessor) processors.pop());
            }

            if ((minProcessors > 0) && (curProcessors < maxProcessors)) {
                return newProcessor();
            } else {
                if (maxProcessors < 0) {
                    return newProcessor();
                } else {
                    return (null);
                }
            }
        }
    }


    /**
     * 实例化Processor
     * @return HttpProcessor
     */
    private HttpProcessor newProcessor() {
        HttpProcessor processor = new HttpProcessor(this, curProcessors++);

        //线程启动
        Thread t = new Thread(processor);
        t.start();
        processors.push(processor);
        return processor;
    }

连接器任务委派模型

实现Processor异步解析方面,Processor继承了Runable接口。具体实现如下:


    //thread synchronization obeject
    private Object threadSync = new Object();

   /**
     * 多线程执行
     * 若avaliable 有新请求接入
     * 若无avaliable 无新请求接入
     */
    @Override
    public void run() {

        while(!stopped) {

            Socket socket = await();
            if(socket == null)
                continue;;

            process(socket);

            httpConnector.recycle(this);
        }

        //唤醒
        synchronized (threadSync) {
            threadSync.notifyAll();
        }
    }

Connector-Processor采用了任务委派模型实现解析控制。
任务委派模型中,任务处于两种状态运行时/等待。

    /**
     * 工作委派
     * @param socket
     */
    public synchronized void assign(Socket socket) {
        /**
         * 判断是否有新socket接入
         * 如果有新socket,进入work等待
         */
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {

            }
        }
        this.scoket = socket;
        available = true;
        notifyAll();
    }

    /**
     * work等待
     * @return
     */
    private synchronized Socket await() {
        /**
         * 若无新socket接入
         * 则等待
         */
        while(!available) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Socket socket = this.scoket;
        available = false;
        notifyAll();
        return socket;
    }

这里我们还可以通过FixedThreadPool实现线程池管理,并进行进一步优化。
但从根本上来说任务委派模型是伪异步交互模型

虽然可以通过池化模型 实现 Connector和Processor分离执行,并且通过backlog控制请求等待数,但伪异步交互存在致命的缺陷。从实现原理来说,通过阻塞Socket实现的读和写操作都是同步阻塞的。直接导致的结果:
1. 服务端处理缓慢,写入写出单通道阻塞;
2. 采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60S;
3. 假如所有的可用线程都被故障服务器阻塞,那后续所有的IO消息都将在队列中排队;
4. 由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;
5. 由于前端只有一个Connector线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;
6. 由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

参考目录:
《Netty 权威指南》—— 伪异步IO编程
Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)

原文地址:https://www.cnblogs.com/cunchen/p/9464117.html