Tomcat性能并发调优
优化tomcat参数
这里以tomcat7的参数配置为例,需要修改conf/server.xml文件,主要是优化连接配置,关闭客户端dns查询。
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="500"
minSpareThreads="20"
acceptCount="100"
disableUploadTimeout="true"
enableLookups="false"
URIEncoding="UTF-8" />port:代表Tomcat监听端口,也就是网站的访问端口,默认为8080,可以根据需要改成其他。
protocol:协议类型,可选类型有四种,分别为BIO(阻塞型IO),NIO,NIO2和APR。
BIO:BIO(Blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。BIO配置采用默认即可。
NIO:NIO(New I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio模式来运行也比较简单,我们只需要protocol类型修改为:
//NIO protocol="org.apache.coyote.http11.Http11NioProtocol"
//NIO2 protocol="org.apache.coyote.http11.Http11Nio2Protocol"
APR:APR(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。你可以简单地理解为:Tomcat将以JNI的形式调用 Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高 Tomcat对静态文件的处理性能。
与配置NIO运行模式一样,也需要将对应的Connector节点的protocol属性改为: protocol="org.apache.coyote.http11.Http11AprProtocol"
maxThreads:由该连接器创建的处理请求线程的最大数目,也就是可以处理的同时请求的最大数目。如果未配置默认值为200。如果一个执行器与此连接器关联,则忽略此属性,因为该属性将被忽略,所以该连接器将使用执行器而不是一个内部线程池来执行任务。
maxThreads是一个重要的配置属性,maxThreads配置的合理直接影响了Tomcat的相关性能,所以这里我们重点讨论下。
maxThreads并不是配置的越大越好,事实上你即使配置成999999也是没有用的,因为这个最大值是受操作系统及相关硬件所制约的,并且最大值并不一定是最优值,所以我们追寻的应该是最优值而不是最大值。
QPS(Query Per Second):每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。我们常常使用 QPS值来衡量一个服务器的性能。
QPS = 并发数 / 平均响应时间 或者 并发数 = QPS * 平均响应时间
4核8G经验数值是800,1c2g为200
一个系统吞吐量通常由QPS、并发数两个因素决定,每套系统的这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。所谓吞吐量这里可以理解为每秒能处理请求的次数。
所以选择一个合理的 maxThreads值,其实并不是那么容易的事。因为过多的线程只会造成,更多的内存开销,更多的CPU开销,但是对提升QPS确毫无帮助;找到最佳线程数后通过简单的设置,可以让web系统更加稳定,得到最高,最稳定的QPS输出。
我们可以通过以下几种方式来获取 maxThreads的最佳值:
(1)通过线上系统不断使用和用户的不断增长来进行性能测试,观察QPS,响应时间,这种方式会在爆发式增长时系统崩溃,如双12等。
(2)根据公式计算,服务器端最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量,这种方式有时会被误导,因为某些系统处理环节可能会耗时比较长,从而影响公式的结果。
(3)单、多用户压力测试,查看CPU的消耗,然后直接乘以百分比,再进行压测,一般这个值的附近应该就是最佳线程数量,这种方式理想场景比较适用,实际情况会比这个复杂的多。
(4)根据系统的自身情况调整,如硬件限制,系统限制,程序处理能力限制等。
(5)定期修改为不同的 maxThreads值,看服务器响应结果及用户反应。
QPS和线程数的关系
(1)在最佳线程数量之前,QPS和线程是互相递增的关系,线程数量到了最佳线程之后,QPS持平,不在上升,甚至略有下降,同时相应时间持续上升。
(2)同一个系统而言,支持的线程数越多(最佳线程数越多而不是配置的线程数越多),QPS越高。
QPS和响应时间的关系
(1)对于一般的web系统,响应时间一般有CPU执行时间+IO等待时间组成。
minSpareThreads:线程的最小运行数目,这些始终保持运行。如果未指定,默认值为10。
acceptCount:当所有可能的请求处理线程都在使用时传入连接请求的最大队列长度。如果未指定,默认值为100。一般是设置的跟 maxThreads一样或一半,此值设置的过大会导致排队的请求超时而未被处理。所以这个值应该是主要根据应用的访问峰值与平均值来权衡配置。
maxConnections:在任何给定的时间内,服务器将接受和处理的最大连接数。当这个数字已经达到时,服务器将接受但不处理,等待进一步连接。NIO与NIO2的默认值为10000,APR默认值为8192。
connectionTimeout:当请求已经被接受,但未被处理,也就是等待中的超时时间。单位为毫秒,默认值为60000。通常情况下设置为30000。
maxHttpHeaderSize:请求和响应的HTTP头的最大大小,以字节为单位指定。如果没有指定,这个属性被设置为8192(8 KB)。
tcpNoDelay:如果为true,服务器socket会设置TCP_NO_DELAY选项,在大多数情况下可以提高性能。缺省情况下设为true。
compression:是否启用gzip压缩,默认为关闭状态。这个参数的可接受值为“off”(不使用压缩),“on”(压缩文本数据),“force”(在所有的情况下强制压缩)。
compressionMinSize:如果compression="on",则启用此项。被压缩前数据的最小值,也就是超过这个值后才被压缩。如果没有指定,这个属性默认为“2048”(2K),单位为byte。
disableUploadTimeout:这个标志允许servlet Container在一个servlet执行的时候,使用一个不同的,更长的连接超时。最终的结果是给servlet更长的时间以便完成其执行,或者在数据上载的时候更长的超时时间。如果没有指定,设为false。
enableLookups:关闭DNS反向查询。
URIEncoding:URL编码字符集。
maxThreads,acceptCount,maxConnections参数说明
maxThreads参数定义了用于处理用户请求的最大线程数。默认值为200。
acceptCount参数定义了当所有可用的请求处理线程都在使用时,可以接收的连接请求的最大队列长度。当队列已满时,任何新的连接请求都将被拒绝。默认值为100。
maxConnections参数定义了Tomcat在同一时刻能够接受的最大连接数。这个参数是用来控制并发连接数的上限,以防止系统资源耗尽。对于Java的阻塞式BIO,默认值是maxthreads的值;如果在BIO模式使用定制的Executor执行器,默认值将是执行器中maxthreads的值。对于Java 新的NIO模式,maxConnections 默认值是10000。
对于windows上APR/native IO模式,maxConnections默认值为8192,这是出于性能原因,如果配置的值不是1024的倍数,maxConnections 的实际值将减少到1024的最大倍数。
如果设置为-1,则禁用maxconnections功能,表示不限制tomcat容器的连接数。
maxConnections和accept-count的关系为:当连接数达到最大值maxConnections后,系统会继续接收连接,但不会超过acceptCount的值。
情况1:接受一个请求,此时 tomcat 起动的线程数没有到达 max-threads,tomcat 会起动一个线程来处理此请求。4c8g机器经验数值是800,1c2g为200
情况2:接受一个请求,此时 tomcat 起动的线程数已经到达 max-threads,tomcat 会把此请求放入等待队列accept-count,等待空闲线程。
情况3:接受一个请求,此时 tomcat 起动的线程数已经到达 max-threads,等待队列中的请求个数也达到了 accept-count,此时 tomcat 会直接拒绝此次请求,返回 connection refused。
假如设置,发起请求200次:
acceptCount = 10;
maxThreads = 50;
maxConnections = 100;
minSpareThreads = 10;
//伪代码
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
private volatile ThreadPoolExecutor parent = null;
public void setParent(ThreadPoolExecutor tp) {
parent = tp;
}
@Override
public boolean offer(Runnable o) {
// 如果工作线程大于等于了最大线程(maxThreads),那么放入队列。不用担心TaskQueue作为无界队列而OOM,因为有maxConnections限制。
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
// 由于创建线程池时调用过prestartAllCoreThreads();,所以会直接初始化出minSpareThreads(默认10个)个工作线程。
// 前minSpareThreads个请求都会走这个if,走这个if时,会将任务直接放入队列,然后这10个主工作线程会从队列里取任务执行。
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
// 如果第十个之后的请求到来(假如前十个还都没有执行完),会走这个if,因为反回了false,所以在ThreadPoolExecutor执行excute的
// 「放任务到队列步骤」时会失败。此时ThreadPoolExecutor会认为队列满了,所以会直接创建新的线程执行任务。
// 这里为什么要这么做呢?如果该if不返回false的话,那么永远也用不到maxThreads的线程,因为TaskQueue是个无界队列。
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
// 如果工作线程大于等于了最大线程(maxThreads),那么放入队列。不用担心TaskQueue作为无界队列而OOM,因为有maxConnections限制。
return super.offer(o);
}
}客户端对服务端发出第1-10个请求,双方建立TCP连接,此时accept queue中有10个连接
Acceptor在while(true)里一直调用serverSocket.accept()从accept queue里获取了10个socket
Acceptor将这10个socket交给线程池执行,线程池将这10个任务直接放到了TaskQueue中。(为什么不是慢慢启动10个主线程呢?因为线程池在初始化时调用了prestartAllCoreThreads(参考图3),所以伪代码6中if (workerCountOf(c) < corePoolSize)是false,会执行下一个if,将任务放入TaskQueue,该步骤会执行TaskQueue.offer里的第二个if)
线程池会中的10个主线程会将TaskQueue中的10个任务取出执行
此时,accept queue中数量为0,正在执行任务的工作线程有10个,TaskQueue中数量为0
客户端对服务端发出第11-20个请求,双方建立TCP连接,此时accept queue中有10个连接
Acceptor在while(true)里一直调用serverSocket.accept()从accept queue里获取了10个socket
Acceptor将这10个socket交给线程池执行,线程池启动新的线程来执行这10个任务。(为什么是启动新的而不是放入队列?注意线程池里的伪代码6中if (workerCountOf(c) < corePoolSize)是false,会执行到if (isRunning(c) && workQueue.offer(command)) ,这里会调用到伪代码5中TaskQueue里的offer,该步骤会执行TaskQueue.offer里的第三个if。为什么要这么做呢?因为TaskQueue是个无界队列,如果不这么处理,那么maxThreads就没用了)
此时,accept queue中数量为0,正在执行任务的工作线程有20个,TaskQueue中数量为0
客户端对服务端发出第21-30个请求、客户端对服务端发出第31-40个请求、客户端对服务端发出第41-50个请求
步骤同「第11-20个请求」流程
此时,accept queue中数量为0,正在执行任务的工作线程有50个,TaskQueue中数量为0
客户端对服务端发出第51-60个请求,双方建立TCP连接,此时accept queue中有10个连接
Acceptor在while(true)里一直调用serverSocket.accept()从accept queue里获取了10个socket
Acceptor将这10个socket交给线程池执行,线程池将这10个任务直接放到了TaskQueue中。(为什么又放入队列了呢?因为这些个请求执行到了伪代码6中if (isRunning(c) && workQueue.offer(command)),接着执行了伪代码5中TaskQueue.offer的第一个if或者最后的return)
此时,accept queue中数量为0,正在执行任务的工作线程有50个,TaskQueue中数量为10
客户端对服务端发出第61-70个请求、客户端对服务端发出第71-100个请求
步骤同「第51-60个请求」流程
此时,accept queue中数量为0,正在执行任务的工作线程有50个,TaskQueue中数量为50
客户端对服务端发出第101-110个请求
伪代码3的countUpOrAwaitConnection()方法,Acceptor在while(true)里调用serverSocket.accept()之前会校验总连接数是否大于maxConnections,如果大于,则等待,所以这个请求,就留在了accept queue中
此时,accept queue中数量为10,正在执行任务的工作线程有50个,TaskQueue中数量为50
客户端对服务端发出第111-200个请求
由于accept queue满了,sync queue还能装一些半连接,但是此时并不能建立完整的连接了,就当做这90个连接被拒绝了
server.xml整体架构
首先我们需要知道server.xml中的xml代码块分类,tomcat官网将其主要分为四类:
Top Level Elements:
server块是整个配置文件的根元素,而service块代表与引擎关联的一组连接器(connector)。Connectors :表示外部客户端向特定服务发送请求和接收响应的接口(比如我们之前提到的coyote连接器以及对应的NIO等IO模式都是整个范畴内的概念)。
Containers:容器(
Container)负责处理传入的请求并创建相应的响应。Engine处理对Service的所有请求,Host处理对特定virtual host的所有请求,而Context处理对特定Web应用程序的所有请求。Nested Components:表示可以嵌套在
Container元素内的元素。 注意一些元素可以嵌套在任何Container中,而另一些元素只能嵌套在Context中。
Top Level Elements
Server块
Server块代表的是整个catalina servlet容器。因此,它必须是conf/server.xml配置文件中最外面的单个元素。它的属性代表了整个servlet容器的特征。Tomcat9中默认的配置文件中Server块内嵌的子元素为 Listener、GlobalNamingResources、Service(可以嵌套多个)。具体的每个属性参数我们可以查询官网,下面解释默认的参数配置。
<Server port="8005" shutdown="SHUTDOWN">
<!--
port : Tomcat监听的关闭服务器的端口
shutdown : 关闭服务器的指令字符串
-->
<!-- 以日志形式输出服务器、操作系统、JVM的版本信息 -->
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- 启动和停止APR。如果找不到APR库会输出日志但并不影响tomcat正常启动 -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="off" />
<!--
注意这里的SSLEngine默认是打开的(on)
如果启用了apr作为连接器的协议
但是只配置了http而没有配置https
则会报错
-->
<!-- 用于避免JRE内存泄漏问题 -->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<!-- 用户加载(服务器启动)和销毁(服务器停止)全局命名服务 -->
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<!-- 用于在Context停止时重建Executor池中的线程, 以避免ThreadLocal相关的内
存泄漏 -->
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- GlobalNamingResources中定义了全局命名服务: -->
<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" />
<!--这里定义的文件就是我们前面配置manager和host manager的用户的文件-->
</GlobalNamingResources>
<Service>
...
</Service>
</Server>Listener
listener可以嵌套在Server、 Engine、Host或 Context中。某些侦听器仅旨在嵌套在特定元素中。这些限制在下面的文档中注明。
Listener的所有实现都 支持以下属性:className:要使用的实现的 Java 类名。这个类必须实现org.apache.catalina.LifecycleListener 接口。
tomcat默认内置了一些Listener如下:
VersionLoggerListener(以志形式输出服务器 、操作系统、JVM的版本信息)
版本日志记录生命周期监听器,会在Tomcat 启动时记录 Tomcat、Java 和操作系统信息。
此侦听器只能嵌套在Server 元素中,并且应该是第一个定义的侦听器。
AprLifecycleListener (加载(服务器启动) 和 销毁 (服务器停) APR。 如果找不到APR库, 则会输出志, 并不影响 Tomcat启动)
APR 生命周期侦听器检查 APR/本机库是否存在,如果存在则加载该库。有关详细信息,请参阅APR/本地指南。
此侦听器只能嵌套在Server 元素中。
JreMemoryLeakPreventionListener (避免JRE内存泄漏问题)
JRE 内存泄漏预防侦听器为 Java 运行时环境使用上下文类加载器加载单例的已知位置提供了解决方法,因为如果 Web 应用程序类加载器恰好是当时的上下文类加载器,这将导致内存泄漏. 解决方法是在此侦听器启动时初始化这些单例,因为当时 Tomcat 的公共类加载器是上下文类加载器。它还为可能导致 JAR 文件锁定的已知问题提供了解决方法。
此侦听器只能嵌套在Server 元素中。
JreMemoryLeakPreventionListener 示例
//以下是如何配置 classesToInitialize此侦听器的属性的示例。
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"
classesToInitialize="oracle.jdbc.driver.OracleTimeoutThreadPerVM" />
//那么OracleTimeoutThreadPerVM类将在侦听器启动期间而不是在请求处理期间加载和初始化。GlobalResourcesLifecycleListener (加载(服务器启动) 和 销毁(服务器停) 全局命名服务)
Global Resources Lifecycle Listener将server.xml 中定义的 Global JNDI 资源初始化为Global Resources元素的一部分。如果没有此侦听器,将无法使用任何全局资源。
此侦听器只能嵌套在Server 元素中。
ThreadLocalLeakPreventionListener (在Context停时重建 Executor 池中的线程, 以避免ThreadLocal 相关的内存泄漏)
在停止Context时触发Executor池中 的线程更新,以避免与线程本地相关的内存泄漏。活动线程在执行完任务后回到池中时会一一更新。更新仅发生在其 属性设置为 的上下文中。renewThreadsWhenStoppingContexttrue
此侦听器只能嵌套在Server 元素中。
org.apache.catalina.core.JniLifecycleListener - JNI 库加载监听器
JNI 库加载侦听器通过使用共享类加载器(通常是通用类加载器,但在某些配置中可能会有所不同)加载本机库,使多个 Web 应用程序可以使用本机库
监听器支持两个互斥的属性,所以必须使用其中一个,但不能同时使用:
org.apache.catalina.security.SecurityListener - 安全生命周期监听器
Security Lifecycle Listener在 Tomcat 启动时执行多项安全检查,并在检查失败时阻止 Tomcat 启动。默认情况下不启用侦听器。要启用它,请取消注释 $CATALINA_BASE/conf/server.xml 中的侦听器。对于 8.5.30 之前的 Tomcat 版本,如果操作系统支持 umask,那么 $CATALINA_HOME/bin/catalina.sh 中获取 umask 的行也需要取消注释。对于 Tomcat 8.5.30 及更高版本,umask 会自动传递到 Tomcat。
org.apache.catalina.startup.UserConfig - 用户配置
UserConfig提供用户 Web 应用程序的功能。用户 Web 应用程序将以波浪字符 ("~") 和用户名开头的请求 URI 映射到服务器上该用户主目录中的目录(通常命名为 public_html)。
有关详细信息,请参阅Host元素上的用户 Web 应用程序 特殊功能。
org.apache.catalina.util.SystemPropertyReplacerListener - 系统属性替换
此侦听器使用在摘要器上配置的属性源执行系统属性替换。当${parameter} 在系统属性的值中找到表示的参数时,将调用属性源以尝试替换它。
Service块
Service元素用于创建 Service 实例,默认使用 org.apache.catalina.core.StandardService。 默认情况下,Tomcat9中默认仅指定了Service的名称为Catalina。
<Service name="Catalina">
...
</Service>Service 可以内嵌的元素为 : Listener、Executor、Connector、Engine ,详细的参数可以点击这里查看官网
Listener 用于为Service 添加生命周期监听器
Executor 用于配置Service 共享线程池
Connector 用于配置 Service 包含的链接器
Engine 用于配置Service中连接器(connector)对应的Servlet 容器引擎
Executor
executor表示可组件之间Tomcat中共享的线程池。默认情况下,Service并未添加共享线程池配置。executor实现了tomcat中的org.apache.catalina.Executor接口。 如果不配置共享线程池,那么Catalina 各组件在用到线程池时会独立创建。由于executor是Service元素的嵌套元素。为了使它能够被Connector使用,Executor元素必须出现在server.xml中的Connector元素之前。下面展示的是一个简单的executor的配置,具体的配置参数可以点这里查看官网:
<Executor name="tomcatThreadPool"
namePrefix="catalina‐exec‐"
maxThreads="200"
minSpareThreads="100"
maxIdleTime="60000"
maxQueueSize="Integer.MAX_VALUE"
prestartminSpareThreads="false"
threadPriority="5"
className="org.apache.catalina.core.StandardThreadExecutor"/>Connector
Connector 用于创建链接器实例。默认情况下,server.xml 配置了两个链接器,一个支 持HTTP协议,一个支持AJP协议。因此大多数情况下,我们并不需要新增链接器配置, 只是根据需要对已有链接器进行优化。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />port为监听的端口,如果设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector 使用protocol为Connector的协议,这里默认的是HTTP和AJP两种协议,后面可以指定对应协议的不同版本,默认情况下会检测本机是否配置了APR库,如果有并且useAprConnector设置为true则会默认使用APR模式的IO协议,如果无则会使用NIO模式connectionTimeOut:Connector 接收链接后的等待超时时间,单位为毫秒。 -1表示永不超时redirectPort:当前Connector 不支持SSL请求, 接收到了一个请求, 并且也符合 security-constraint 约束, 需要SSL传输,Catalina自动将请求重定向到指定的端口executor: 指定前面提到的共享线程池的名称,也可以通过maxThreads、minSpareThreads 等属性对该connector进行单独配置对应的内部线程池URIEncoding: 用于指定编码URI的字符编码, Tomcat8.x和Tomcat9.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-8859-1
engine
Engine 作为Servlet 引擎的顶级元素,内部可以嵌入: Cluster、Listener、Realm、 Valve和Host。
<Engine name="Catalina" defaultHost="localhost">
……
</Engine>name:用于指定Engine 的名称, 默认为CatalinadefaultHost:默认使用的虚拟主机名称,当客户端请求访问的host无效时,会跳转到默认的host来处理请求
Host
Host 元素用于配置一个虚拟主机,它支持以下嵌入元素:Alias、Cluster、Listener、 Valve、Realm、Context
如果在Engine下配置Realm,那么此配置将在当前Engine下的所有Host中共享。 同样,如果在Host中配置Realm ,则在当前Host下的所有Context 中共享
Context中的Realm优先级 > Host的Realm优先级 > Engine中的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" />
<Alias>www.example.com</Alias>
<Alias>www.example2.com</Alias>
</Host>上面这一段Host的配置文件中还额外添加了Valve配置来实现自定义的日志记录。其中一些参数的详细信息和配置方式可以查看官网的说明 。
The shorthand patternpattern="common"corresponds to the Common Log Format defined by '%h %l %u %t "%r" %s %b'.
name: 当前Host通用的网络名称,也就是常用的域名,如果有多个域名对应同一个Host的应用,我们可以设置一个或多个Alias来实现访问
appBase:当前Host应用对应的目录,当前Host上部署的Web应用均在该目录下(相对路径和绝对路径均可),默认为webapps
unpackWARs:设置为true,Host在启动时会将appBase目录下war包解压为目 录。设置为false,Host将直接从war文件启动
autoDeploy: 控制tomcat是否在运行时定期检测并自动部署新增或变更的web应用
Context
Context的完整配置官网文档,Context 用于配置一个Web应用,默认的配置如下。它支持的内嵌元素为:CookieProcessor,Loader,Manager,Realm,Resources,WatchedResource,JarScanner,Valve。
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context docBase="myAppDeploy" path="/myApp">
....
</Context>
</Host>docBase:Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于该Context所属的Host中的
appBase的相对路径。path:Web应用的Context的访问路径。
假设tomcat的安装目录为/home/tomcat9,Host为默认的localhost, 则该web应用访问的根路径为: http://localhost:8080/myApp,对应的部署文件所存放的路径为:/home/tomcat9/webapps/myAppDeploy。
BIO、NIO、APR
Tomcat支持的I/O模型如下表(自8.5/9.0 版本起,Tomcat移除了对BIO的支持),在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2 还是 APR, 在性能方面均优于以往的BIO。

过配置 protocol的类型可以使用不同的 Connector处理请求。
//BIO protocol="HTTP/1.1"
//NIO protocol="org.apache.coyote.http11.Http11NioProtocol"
//NIO2 protocol="org.apache.coyote.http11.Http11Nio2Protocol"
//APR protocol="org.apache.coyote.http11.Http11AprProtocol"

并不是说 BIO的性能就一定不如 NIO,这几种类型 Connector之间并没有明显的性能区别,它们之间实现流程和原理不同,所以它们的选择是需要根据应用的类型来决定的。
BIO更适合处理简单流程,如程序处理较快可以立即返回结果。简单项目及应用可以采用BIO。
NIO更适合后台需要耗时完成请求的操作,如程序接到了请求后需要比较耗时的处理这已请求,所以无法立即返回结果,这样如果采用BIO就会占用一个连接,而使用NIO后就可以将此连接转让给其他请求,直至程序处理完成返回为止。
APR可以大大提升Tomcat对静态文件的处理性能,同时如果你使用了HTTPS方式传输的话,也可以提升SSL的处理性能。
NIO(New I/O APIs、同步非阻塞)
Tomcat中的NIO模型是使用的JAVA的NIO类库,其内部的IO实现是同步的(也就是在用户态和内核态之间的数据交换上是同步机制),采用基于selector实现的异步事件驱动机制(这里的异步指的是selector这个实现模型是使用的异步机制)。而对于Java来说,非阻塞I/O的实现完全是基于操作系统内核的非阻塞I/O,它将操作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心操作系统。JDK会帮我们选择非阻塞I/O的实现方式。
这里需要提一下同步异步和阻塞非阻塞的概念:
同步和异步关注的是消息通信机制,同步异步指的是应用程序发起的调用请求和获得的返回值是否一起返回,如果一起返回就是同步,否则就是异步,异步可以通过回调函数等方式实现。
阻塞和非阻塞关注的是程序在等待调用结果时的状态,应用程序发起调用请求之后不能干别的事情直到请求处理完成了就是阻塞,否则就是非阻塞。
所以我个人认为,对于阻塞I/O谈同步异步是没有太大意义的,因为此时进程已经阻塞,想要去干别的事情必须得等请求处理完,而请求处理完必然会得到返回值。
上面我们提到得内核基于回调得事件检测方式二就是典型的异步非阻塞I/O模型。
NIO2(New I/O APIs 2、异步非阻塞、AIO)
NIO2和前者相比的最大不同就在于引入了异步通道来实现异步IO操作,因此也叫AIO(Asynchronous I/O)。NIO.2 的异步通道 APIs 提供方便的、平台独立的执行异步操作的标准方法。这使得应用程序开发人员能够以更清晰的方式来编写程序,而不必定义自己的 Java 线程,此外,还可通过使用底层 OS 所支持的异步功能来提高性能。如同其他 Java API 一样,API 可利用的 OS 自有异步功能的数量取决于其对该平台的支持程度。
异步通道提供支持连接、读取、以及写入之类非锁定操作的连接,并提供对已启动操作的控制机制。Java 7 中用于 Java Platform(NIO.2)的 More New I/O APIs,通过在 java.nio.channels 包中增加四个异步通道类,从而增强了 Java 1.4 中的 New I/O APIs(NIO),这些类在风格上与 NIO 通道 API 很相似。他们共享相同的方法与参数结构体,并且大多数对于 NIO 通道类可用的参数,对于新的异步版本仍然可用。主要区别在于新通道可使一些操作异步执行。
异步通道 API 提供两种对已启动异步操作的监测与控制机制。第一种是通过返回一个 java.util.concurrent.Future 对象来实现,它将会建模一个挂起操作,并可用于查询其状态以及获取结果。第二种是通过传递给操作一个新类的对象,java.nio.channels.CompletionHandler,来完成,它会定义在操作完毕后所执行的处理程序方法。每个异步通道类为每个操作定义 API 副本,这样可采用任一机制。
用的不多,Tomcat,Netty等都使用的是NIO, 因为AIO每个channel都需要预先分配一些缓存。连接量起来后,占用缓存太多,不是太成熟。
APR
之前一直都在说APR,那么APR到底能给我们带来什么?这节就开始学习APR相关知识。
APR(Apache Portable Runtime)是一个高可移植库,它是Apache HTTP Server 2.x的核心。APR有很多用途,包括访问高级 IO功能(例如sendfile,epoll和OpenSSL),OS级别功能(随机数生成,系统状态等等),本地进程管理(共享内存,NT管道和UNIX sockets)。这些功能可以使Tomcat作为一个通常的前台WEB服务器,能更好地和其它本地web技术集成,总体上让Java更有效率作为一个高性能web服务器平台而不是简单作为后台容器。
APR的目的如其名称一样,主要为上层的应用程序提供一个可以跨越多操作系统平台使用的底层支持接口库。在早期的Apache版本中,应用程序本身必须能够处理各种具体操作系统平台的细节,并针对不同的平台调用不同的处理函数。随着Apache的进一步开发,Apache组织决定将这些通用的函数独立出来并发展成为一个新的项目。这样,APR的开发就从Apache中独立出来,Apache仅仅是使用APR而已。目前APR主要还是由Apache使用,不过由于APR的较好的移植性,因此一些需要进行移植的C程序也开始使用APR。
APR使得平台细节的处理进行下移。对于应用程序而言,它们根本就不需要考虑具体的平台,不管是Unix、linux还是Window,应用程序执行的接口基本都是统一一致的。因此对于APR而言,可移植性和统一的上层接口是其考虑的一个重点。而APR最早的目的并不是如此,它最早只是希望将Apache中用到的所有代码合并为一个通用的代码库,然而这不是一个正确的策略,因此后来APR改变了其目标。有的时候使用公共代码并不是一件好事,比如如何将一个请求映射到线程或者进程是平台相关的,因此仅仅一个公共的代码库并不能完成这种区分。APR的目标则是希望安全合并所有的能够合并的代码而不需要牺牲性能。
Tomcat扩展了线程池增强了功能。
JDK线程池流程:minThreads --> queue --> maxThreads --> Exception
Tomcat增强后: minThreads --> maxThreads --> queue --> Exception
JDK的连接池流程:

TOMCAT连接池流程

日志线程名称分析
tomcat打印的日志记录分析
2025-04-29 09:28:36.425 INFO 3020 --- [o-9001-exec-101]
线程对应BIO模式(同步阻塞),适用于低并发长连接场景;
2025-04-29 09:28:36.425 INFO 3020 --- [io-9001-exec-101]
线程可能为NIO模式(同步非阻塞)的简化标识,适用于高并发短连接场景。设置内嵌tomcat为nio2
@Configuration
public class TomcatConf implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
}
}