Tomcat处理HTTP请求高并发原理剖析

Tomcat处理HTTP请求过程分析
一、Tomcat是什么?
Tomcat是一个web应用服务器,是一个Servlet/Jsp容器,主要负责将客户端请求传递给对应的Servlet,并且将Servlet的响应数据返回给客户端。
Tomcat是基于组件的服务器。
二、Tomcat体系结构
Tomcat是一个基于组件的服务器,它的构成组件都是可配置的。其各个组件都在Tomcat安装目录下的…/conf/server.xml文件中配置。

<?xml version="1.0" encoding="UTF-8"?>
 
<!--顶层类元素,可以包含多个Service-->
 
<Server port="8005" shutdown="SHUTDOWN">  
 
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
 
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
 
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
 
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
 
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
 
  <GlobalNamingResources>
 
    <Resource name="UserDatabase" auth="Container"
 
              type="org.apache.catalina.UserDatabase"
 
              description="User database that can be updated and saved"
 
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
 
              pathname="conf/tomcat-users.xml" />
 
  </GlobalNamingResources>
 
  <!--顶层类元素,可包含一个Engine(container),多个connector-->
 
  <Service name="Catalina">
 
  <!--连接器类元素,代表通信接口-->
 
    <Connector port="8080" protocol="HTTP/1.1"
 
               connectionTimeout="20000"
 
               redirectPort="8443" />
 
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
 
    <!--容器类元素,为特定的service组件处理客户请求-->
 
    <Engine name="Catalina" defaultHost="localhost">
 
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
 
               resourceName="UserDatabase"/>
 
        </Realm>
 
        <!--容器类元素,为特定的虚拟主机组件处理客户请求-->
 
      <Host name="localhost"  appBase="webapps"
 
            unpackWARs="true" autoDeploy="true">
 
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
 
               prefix="localhost_access_log" suffix=".txt"
 
               pattern="%h %l %u %t "%r" %s %b" />
 
      </Host>
 
    </Engine>
 
  </Service>
 
</Server>

在这里插入图片描述
由上图可以看出Tomcat的心脏是两个核心组件:Connector(连接器)和Container(容器)。其中一个Container可以选择多个Connector。
扩展:Tomcat默认提供两个Connector连接器,一个默认监听8080端口,一个默认监听8009端口,这两种连接器有什么区别呢?redirectPort有什么作用?
(1)8080端口监听的是通过HTTP/1.1协议访问的连接,而8009端口主要负责和其他HTTP服务器(如Apache、IIS)建立连接,使用AJP/1.3协议,当Tomcat和其他服务器集成时就会使用到这个连接器。如下图。

在这里插入图片描述

Web1和Web2都是访问服务器的index.jsp页面。Web1直接访问Tomcat服务器,访问地址是http://localhost:8080/index.jsp。Web2访问HTTP服务器,HTTP服务器再通过访问Tomcat的8009端口找到index.jsp。假设HTTP服务器的端口为80端口,则访问地址为http://localhost:80/index.jsp 或者 http://localhost/index.jsp。
Apache、IIS服务器一般只支持静态页面,如HTML,不支持JSP动态页面。Tomcat对HTML的解析速度不如Apache、IIS服务器。因此一般将两者整合使用。
(2)redirectPort字面意思是重定向端口。当用户用http请求某个资源,而该资源本身又被设置了必须要https方式访问,此时Tomcat会自动重定向到这个redirectPort设置的https端口。
三、组件
1、Connector组件
在这里插入图片描述

Connector 最重要的功能就是接收连接请求然后分配线程让 Container来处理这个请求,所以Connector必然是多线程的,多线程的处理是 Connector 设计的核心。Connector监听指定端口上请求,当请求到来时创建一个request和response对象交换数据,然后新建一个线程来处理请求并把request和response传递给Engine组件,最后从Engine获取一个响应并返回给客户端。
Connector组件常用属性说明:
(1) address:指定连接器监听的地址,默认为所有地址,即0.0.0.0,可以自己指定地。(2) maxThreads:支持的最大并发连接数,默认为200;
(3) port:监听的端口;
(4) protocol:连接器使用的协议,默认为HTTP/1.1,定义AJP协议时通常为AJP/1.3;
(5) redirectPort:如果某连接器支持的协议是HTTP,当接收客户端发来的HTTPS请求时,则转发至此属性定义的端口;
(6) connectionTimeout:等待客户端发送请求的超时时间,单位为毫秒,默认为60000,即1分钟;
(7) enableLookups:是否通过request.getRemoteHost()进行DNS查询以获取客户端的主机名;默认为true; 进行反解的,可以设置为false。
(8) acceptCount:设置等待队列的最大长度;通常在tomcat所有处理线程均处于繁忙状态时,新发来的请求将被放置于等待队列中;

