为您推荐:
精华内容
最热下载
问答
  • 5星
    185MB csde12 2021-07-12 16:56:02
  • 5星
    5.16MB weixin_46831482 2021-09-14 15:57:03
  • 5星
    15.07MB z18223345669 2021-01-11 19:37:48
  • 5星
    6.59MB helongqiang 2021-06-23 22:24:41
  • 5星
    36.05MB tangyang8942 2021-06-12 16:15:06
  • 5星
    3.04MB qq_34715954 2021-06-05 13:33:35
  • 5星
    14.48MB tangyang8942 2021-06-26 20:54:35
  • 5星
    52.18MB huayula 2021-09-07 06:59:33
  • 5星
    25.86MB tangyang8942 2021-06-12 17:46:24
  • 5星
    24.84MB tangyang8942 2021-06-13 22:04:38
  • 这样,这些应用服务器的性能基本上就取决于Tomcat处理HTTP请求的connector模块的性能。本文首先从应用层次分析了tomcat所有的connector种类及用法,接着从架构上分析了connector模块在整个tomcat中所处的位置,最后...

    很多开源应用服务器都是集成tomcat作为web container的,而且对于tomcat的servlet container这部分代码很少改动。这样,这些应用服务器的性能基本上就取决于Tomcat处理HTTP请求的connector模块的性能。本文首先从应用层次分析了tomcat所有的connector种类及用法,接着从架构上分析了connector模块在整个tomcat中所处的位置,最后对connector做了详细的源代码分析。并且我们以Http11NioProtocol为例详细说明了tomcat是如何通过实现ProtocolHandler接口而构建connector的。

    4 如何实现Connector

    由上面的介绍我们可以知道,实现Connector就是实现ProtocolHander接口的过程。

    AjpAprProtocol、AjpProtocol、Http11AprProtocol、Http11Protocol、JkCoyoteHandler、MemoryProtocolHandler这些实现类的实现流程与Http11NioProtocol相同,下面我们以Http11NioProtocol为类重点说明tomcat中如何实现ProtocolHander接口的。

    Http11NioProtocol实现了ProtocolHander接口,它将所有的操作委托给NioEndpoint类去做,如下图:

    p_w_picpath1.jpg

    p_w_picpath2.jpgNioEndpoint类中的init方法中首先以普通阻塞方式启动了SocketServer:

    NioEndpoint类的start方法是关键,如下:

    p_w_picpath3.jpg

    可以看出,在start方法中启动了两个线程和一个线程池:Acceptor线程,该线程以普通阻塞方式接收客户端请求(socket.accep()),将客户Socket交由线程池是处理,线程池要将该Socket配置成非阻塞模式(socket.configureBlocking(false)),并且向Selector注册READ事件。该线程数目可配置,默认为1个。

    Poller线程,由于Acceptor委托线程为客户端Socket注册了READ事件,当READ准备好时,就会进入Poller线程的循环,Poller线程也是委托线程池去做,线程池将NioChannel加入到ConcurrentLinkedQueue队列中。该线程数目可配置,默认为1个。

    线程池,就是上面说的做Acceptor与Poller线程委托要做的事情。

    4.1 Init接口实现方法中阻塞方式启动ServerSocketChannel

    在Init接口实现方法中阻塞方式启动ServerSocketChannel。

    p_w_picpath4.jpg

    4.2 Start接口实现方法中启动所有线程

    Start方法中启动了线程池,acceptor线程与Poller线程。其中acceptor与poller线程一般数目为1,当然,数目也可配置。

    p_w_picpath5.jpg

    可以看出,线程池有两种实现方式:普通queue + wait + notify方式,默认使用的方式,据说实际测试这种比下种效率高

    JDK1.5自带的线程池方式

    4.3 Acceptor线程接收客户请求、注册READ事件

    在Acceptor线程中接收了客户请求,同时委托线程池注册READ事件。在Acceptior线程中接收了客户请求(serverSock.accept())

    p_w_picpath6.jpg

    委托线程池处理

    p_w_picpath7.jpg

    在线程池的Worker线程的run方法中有这么几句:

    p_w_picpath8.jpg

    在setSocketOptions方法中,首先将socket配置成非阻塞模式:

    p_w_picpath9.jpg

    在setSocketOptions方法中,最后调用getPoller0().register(channel);一句为SocketChannel注册READ事件,register方法代码如下(注意:这是Poller线程的方法):

    p_w_picpath10.jpg

    其中p_w_upload的结构如下,它可以看做是一个共享的数据结构:

    p_w_picpath11.jpg

    4.4 Poller线程读请求、生成响应数据、注册WRITE事件在上面说的setSocketOptions方法中调用Poller线程的register方法注册读事件之后,当READ准备就绪之后,就开始读了。下面代码位于Poller线程的run方法之中:

    p_w_picpath12.jpg

    可以看到,可读之后调用processSocket方法,该方法将读处理操作委拖给线程池处理(注意此时加入到线程池的是NioChannel,不是SocketChannel):

    p_w_picpath13.jpg

    线程池的Worker线程中的run方法中的部分代码如下(请注意handler.process(socket)这一句):

    p_w_picpath14.jpg

    注意:调用了hanler.process(socket)来生成响应数据)

    数据生成完之后,注册WRITE事件的,代码如下:

    p_w_picpath15.jpg

    4.5 Handle接口实现类通过Adpater调用Servlet容器生成响应数据

    NioEndpoint类中的Handler接口定义如下:

    p_w_picpath16.jpg

    其中process方法通过Adapter来调用Servlet Container生成返回结果。Adapter接口定义如下:

    p_w_picpath17.jpg

    4.6 小结

    实现一个tomcat连接器Connector就是实现ProtocolHander接口的过程。Connector用来接收Socket Client端的请求,通过内置的线程池去调用Servlet Container生成响应结果,并将响应结果同步或异步的返回给Socket Client。在第三方应用集成tomcat作为Web容器时,一般不会动Servlet Container端的代码,那么connector的性能将是整个Web容器性能的关键。

    展开全文
    weixin_42364040 2021-04-23 06:35:41
  • 前言:最近,测试部门的共事找到我,说他们测试时,没一会就发现服务接口申请始终无响应,Tomcat跟死掉了一样,也没有返回任何的谬误响应,说让我连忙排查下;听完后,我霎时激灵了下,妹的,最近老是出问题,领导都...

    最艰难的事件就是意识本人!

    集体网站 ,欢送拜访!

    前言:

    最近,测试部门的共事找到我,说他们测试时,没一会就发现服务接口申请始终无响应,Tomcat跟死掉了一样,也没有返回任何的谬误响应,说让我连忙排查下;听完后,我霎时激灵了下,妹的,最近老是出问题,领导都要给我开批评大会了。哈哈,开玩笑的,像我这么英俊的人,领导怎么会忍心批评我呢,哼,我把这个问题马上解决掉,都不会让领导晓得的!

    简略说下程序部署状况:tomcat + oracle

    排查过程:

    排查时,能够应用命令进行排查,也能够应用可视化监控工具;例如应用应用JDK自带的 jvisualvm.exe 监控工具。

    命令排查过程:

    1、申请服务无响应,首先看看tomcat是否是真的挂掉了:

    命令: ps -ef | grep tomcat

    通过下面的命令查看tomcat运行着;执行后果如下:

    留神: 如果此服务器中运行着多个tomcat,须要查看下图中画框的中央运行的tomcat地址是否正确;

    通过命令查看发现,tomcat失常运行着,那么这就是处于假死状态,上面接着排查。

    2、查看http申请是否达到了tomcat:

    通过查看 tomcat 的 logs 目录下的 localhost_access_log 日志文件中 申请记录;

    命令: tail -100f localhost_access_log

    通过下面的命令查看实时的日志,执行完下面的查看日志的命令后,而后再次申请下程序,在日志中并没有发现申请记录,阐明tomcat处于一种假死状态,上面接着排查。

    3、查看tomcat的JVM的GC状况:

    查看GC状况,是否因为频繁的GC,长时间的GC,导致程序呈现长时间的卡顿,最终导致申请来不及解决,进入队列中进行期待,调用方长时间得不到响应,造成tomcat假死状态;

    命令:jstat -gc pid time count

    例如: jstat -gc 71129 1000 5 监控 71129 这个过程JVM的GC状况,每隔1000ms 输入一次,共输入5次;

    命令执行后果参数解析:

    通过下面命令查看GC状况,发现垃圾回收也不频繁,并且进行GC的工夫也不长,应该不是GC的起因。

    4、查看tomcat的JVM的堆状况:

    查看堆内存的状况,是否存在堆内存溢出 导致tomcat假死,无奈为新申请调配堆内存资源;

    命令 : jmap -heap pid

    例子: jmap -heap 71129 71129是正在运行tomcat的过程号 ;

    通过命令执行后果得悉,堆内存中可应用内存还很大,不会呈现内存溢出的问题,所以也不是堆内存过小导致的tomcat假死。

    5、查看tomcat的 JVM线程状况:

    ①、应用 jstack 命令导出以后JVM的线程dump快照,而后看看dump中线程都在干什么?

    命令:jstack pid >> jvmThreadDump.log

    例子:jstack 71129 >> jvmThreadDump.log

    生成 71129 过程的 JVM的线程快照,并将快照内容重定向到 jvmThreadDump.log 文件中;

    留神:生成的 jvmThreadDump.log 在你以后执行命令的目录下。

    ②、接着应用命令 more 查看 jvmThreadDump.log 内容;

    命令 : more jvmThreadDump.log

    如果的dump文件太大的话,须要应用more 命令一点点看;执行完more 命令的话,再按 enter 回车键 一点点展现文件内容;

    ③、通过查看线程快照文件,发现很多线程的状态是 WAITING 期待状态;

    并且应用命令查看线程状态为 WAITING 的线程占总线程的比例:

    留神:tomcatDump.log 为生成的线程快照文件名称,记得改为本人设置的名称 ;

    count=`cat tomcatDump.log | grep java.lang.Thread.State | wc -l`; wait=`cat tomcatDump.log | grep WAITING | wc -l`; a=`echo | awk "{print $wait/$count*100}"`; echo "$a%"

    执行命令,失去后果 : 91.9786% ,发现九成多的线程处于期待状态;

    至此,找到了tomcat假死的起因,然而还需进一步确定 什么起因导致的大量线程始终期待?

    通过查看调用的服务接口代码得悉,此接口业务逻辑中本人没设置任何的锁,所以应该不是本人写的代码的问题,然而此接口中波及到了很多 JDBC数据库操作,那是不是数据库连接池中的连贯不够用了呢?因为数据库连贯属于竞争资源,如果连接池中的连贯曾经耗尽了,那么接下来的进行 JDBC的线程就须要进行wait 期待连贯。

    6、查看与数据库建设的TCP连贯状况:

    在下面发现,大量线程处于期待状态,而通过剖析得悉,可能是因为数据库连接池中的连贯耗尽导致的,所以能够通过命令查看下,部署服务代码的服务器与数据库所在服务器建设的TCP连接数是否曾经达到了配置的数据库连接池的最大连接数;

    命令:netstat -pan | grep 1521 | wc -l

    因为本文中应用的数据库是Oracle,所以 grep 搜寻匹配的端口号是 1521;

    如果是mysql数据库则将端口号改为3306 即可, netstat -pan | grep 3306 | wc -l ;

    如果设置了自定义的数据库端口号,则改为自定义的端口号即可;

    通过命令查问到 曾经应用的数据库的连接数为 6 个,那接着看下设置的数据库连接池最大连接数;

    数据源配置如下:

    type="javax.sql.DataSource"

    factory="com.alibaba.druid.pool.DruidDataSourceFactory"

    url="jdbc:oracle:thin:@192.168.3.125:1521:ora11g"

    driverClassName="oracle.jdbc.driver.OracleDriver"

    username="root"

    password="root"

    auth="Container"

    initialSize="2"

    maxActive="6"

    minIdle="3"

    maxWait="30000"

    timeBetweenEvictionRunsMillis="30000"

    minEvictableIdleTimeMillis="600000"

    maxEvictableIdleTimeMillis="900000"

    poolPreparedStatements="true"

    maxOpenPreparedStatements="20"

    validationQuery="select 1 from dual"

    testOnBorrow="false"

    testOnReturn="false"

    testWhileIdle="true"

    filters="wall,stat,log4j2"

    />

    通过查看数据源发现,连接池配置的最大连接数是 maxActive=”6″ ;发现目前程序中应用的连接数已达到最大值,那么前面再进行 JDBC 操作的线程将进入 期待状态 ,期待获取连贯;

    至此,tomcat假死的排查过程就完结了,并且起因也找到了,就是数据库连接池中的连贯耗尽了;所以,在前面测试中,须要在数据源中将最大连接数设置的大一些,并且也再进一步查看下代码,看看是否存在数据库连贯应用完后没有进行敞开的问题。

    除了数据库连接池连贯耗尽会导致tomcat假死外,还有一些其它的状况也会导致产生,例如: redis 连接池连贯耗尽,或者是redis连贯应用完不开释,最终导致redis连贯耗尽。

    除了应用下面的命令进行问题排查外,也能够间接应用可视化监控工具进行排查,更加简便、直观。

    可视化监控工具排查

    应用 JDK 自带的 jvisualvm.exe 工具进行 JMX近程 可视化监控tomcat;

    jvisualvm.exe 位于 $JAVA_HOME/bin 目录下;

    1、应用JMX实现近程监控步骤:

    上面应用 JMX实现近程监控的内容参考自:jvisualvm近程监控tomcat

    ①、在 Tomcat 的 bin 目录下的 startup.sh 文件中的 倒数第一行上 加上如下内容:

    export CATALINA_OPTS="$CATALINA_OPTS

    -Dcom.sun.management.jmxremote

    -Djava.rmi.server.hostname=192.168.1.130

    -Dcom.sun.management.jmxremote.port=7003

    -Dcom.sun.management.jmxremote.ssl=false

    -Dcom.sun.management.jmxremote.authenticate=false"

    下面内容参数解析:

    -Dcom.sun.management.jmxremote 启用JMX近程监控

    -Djava.rmi.server.hostname=192.168.1.130 这是连贯你的tomcat所在的服务器地址

    -Dcom.sun.management.jmxremote.port=7003 jmx连贯端口

    -Dcom.sun.management.jmxremote.ssl=false 是否ssl加密

    -Dcom.sun.management.jmxremote.authenticate=false 近程连贯须要明码认证

    在 startup.sh 文件中增加上下面的内容后,须要将tomcat重启下才会失效;

    ②、将 jvisualvm.exe 关上,界面如下:

    ③、在近程上右击,增加主机,输出服务器的ip:就是在 startup.sh 文件中增加内容中的hostname

    ④、在近程主机上右击,增加 JMX连贯 ,手动在ip地址前面加上设置的jmx连贯端口7003,而后点击确定即可:

    ⑤、通过下面的步骤,就曾经实现了近程监控连贯了,而后本人双击就能进行监控界面了:

    2、查看监督内容:

    通过查看监督画面得悉,CPU、GC、堆Heap的状况都没有问题,那接着查看下线程的状况:

    点击下面图片中的 线程Dump 按钮,生成线程的快照,快照文件内容局部如下:

    通过查看快照文件内容发现,很多线程的状态的都是 WAITING 期待状态;

    接下来的剖析排查过程就如下面的 命令排查过程 一样了。

    总结:

    下面的两种排查形式,自己比拟举荐还是应用第一种 命令排查 ,因为很多的状况是不会让你批改配置文件进行近程监控的,即使应用监控工具看起来更加直观、简便;所以,平时须要记一些罕用的排查命令,以备不时之需。

    因为自己程度无限,如有问题,敬请提出;

    ❤不要遗记留下你学习的脚印 [点赞 + 珍藏 + 评论]嘿嘿ヾ

    所有看文章不点赞都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!开个玩笑,动一动你的小手,点赞就完事了,你每个人出一份力量(点赞 + 评论)就会让更多的学习者退出进来!非常感谢! ̄ω ̄=

    展开全文
    weixin_42601134 2021-08-01 06:05:46
  • 1. 前言 参考“The HTTP Connector”...Tomcat的每个请求的持续期间,都需要一个线程。如果当前存在的用来处理请求的线程不足以处理接收到的并发请求,将会创

    1. 前言

    参考“The HTTP Connector”( http://tomcat.apache.org/tomcat-8.5-doc/config/http.html#Introduction ),描述了当Tomcat接收到新的请求,直到超过Tomcat处理能力时,Tomcat的处理过程,涉及到三个相关的属性:maxThreads、maxConnections、acceptCount。

    • Tomcat的每个请求的持续期间,都需要一个线程。如果当前存在的用来处理请求的线程不足以处理接收到的并发请求,将会创建另外的线程,直接达到配置的maxThreads属性。

    • 当连接数量达到了maxConnections属性时,Tomcat服务器会接收(accept),但不会处理更多的请求。额外的请求会被阻塞,直到被处理的连接数量降到maxConnections以下,此时Tomcat服务器会开始接受并处理新的连接。当超过了以上限制时,操作系统可能仍然会基于acceptCount属性值接收新的连接。

    • 如果仍然接收到更多的并发请求,这些请求会堆积在Connector创建的服务器socket内,直到达到配置的acceptCount属性。更多的并发请求会接收到连接被拒绝(connection refused)的错误,直到可以有处理的资源可用为止。

    maxThreads、maxConnections、acceptCount属性是Tomcat标准HTTP Connector(NIO, NIO2,APR/native)都支持的属性。

    maxQueueSize是Executor元素的属性,用于配置处理请求的任务队列长度。

    当Tomcat接收的请求超过了以上属性时,都会导致Tomcat无法正常访问,以下分别进行分析。

    以下使用的Tomcat版本为8.5.63,使用NIO Connector。

    2. Tomcat connector分析

    可参考 https://blog.csdn.net/a82514921/article/details/115359632

    3. maxThreads(处理请求最大线程数)

    maxThreads属性指定当前Connector可以创建用于处理请求的最大线程数,决定了可以处理并发请求的最大数量。该属性默认值为200。

    若当前Connector关联了某个executor时,会使用指定的executor执行任务,而不会使用Connector的内部线程池,maxThreads属性也会被忽略。

    以下首先分析Connector使用内部线程池的情况。

    3.1. 创建处理请求的线程池代码分析

    Tomcat Connector协议类的父类为org.apache.coyote.AbstractProtocol,该类在tomcat-coyote.jar中,使用了Connector的maxThreads属性,get/setMaxThreads()方法调用了AbstractEndpoint类型endpoint对象的get/setMaxThreads()方法,对成员变量maxThreads进行了操作。

    AbstractEndpoint类的子类NioEndpoint、Nio2Endpoint、AprEndpoint中,在startInternal()方法中判断若this.getExecutor()等于null,则执行this.createExecutor()方法,在Nio2Endpoint类的bind()方法中,也执行了该方法。

    createExecutor()方法定义在AbstractEndpoint类中,该方法创建用于处理请求的线程池,执行以下操作:

    • 设置internalExecutor=true,代表Connector使用内部线程池;

    • 创建org.apache.tomcat.util.threads.TaskQueue类的对象,作为任务队列;

    • 创建org.apache.tomcat.util.threads.TaskThreadFactory类的对象,该类继承自java.util.concurrent.ThreadFactory类,在创建以上对象时,namePrefix设置为“this.getName() + “-exec-””;

    在之前分析过,AbstractEndpoint类的getName()返回值等于AbstractProtocol类的getName()返回值,AbstractProtocol各子类及对应的AbstractEndpoint子类,及Connector使用HTTP协议时(使用HTTPS协议时不同)对应的getName()返回值开头如下:

    AbstractProtocol各子类AbstractEndpoint各子类getName()返回值开头
    Http11ProtocolNioEndpointhttp-nio
    Http11NioProtocolNioEndpointhttp-nio
    Http11Nio2ProtocolNio2Endpointhttp-nio2
    Http11AprProtocolAprEndpointhttp-apr
    • 创建org.apache.tomcat.util.threads.ThreadPoolExecutor类的对象,作为线程池,corePoolSize使用this.getMinSpareThreads(),maximumPoolSize使用this.getMaxThreads(),keepAliveTime使用60秒,workQueue使用上述TaskQueue类的对象,threadFactory使用上述TaskThreadFactory类的对象;

    getMinSpareThreads()方法返回值可认为等于minSpareThreads属性值,getMaxThreads()方法返回值可认为等于maxThreads属性值。即以上创建的用于处理请求的线程池,corePoolSize等于minSpareThreads,maximumPoolSize等于maxThreads。

    使用jstack或jconsole命令查看Tomcat进程中Connector相关的线程,可以看到如“http-nio-8080-exec-xxx”之类的线程,即以上代码创建的线程。

    • 调用以上生成的TaskQueue类的对象的setParent()方法,设置为以上生成的ThreadPoolExecutor类的对象。

    3.2. Tomcat线程池与任务队列代码分析

    3.2.1. Tomcat线程池创建过程

    org.apache.tomcat.util.threads.ThreadPoolExecutor类,继承自java.util.concurrent.ThreadPoolExecutor类,在tomcat-util.jar中。

    以上创建org.apache.tomcat.util.threads.ThreadPoolExecutor类对象时,调用的构造方法为“ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)”,在该方法中,会调用父类即java.util.concurrent.ThreadPoolExecutor类的构造方法,传入的RejectedExecutionHandler对象为org.apache.tomcat.util.threads.ThreadPoolExecutor类中的RejectHandler类的对象。

    org.apache.tomcat.util.threads.ThreadPoolExecutor$RejectHandler类实现了RejectedExecutionHandler接口,在rejectedExecution()方法中抛出了RejectedExecutionException异常,与java.util.concurrent.ThreadPoolExecutor$AbortPolicy效果相同。

    即Tomcat Connector内部线程池,拒绝策略使用RejectedExecutionException,即抛出异常以拒绝任务。

    3.2.2. Tomcat任务队列创建过程

    TaskQueue类继承自java.util.concurrent.LinkedBlockingQueue类。

    以上创建TaskQueue类对象时,调用的构造方法为无参数构造方法,会调用父类LinkedBlockingQueue的无参数构造方法。

    LinkedBlockingQueue类的无参数构造方法调用了LinkedBlockingQueue(int capacity)构造方法,传入的容量为Integer.MAX_VALUE,可认为长度是无限。

    即Tomcat Connector内部线程池的任务队列长度为无限(Integer.MAX_VALUE)。

    3.2.3. Tomcat线程池创建新线程的条件

    org.apache.tomcat.util.threads.ThreadPoolExecutor类的execute()方法调用java.util.concurrent.ThreadPoolExecutor类的execute()方法。在java.util.concurrent.ThreadPoolExecutor类中,addWorker()方法用于尝试创建工作线程。

    众所周知,使用java.util.concurrent.ThreadPoolExecutor类,若线程数已达到corePoolSize且未超过maximumPoolSize,当队列满时,才会创建新的线程;使用org.apache.tomcat.util.threads.ThreadPoolExecutor类,若线程数已达到corePoolSize且未超过maximumPoolSize,当有请求没有空闲线程能够处理时,就会创建新的线程。以下针对以上创建新线程条件的不同进行对比。

    在java.util.concurrent.ThreadPoolExecutor类的execute()方法中,若调用workQueue.offer()方法向工作队列中插入元素返回false,则会调用addWorker()方法尝试创建工作线程。

    对于LinkedBlockingQueue类的offer()方法,仅当队列元素满时才会返回false,使java.util.concurrent.ThreadPoolExecutor类的execute()方法创建新的线程。

    对于TaskQueue类的offer()方法,当this.parent.getPoolSize()小于this.parent.getMaximumPoolSize()时会返回false,即TaskQueue类对象关联的org.apache.tomcat.util.threads.ThreadPoolExecutor类对象的线程数量小于maximumPoolSize时,就可以创建新的线程。

    3.3. Tomcat处理请求线程池已满场景模拟及分析

    将Tomcat Connector的maxConnections、acceptCount属性值配置得足够大,将maxThreads设置为1,模拟Tomcat处理请求线程池已满的场景。

    <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" acceptCount="100" maxConnections="100" maxThreads="1"/>
    

    使用curl命令发起请求,访问Tomcat对应端口,通过–trace-time参数指定打印curl每一步执行时的时间。

    在多个终端同时执行以下命令,使每次请求将Tomcat处理请求的线程占用指定时间:

    curl -v --trace-time http://127.0.0.1:8081/test/sleep_seconds.jsp?sleep_seconds=5
    

    可以观察到以下现象:

    • 当Tomcat处理请求的线程不够时,新的请求能够建立连接,但无法立刻读取到服务器返回数据,会被阻塞,现象与Acceptor线程被终止时相同,客户端连接不会被Tomcat终止;

    • 当Tomcat处理请求的线程执行完毕变为空闲后,会开始依次执行被阻塞的请求(如在3个终端分别执行以上命令,分别需要等待5、10、15秒读取到服务器返回)。

    以上现象的原因如下:

    • Tomcat处理请求时,当连接完成并被accept之后,使用线程池处理对应请求;

    • 当线程池数量达到maxThreads导致不能再创建线程后,新到的请求如果完成连接并被accept之后,会被加入到任务队列中;

    • Tomcat Connector默认使用内部线程池,对应的任务队列长度为Integer.MAX_VALUE,可认为队列长度为无限;

    • 如果一直没有线程处理结束变为空闲,则处在任务队列中的请求会一直等待,客户端此时无法读取到Tomcat的返回,Tomcat也不会主动将任务队列中的请求结束连接,因此客户端会一直等待;

    • 当有线程处理结束变为空闲后,会依次处理任务队列中的请求,此时客户端可以读取到Tomcat的返回,并结束此次请求。

    以上sleep_seconds.jsp文件用于使Tomcat的处理请求的线程在执行时等待指定的时间,内容如下,

    <%@ page import="java.lang.*"%>
    
    <%
    	String sleepSeconds = request.getParameter("sleep_seconds");
    	System.out.println("begin to sleep: " + Thread.currentThread().getName() + " : " + sleepSeconds);
    	
    	long startTime = System.currentTimeMillis();
    	Thread.sleep(Integer.parseInt(sleepSeconds) * 1000L);
    	System.out.println("after sleep: " + Thread.currentThread().getName() + " : " + sleepSeconds);
    	out.print("start: " + startTime + " end: " + System.currentTimeMillis());
    %>
    

    4. maxQueueSize(处理请求队列长度)

    Tomcat Connector默认使用内部线程池,当Connector使用命名Executor时,可以通过maxQueueSize属性指定处理请求队列长度。

    4.1. Connector使用命名Executor

    Tomcat标准HTTP Connector都支持executor属性,该属性用于指定conf/server.xml配置文件中Executor元素。当有配置该属性,且对应的命名executor存在时,connector会使用对应的executor(否则),其他与线程相关的属性会被忽略。否则connector会使用私有的内部线程池。

    参考 “The Executor (thread pool)”( http://tomcat.apache.org/tomcat-8.5-doc/config/executor.html ),Executor元素支持以下配置:

    • className

    指定线程池实现类,需要实现org.apache.catalina.Executor接口,默认值为org.apache.catalina.core.StandardThreadExecutor。

    • name

    线程池名称,在server.xml可通过该名称进行引用,该属性必须指定,且需要是唯一的。

    • daemon

    指定线程是否需要为daemon线程,默认值为true。

    • namePrefix

    指定由线程池创建的每个线程名称的前缀,线程名称格式为namePrefix + threadNumber。

    • maxThreads

    线程池中活动线程最大数量,默认值为200。该默认值与Connector使用内部线程池时值相同。

    • minSpareThreads

    永远保持活动的线程最小值,默认值为25。当Connector使用内部线程池时,该默认值为10。

    • maxIdleTime

    空闲线程被关闭前的时间毫秒数,默认值为60000(1分钟)。

    • maxQueueSize

    能够进入队列排队的可执行任务最大数量,当超过该值后,Tomcat会拒绝新的任务。默认值为Integer.MAX_VALUE。

    4.2. 命名Executor创建线程池代码分析

    命名Executor默认使用org.apache.catalina.core.StandardThreadExecutor类,该类在catalina.jar中,startInternal()方法中创建了线程池,执行以下操作:

    • 创建TaskQueue对象,作为任务队列,调用构造方法时传入的容量为this.maxQueueSize(创建内部线程池时,无法指定任务队列容量);

    • 创建TaskThreadFactory对象,namePrefix使用this.namePrefix,默认值为“tomcat-exec-”,daemon使用this.daemon;

    • 创建org.apache.tomcat.util.threads.ThreadPoolExecutor类的对象,作为线程池,与AbstractEndpoint.createExecutor()方法中的操作类似;

    • 调用以上生成的TaskQueue类的对象的setParent()方法,设置为以上生成的ThreadPoolExecutor类的对象。

    4.3. Tomcat线程池执行任务代码分析

    StandardThreadExecutor类的execute()方法的执行步骤如下:

    • 调用executor对象,即org.apache.tomcat.util.threads.ThreadPoolExecutor类的execute()方法;

    • 若出现RejectedExecutionException异常,且调用((TaskQueue)this.executor.getQueue()).force()方法向线程池任务队列中再次插入当前任务失败时,抛出异常RejectedExecutionException(“Work queue full.”)。

    org.apache.tomcat.util.threads.ThreadPoolExecutor类的execute()方法的主要执行步骤如下:

    • 调用super即java.util.concurrent.ThreadPoolExecutor类的execute()方法;

    • 若出现RejectedExecutionException异常,且调用((TaskQueue)this.executor.getQueue()).force()方法向线程池任务队列中再次插入当前任务失败时,抛出异常RejectedExecutionException。

    4.4. Tomcat处理请求的任务队列已满场景模拟

    Tomcat maxQueueSize默认值为Integer.MAX_VALUE,大于21亿,超过该值的可能性很小。为了模拟Tomcat maxQueueSize不够的场景,使用以下配置,指定Executor的maxThreads、minSpareThreads与maxQueueSize均为1。

    <Executor name="testThreadPool" maxThreads="1" minSpareThreads="1" maxQueueSize="1"/>
    
    <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" executor="testThreadPool"/>
    

    使用curl命令发起以下请求,使每次请求将Tomcat处理请求的线程占用较长的时间:

    curl -v --trace-time http://127.0.0.1:8081/test/sleep_seconds.jsp?sleep_seconds=100
    

    第一个被Tomcat处理的请求会执行以上jsp文件,等待指定的时间后会返回;

    由于Executor的maxThreads为1,第二个请求没有线程能够处理,会进入任务队列中,等待第一个请求被处理完毕后会被执行,再等待指定的时间后会返回;

    由于Executor的maxQueueSize为1,第三个及之后的请求没有线程能够处理,也无法加入到任务队列中,会被拒绝连接,Tomcat会主动断开连接。

    被拒绝连接的curl执行结果如下所示:

    13:15:42.342233 * About to connect() to 127.0.0.1 port 8081 (#0)
    13:15:42.342342 *   Trying 127.0.0.1...
    13:15:42.342450 * Connected to 127.0.0.1 (127.0.0.1) port 8081 (#0)
    13:15:42.342512 > GET /test/sleep_seconds.jsp?sleep_seconds=100 HTTP/1.1
    13:15:42.342512 > User-Agent: curl/7.29.0
    13:15:42.342512 > Host: 127.0.0.1:8081
    13:15:42.342512 > Accept: */*
    13:15:42.342512 >
    13:15:42.345888 * Recv failure: Connection reset by peer
    13:15:42.345931 * Closing connection 0
    curl: (56) Recv failure: Connection reset by peer
    

    查看Tomcat的catalina.out日志,可以看到以下日志,异常信息为“java.util.concurrent.RejectedExecutionException: Work queue full.”,由StandardThreadExecutor.execute()方法抛出,与上述代码一致。

    [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@10dcbc52:org.apache.tomcat.util.net.NioChannel@474893b1:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8081 remote=/127.0.0.1:56713]] for processing
            java.util.concurrent.RejectedExecutionException: Work queue full.
                    at org.apache.catalina.core.StandardThreadExecutor.execute(StandardThreadExecutor.java:172)
                    at org.apache.tomcat.util.net.AbstractEndpoint.processSocket(AbstractEndpoint.java:1105)
                    at org.apache.tomcat.util.net.NioEndpoint$Poller.processKey(NioEndpoint.java:896)
                    at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:872)
                    at java.lang.Thread.run(Thread.java:748)
    

    5. maxConnections(accept连接最大数量)

    maxConnections属性指定了Tomcat服务器在同一时间会accept并处理的连接数量。对于NIO与NIO2 Connector,该默认值为10000,对于APR/nativeConnector,默认值为8192。

    5.1. 创建Acceptor线程的代码

    创建Acceptor线程的代码,可参考 https://blog.csdn.net/a82514921/article/details/115359632

    5.2. 对连接进行accept处理代码分析

    AbstractEndpoint类的startAcceptorThreads()方法中创建了Acceptor线程,在创建Thread对象时,传入的Runnable对象为this.createAcceptor()方法的返回值,AbstractEndpoint类的createAcceptor()方法为抽象方法,AbstractEndpoint子类的createAcceptor()方法返回值如下:

    类名createAcceptor()方法返回值
    NioEndpointnew NioEndpoint.Acceptor()
    Nio2Endpointnew Nio2Endpoint.Acceptor()
    AprEndpointnew AprEndpoint.Acceptor()

    NioEndpoint$Acceptor、Nio2Endpoint$Acceptor、AprEndpoint$Acceptor类都继承自AbstractEndpoint$Acceptor类,实现了Runnable接口。以上类的run()方法中,在调用accept()方法接收连接前,都会调用AprEndpoint.this.countUpOrAwaitConnection()方法;在出现异常,或关闭连接前,都会调用this.countDownConnection()方法。

    countUpOrAwaitConnection()方法定义在AbstractEndpoint类中,在其中调用了类型为LimitLatch的成员变量connectionLimitLatch的countUpOrAwait()方法;countDownConnection()方法也定义在以上类中,在其中调用了类型为LimitLatch的成员变量connectionLimitLatch的countDown()方法。

    AbstractEndpoint类的成员变量connectionLimitLatch在initializeConnectionLatch()方法中,进行实例化,调用LimitLatch类的构建方法,传入的参数为this.getMaxConnections(),即Connector的maxConnections属性值。

    参考 http://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/tomcat/util/threads/LimitLatch.html 。

    LimitLatch类的构造方法,使用指定的初始限制实例化一个LimitLatch对象,该限制用于指定此锁存器并发获取的最大值。

    调用countUpOrAwait()方法时,如果存在可用共享锁存器,则获取一个共享锁存器;如果当前没有可用的共享锁存器,则等待一个共享锁存器。

    countDown()方法可以释放一个共享锁存器,使它可被其他线程使用。

    根据以上代码可知,AbstractEndpoint子类执行accept操作时的并发控制实现如下:

    • 初始化一个最大并发获取数等于maxConnections属性值的共享锁存器;

    • 在执行accept操作前,获取一个共享锁存器,若能获取到则立即执行accept操作;若无法获取到则等待获取到共享锁存器后,再执行accept操作;

    • 当连接出现异常,或需要关闭连接前,释放当前使用的共享锁存器,使其他线程能够使用。

    5.3. ServerSocket不进行accept处理场景分析

    在模拟Tomcat最大连接数已满的场景之前,首先分析Java的ServerSocket监听端口但不进行accept处理时的场景(不进行accept处理,与Tomcat最大连接数已满,无法进行accept处理可以认为是相同的场景)。

    以下代码,分别对应BIO及NIO的ServerSocket不进行accept处理的场景:

    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    
    public class ServerSocketBIONoAccept {
        public static void main(String[] args) throws Exception {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress("0.0.0.0", 8080), 1);
    
            while (true) {
                Thread.sleep(1000L);
            }
        }
    }
    
    import java.net.InetSocketAddress;
    import java.nio.channels.ServerSocketChannel;
    
    public class ServerSocketNIONoAccept {
        public static void main(String[] args) throws Exception {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            ServerSocket serverSocket = serverSocketChannel.socket();
            serverSocket.bind(new InetSocketAddress("0.0.0.0", 8080), 1);
    
            while (true) {
                System.out.println("wait accept");
                Socket socket = serverSocket.accept();
                System.out.println("after accept");
            }
        }
    }
    

    分别执行以上代码,会使用ServerSocket监听8080端口,但不会进行accept处理。使用telnet命令或其他客户端连接对应的端口,能够建立连接,但服务器不会有返回,客户端会一直阻塞等待服务器返回。与Tomcat的Acceptor线程终止后的现象一样。

    5.4. Tomcat最大连接数已满场景模拟

    maxConnections属性默认值足够大,Tomcat最大连接数通常情况下并不会被占满。将Tomcat Connector的acceptCount、maxThreads属性值配置得足够大,将maxConnections设置为1,模拟Tomcat最大连接数已满的场景。

    <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" acceptCount="100" maxConnections="1" maxThreads="100"/>
    

    在这种情况下,Tomcat的现象与Tomcat处理请求线程池已满,或Acceptor线程终止时的现象相同,即客户端可以建立连接,但服务器没有返回,客户端会一直阻塞等待服务器返回。

    6. acceptCount(connect队列长度)

    acceptCount属性用于指定当所有的处理请求的线程都在被使用时,处理接入连接请求的最大队列长度。默认值为100。

    当该队列已满时,任意接收到的请求会被拒绝。

    6.1. connect队列处理代码分析

    AbstractProtocol类的get/setAcceptCount()方法中,调用了AbstractEndpoint类型endpoint对象的get/setAcceptCount()方法,对成员变量acceptCount进行了操作。

    AbstractEndpoint子类中对于成员变量acceptCount的使用如下:

    • NioEndpoint

    在NioEndpoint类的bind()方法中,调用了“this.serverSock.socket().bind(addr, this.getAcceptCount())”方法,即java.net.ServerSocket类的bind(SocketAddress endpoint, int backlog)方法。

    • Nio2Endpoint

    在Nio2Endpoint类的bind()方法中,调用了“this.serverSock.bind(addr, this.getAcceptCount())”方法,即java.nio.channels.AsynchronousServerSocketChannel类的bind(SocketAddress local, int backlog)方法。

    • AprEndpoint

    在AprEndpoint类的bind()方法中,调用了“Socket.listen(this.serverSock, this.getAcceptCount())”方法,即org.apache.tomcat.jni.Socket类的native方法listen(long sock, int backlog)。可参考 http://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/tomcat/jni/Socket.html#listen(long,%20int) 。

    以上方法均说明backlog参数用于指定Sockect执行bind操作时,接入连接的最大队列长度。

    java.net.ServerSocket及java.nio.channels.AsynchronousServerSocketChannel类的bind()方法说明backlog参数的使用取决于具体的实现,有的实现可能会实现该最大长度的限制,有的实现可能会忽略该参数。

    java.net.ServerSocket类的bind()方法中,调用了“getImpl().listen(backlog)”方法,即java.net.SocketImpl类的listen(int backlog)方法,backlog参数用于指定接入请求的最大队列长度。当队列满后,若有连接请求达到时,连接会被拒绝。

    java.nio.channels.AsynchronousServerSocketChannel及org.apache.tomcat.jni.Socket类对于backlog参数的使用的说明未找到。

    6.2. ServerSocket的backlog已满场景分析

    6.2.1. 不能及时对套接字进行accept处理

    当ServerSocket不能及时对套接字进行accept处理时,以backlog参数值为长度的连接队列可能会被占满。

    如以上ServerSocketBIONoAccept、ServerSocketNIONoAccept类,在调用ServerSocket类的bind()方法时,backlog参数为1。且由于没有对套接字进行accept处理,仅能接受一个连接,之后连接队列会被占满。

    • 在Linux环境监听

    在Linux环境启动以上类后,可以使用“ curl -v http://127.0.0.1:8080 ”或telnet命令访问对应的端口;

    前backlog + 1个连接能够正常建立,之后的请求均无法建立连接。

    • 在Windows环境监听

    在Windows环境启动以上类后,可以使用telnet命令访问对应的端口。

    前backlog个连接能够正常建立,之后的请求均无法建立连接。

    6.2.2. 能够及时对套接字进行accept处理

    当ServerSocket能够及时对套接字进行accept处理时,以backlog参数值为长度的连接队列不会被占满。

    如修改以上ServerSocketBIONoAccept、ServerSocketNIONoAccept类,在执行ServerSocket类的bind()方法后,执行accept()方法,当能够及时对套接字进行accept处理时,连接队列不会被占满,可以接受多个连接。

    6.3. Tomcat连接队列已满场景模拟

    maxConnections属性默认值足够大,能够快速地对连接进行accept操作,连接队列通常情况下并不会被占满。将Tomcat Connector的acceptCount、maxThreads属性值设置为1,将maxConnections配置得足够大,模拟Tomcat连接队列已满的场景。

    <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" acceptCount="1" maxConnections="1" maxThreads="100"/>
    
    • Linux环境

    在Linux环境启动Tomcat,在多个客户端同时使用curl命令或其他方式分别访问上述sleep_seconds.jsp,使当前请求等待较长的时间才处理完毕,例如访问的URL为http://127.0.0.1:8081/test/sleep_seconds.jsp?sleep_seconds=100。

    前三个发送到Tomcat服务器的请求会被Tomcat处理,并等待服务器返回。从第四个请求开始连接被拒绝。

    • Windows环境

    前两个发送到Tomcat服务器的请求会被Tomcat处理,并等待服务器返回。从第三个请求开始连接被拒绝。

    展开全文
    a82514921 2021-03-31 20:15:59
  • Tomcat是一个web应用服务器,是一个Servlet/Jsp容器,主要负责将客户端请求传递给对应的Servlet,并且将Servlet的响应数据返回给客户端。 Tomcat是基于组件的服务器。 二、Tomcat体系结构 Tomcat是一个基于组件的...

    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 2021-02-01 12:43:19
  • zj735539703 2021-04-08 07:16:24
  • weixin_35728049 2021-03-04 00:33:56
  • qq_45074341 2021-05-30 16:32:51
  • jzx937345232 2021-06-01 21:54:44
  • hello_pw 2021-02-27 17:13:33
  • qq_33949023 2021-01-07 04:18:29
  • virgilli 2021-05-19 11:05:35
  • qq_33590654 2021-05-13 09:12:28
  • qq_29025955 2021-03-05 14:04:23
  • Xx__WangQi 2021-05-19 21:28:48
  • weixin_39521520 2021-03-09 09:22:16
  • weixin_33728774 2021-01-13 06:30:32
  • u010734852 2021-06-10 11:09:39
  • heihei1314A 2021-06-03 23:47:14
  • qq_42914528 2021-06-29 00:12:58
  • weixin_28875281 2021-02-10 01:31:33
  • lisheng5218 2021-01-23 18:01:32
  • ganjing222 2021-09-30 13:49:44
  • zx156955 2021-02-23 13:15:48
  • weixin_47061482 2021-03-08 14:27:05
  • MiaoWei_ 2021-02-24 20:21:53
  • weixin_39957951 2021-03-26 13:44:21
  • weixin_39734987 2020-12-30 06:40:49
  • guozhe888 2021-08-31 19:27:56
  • lanyingchu 2021-04-08 19:38:43
  • weixin_34333047 2021-03-13 06:27:17

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 35,440
精华内容 14,176
关键字:

tomcat请求被阻塞

友情链接: xuanzhuan.rar