2.container组件

在这里插入图片描述

Container是容器的父接口,该容器的设计用的是典型的责任链的设计模式,它由四个子容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。其中Engine是最顶层,每个service 最多只能有一个Engine, Engine 里面可以有多个Host ,每个Host 下可以有多个Context ,每个Context 下可以有多个Wrapper。通常一个Servlet class对应一个Wrapper,如果有多个Servlet则定义多个Wrapper,如果有多个Wrapper就要定义一个更高的Container,如Context。 Context定义在父容器 Host 中,其中Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。
2.1、Engine
Engine是Servlet处理器的一个实例,即servlet引擎, 一个Service 最多只能有一个Engine。默认为定义在server.xml中的Catalina。Engine需要defaultHost属性来为其定义一个接收所有请求的虚拟主机host组件。
2.2、Host
Host是Engine的子容器。一个 Host 在 Engine 中代表一个站点,也叫虚拟主机,这个虚拟主机的作用就是运行多个应用、接收并处理请求、保存一个主机应该有的信息。
常用属性说明:
(1)appBase:此Host的webapps目录,项目存放路径,可以使用绝对路径;
(2)autoDeploy:在Tomcat处于运行状态时放置于appBase目录中的应用程序文件是否自动进行deploy;默认为true;
(3)unpackWars:在启用此webapps时是否对WAR格式的归档文件先进行展开;默认为true;
2.3、Context
Context :代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF 目录以及下面的web.xml 文件。它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,获取子容器都是通过 request 来分配的。
常用属性定义:
(1) docBase:相应的Web应用程序的存放位置;也可以使用相对路径,起始路径为此Context所属Host中appBase定义的路径;切记,docBase的路径名不能与相应的Host中appBase中定义的路径名有包含关系,比如,如果appBase为deploy,而docBase绝不能为deploy-bbs类的名字;
(2)path:相对于Web服务器根路径而言的URI;如果为空“”,则表示为此webapp的根路径;如果context定义在一个单独的xml文件中,此属性不需要定义,有可能是别名;
(3) reloadable:是否允许重新加载此context相关的Web应用程序的类;默认为false;
2.4、Wrapper
Wrapper :每个Wrapper 封装着一个servlet,也代表一个 Servlet,它负责管理一个 Servlet,包括Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。 Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。
2.5、Value
Valve类似于过滤器,它可以工作于Engine和Host/Context之间、Host和Context之间以及Context和Web应用程序的某资源之间。一个容器内可以建立多个Valve,而且Valve定义的次序也决定了它们生效的次序。

四、Tomcat处理一个HTTP请求的过程
在这里插入图片描述

1.用户在浏览器中输入网址localhost:8080/test/index.jsp,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得;
2.Connector把该请求交给它所在的Service的Engine(Container)来处理,并等待Engine的回应;
3.Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host;
4.Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)。名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理);
5.path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL Pattern为*.jsp的Servlet,对应于JspServlet类;
6.构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等;
7.Context把执行完之后的HttpServletResponse对象返回给Host;
8.Host把HttpServletResponse对象返回给Engine;
9.Engine把HttpServletResponse对象返回Connector;
10.Connector把HttpServletResponse对象返回给客户Browser。
11.扩展:下图是struts使用tomcat处理请求的过程
在这里插入图片描述

五、线程池的原理和Tomcat的Connector及线程池配置
Tomcat处理请求流程:
先启动若干数量的线程,并让这些线程都处于睡眠 状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态。可能你也许会 问:为什么要搞得这么麻烦,如果每当客户端有新的请求时,我就创建一个新的线程不就完了?这也许是个不错的方法,因为它能使得你编写代码相对容易一些,但 你却忽略了一个重要的问题??性能!例如:一个省级数据大集中的银行网络中心,高峰期每秒的客户端请求并发数超过100,如果 为每个客户端请求创建一个新线程的话,那耗费的CPU时间和内存将是惊人的,如果采用一个拥有200个线程的线程池,那将会节约大量的的系统资源,使得更 多的CPU时间和内存用来处理实际的商业应用,而不是频繁的线程创建与销毁。
配置executor属性(各项参数值根据自身情况配置)
1.1)打开/conf/server.xml文件,在Connector之前配置一个线程池:

参数详解:
name:共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须唯一。默认值:None;
namePrefix:在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-;
maxThreads:该线程池可以容纳的最大线程数。默认值:200;
maxIdleTime:在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。
minSpareThreads:Tomcat应该始终打开的最小不活跃线程数。默认值:25。
配置Connector

参数详解:
executor:表示使用该参数值对应的线程池;
minProcessors:服务器启动时创建的处理请求的线程数;
maxProcessors:最大可以创建的处理请求的线程数;
acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。
问题:
1.为什么要多线程?
答:因为如果不采用多线程的策略,那么所有的请求放在一起处理,会大大降低CPU的使用效率,系统处理业务的效率也会因此大打折扣。采用多线程,可以提高CPU的使用效率,进而提高系统处理业务的效率。
2.为什么多线程的情景下,系统只需要准备一份程序代码就够了?
答:因为Tomcat从本质上来讲,是一个JVM进程。一个JVM可以派生出多个线程,每个线程都有自己的程序计数器、虚拟机栈、本地方法栈、所有线程共用堆区和方法区(元数据区)。学习ucosii操作系统时,可以知道真正在CPU上执行的单元是任务,并且任务之间可以切换。每个任务都有自己的任务控制块和任务堆栈,并且任务控制块中有一个指针SP指向任务堆栈,而任务堆栈中又有一个指针PC指向任务程序代码。ucosii中任务切换时,只要在代码中替换SP就可以实现任务切换。同理,在JVM中,线程的切换也是类似的道理。存在方法区或元数据区的代码是静态的,线程真正关心的是代码执行进度。形象的比喻一下,线程好比参加赛跑的运动员,存在方法区或元数据区的代码就好比一个用来制作跑到的模块,每个运动员都要依照模板制作一条属于自己的跑道,然后在上面跑起来。而自然的,系统只需要准备一份程序代码就够了。
六、tomcat线程池和jdk线程池的关系
前言
Tomcat/Jetty 是目前比较流行的 Web 容器,两者接受请求之后都会转交给线程池处理,这样可以有效提高处理的能力与并发度。JDK 提高完整线程池实现,但是 Tomcat/Jetty 都没有直接使用。Jetty 采用自研方案,内部实现 QueuedThreadPool 线程池组件,而 Tomcat 采用扩展方案,踩在 JDK 线程池的肩膀上,扩展 JDK 原生线程池。
JDK 原生线程池可以说功能比较完善,使用也比较简单,那为何 Tomcat/Jetty 却不选择这个方案,反而自己去动手实现那?
JDK 线程池
通常我们可以将执行的任务分为两类:
cpu 密集型任务
io 密集型任务
cpu 密集型任务,需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,过多的线程将会频繁引起上文切换,降低任务处理处理速度。
而 io 密集型任务,由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。
JDK 原生线程池工作流程如下:
在这里插入图片描述
上图假设使用 LinkedBlockingQueue 。
灵魂拷问:上述流程是否记错过?在很长一段时间内,我都认为线程数量到达最大线程数,才放入队列中。 ̄□ ̄||
上图中可以发现只要线程池线程数量大于核心线程数,就会先将任务加入到任务队列中,只有任务队列加入失败,才会再新建线程。也就是说原生线程池队列未满之前,最多只有核心线程数量线程。
这种策略显然比较适合处理 cpu 密集型任务,但是对于 io 密集型任务,如数据库查询,rpc 请求调用等,就不是很友好了。
由于 Tomcat/Jetty 需要处理大量客户端请求任务,如果采用原生线程池,一旦接受请求数量大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样做显然降低这些请求总体处理速度,所以两者都没采用 JDK 原生线程池。
解决上面的办法可以像 Jetty 自己实现线程池组件,这样就可以更加适配内部逻辑,不过开发难度比较大,另一种就像 Tomcat 一样,扩展原生 JDK 线程池,实现比较简单。
下面主要以 Tomcat 扩展线程池,讲讲其实现原理。
扩展线程池
首先我们从 JDK 线程池源码出发,查看如何这个基础上扩展。
在这里插入图片描述
可以看到线程池流程主要分为三步,第二步根据 queue#offer 方法返回结果,判断是否需要新建线程。
JDK 原生队列类型 LinkedBlockingQueue , SynchronousQueue ,两者实现逻辑不尽相同。
LinkedBlockingQueue
offer 方法内部将会根据队列是否已满作为判断条件。若队列已满,返回 false ,若队列未满,则将任务加入队列中,且返回 true 。
SynchronousQueue
这个队列比较特殊,内部不会储存任何数据。若有线程将任务放入其中将会被阻塞,直到其他线程将任务取出。反之,若无其他线程将任务放入其中,该队列取任务的方法也将会被阻塞,直到其他线程将任务放入。
对于 offer 方法来说,若有其他线程正在被取方法阻塞,该方法将会返回 true 。反之,offer 方法将会返回 false。
所以若想实现适合 io 密集型任务线程池,即优先新建线程处理任务,关键在于 queue#offer 方法。可以重写该方法内部逻辑,只要当前线程池数量小于最大线程数,该方法返回 false ,线程池新建线程处理。
当然上述实现逻辑比较糙,下面我们就从 Tomcat 源码查看其实现逻辑。
Tomcat 扩展线程池
Tomcat 扩展线程池直接继承 JDK 线程池 java.util.concurrent.ThreadPoolExecutor ,重写部分方法的逻辑。另外还实现了 TaskQueue ,直接继承 LinkedBlockingQueue ,重写 offer 方法。
首先查看 Tomcat 线程池的使用方法。
在这里插入图片描述

可以看到 Tomcat 线程池使用方法与普通的线程池差不太多。
接着我们查看一下 Tomcat 线程池核心方法 execute 的逻辑。
在这里插入图片描述

execute 方法逻辑比较简单,任务核心还是交给 Java 原生线程池处理。这里主要增加一个重试策略,如果原生线程池执行拒绝策略的情况,抛出 RejectedExecutionException 异常。这里将会捕获,然后重新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。
这里需要注意 submittedCount 变量。这是 Tomcat 线程池内部一个重要的参数,它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。 TaskQueue#offer 将会使用该参数实现相应的逻辑。
接着我们主要查看 TaskQueue#offer 方法逻辑。
在这里插入图片描述

核心逻辑在于第三步,这里如果 submittedCount 小于当前线程池线程数量,将会返回 false。上面我们讲到 offer 方法返回 false ,线程池将会直接创建新线程。
Dubbo 2.6.X 版本增加 EagerThreadPool ,其实现原理与 Tomcat 线程池差不多,感兴趣的小伙伴可以自行翻阅。
折衷方法
上述扩展方法虽然看起不是很难,但是自己实现代价可能就比较大。若不想扩展线程池运行 io 密集型任务,可以采用下面这种折衷方法。
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100));
不过使用这种方式将会使 keepAliveTime 失效,线程一旦被创建,将会一直存在,比较浪费系统资源。
总结
JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务。对于 IO 密集型任务可以间接通过设置线程池参数方式做到。

https://www.cnblogs.com/GooPolaris/p/8111837.html
https://www.cnblogs.com/GooPolaris/p/8115784.html
https://blog.csdn.net/gchd19921992/article/details/79071288
https://blog.csdn.net/gchd19921992/article/details/79076926
https://blog.csdn.net/LeiXiaoTao_Java/article/details/85003421
https://www.cnblogs.com/pingxin/p/p00063.html
https://blog.csdn.net/ljxljxljx747/article/details/80142293
https://www.cnblogs.com/weiyiming007/p/12600027.html


版权声明:本文为u011159820原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。