• Tomcat系列(8)——Tomcat运行模式连接数和线程池


      Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。根据协议的不同,Connector可以分为HTTP Connector、AJP Connector等,本文只讨论HTTP Connector。

    一、三中运行模式Nio、Bio、APR

    1、Connector的protocol

      Connector在处理HTTP请求时,会使用不同的protocol。不同的Tomcat版本支持的protocol不同,其中最典型的protocol包括BIO、NIO和APR(Tomcat7中支持这3种,Tomcat8增加了对NIO2的支持,而到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持)。

      BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包。

    2、如何指定protocol

    Connector使用哪种protocol,可以通过<connector>元素中的protocol属性进行指定,也可以使用默认值。

    指定的protocol取值及对应的协议如下:

    • HTTP/1.1:默认值,使用的协议与Tomcat版本有关
    • org.apache.coyote.http11.Http11Protocol:BIO
    • org.apache.coyote.http11.Http11NioProtocol:NIO
    • org.apache.coyote.http11.Http11Nio2Protocol:NIO2
    • org.apache.coyote.http11.Http11AprProtocol:APR

      如果没有指定protocol,则使用默认值HTTP/1.1,其含义如下:在Tomcat7中,自动选取使用BIO或APR(如果找到APR需要的本地库,则使用APR,否则使用BIO);在Tomcat8中,自动选取使用NIO或APR(如果找到APR需要的本地库,则使用APR,否则使用NIO)。

    3、BIO/NIO有何不同

    无论是BIO,还是NIO,Connector处理请求的大致流程是一样的:

      在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response

      为了便于后面的说明,首先明确一下连接与请求的关系:连接是TCP层面的(传输层),对应socket;请求是HTTP层面的(应用层),必须依赖于TCP的连接实现;一个TCP连接中可能传输多个HTTP请求。

      在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了Acceptor和Worker:Acceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。其中Worker是Tomcat自带的线程池,如果通过<Executor>配置了其他线程池,原理与Worker类似

      在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller,处理流程如下图所示(图片来源:http://gearever.iteye.com/blog/1844203)。

      

      Acceptor接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。与BIO类似,Worker也可以被自定义的线程池代替。通过上述过程可以看出,在NIoEndpoint处理请求的过程中,无论是Acceptor接收socket,还是线程处理请求,使用的仍然是阻塞方式;但在“读取socket并交给Worker中的线程”的这个过程中,使用非阻塞的NIO实现,这是NIO模式与BIO模式的最主要区别(其他区别对性能影响较小,暂时略去不提)。而这个区别,在并发量较大的情形下可以带来Tomcat效率的显著提升:

      目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。

    二、线程池原理

    线程池一般有三个重要参数:
      1. 最大线程数。在程序运行的任何时候,线程数总数都不会超过这个数。如果请求数量超过最大数时,则会等待其他线程结束后再处理。
      2. 最大共享线程数,即最大空闲线程数。如果当前的空闲线程数超过该值,则多余的线程会被杀掉。
      3. 最小共享线程数,即最小空闲线程数。如果当前的空闲数小于该值,则一次性创建这个数量的空闲线程,所以它本身也是一个创建线程的步长。

    线程池有两个概念:
      1. Worker线程。工作线程主要是运行执行代码,有两种状态:空闲状态和运行状态。在空闲状态时,类似“休眠”,等待任务;处理运行状态时,表示正在运行任务(Runnable)。
      2. 辅助线程。主要负责监控线程池的状态:空闲线程是否超过最大空闲线程数或者小于最小空闲线程数等。如果不满足要求,就调整之。

    Tomcat线程池有如下参数:

       maxThreads, 最大线程数,tomcat能创建来处理请求的最大线程数

       maxSpareTHreads, 最大空闲线程数,在最大空闲时间内活跃过,但现在处于空闲,若空闲时间大于最大空闲时   间,则回收,小于则继续存活,等待被调度。

       minSpareTHreads,最小空闲线程数,无论如何都会存活的最小线程数

       acceptCount, 最大等待队列数 ,请求并发大于tomcat线程池的处理能力,则被放入等待队列等待被处理。

       maxIdleTime, 最大空闲时间,超过这个空闲时间,且线程数大于最小空闲数的,都会被回收   

    原理图

       tomcat原理如上图。Tomcat线程池在工作的时候,实际情况是:以上述线程池为例,Tomcat启动时如果没有请求过来,那么线程数(都是指线程池的)为0;一旦有请求,Tomcat会初始化minSapreThreads设置的线程数;一开始就创建最小空闲数的线程在池里,20个,当同一时间请求数量大于最小空闲数20,比如来了50个并发请求,那么线程池还需要创建30个线程来处理请求。这时候当请求都处理完了,持续来的请求低于50个的时候,那么当时间过了60秒,并发数还是没有达到50,那么从第50个线程开始,线程池将按照,空闲时间达到60s的,开始逐个回收,49个,48个,47个,如此回收。如果并发请求小于20个,那么线程池会回收至20个的时候,停止回收,这就是最小空闲数的作用,即使一个请求都没有,那么线程池也得保证随时都有20个。所谓空闲回收是指:一个线程在60s的时间内,一直处于等待。那么就可以判定该线程是空闲。如果这个空闲线程是在最小空闲数以上,则会被回收。当请求并发高于500最大空闲数的时候,线程池是会继续创建线程的,来满足特大突发性并发。当并发请求数降下之后,线程池中有空闲,那么,无论线程空闲时间是否达到60s,线程池都会进行回收至500。500以类的线程也会根据空闲时间是否大于60s来判断是否需要进行回收。

    三、线程池6大重要参数

      再回顾一下Tomcat处理请求的过程:在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response

    相对应的,Connector中的几个参数功能如下:

    1、连接acceptCount

      accept队列的长度;当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。默认值是100。

    2、连接maxConnections

      Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。

      默认值与连接器使用的协议有关:NIO的默认值是10000,APR/native的默认值是8192,而BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads)。在windows下,APR/native的maxConnections值会自动调整为设置值以下最大的1024的整数倍;如设置为2000,则最大值实际是1024。

    3、处理线程maxThreads

      请求处理线程的最大数量。默认值是200(Tomcat7和8都是的)。如果该Connector绑定了Executor,这个值会被忽略,因为该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。与并发量有关系。

      maxThreads规定的是最大的线程数目,并不是实际running的CPU数量;实际上,maxThreads的大小比CPU核心数量要大得多。这是因为,处理请求的线程真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。因此,在某一时刻,只有少数的线程真正的在使用物理CPU,大多数线程都在等待;因此线程数远大于物理核心数才是合理的。换句话说,Tomcat通过使用比CPU核心数量多得多的线程数,可以使CPU忙碌起来,大大提高CPU的利用率

    4、处理线程minSpareThreads

      minSpareTHreads,最小空闲线程数,无论如何都会存活的最小线程数

    5、处理线程maxSpareThreads

      maxSpareTHreads, 最大空闲线程数,在最大空闲时间内活跃过,但现在处于空闲,若空闲时间大于最大空闲时   间,则回收,小于则继续存活,等待被调度。

    6、处理线程maxIdeleTime

      maxIdleTime, 最大空闲时间,超过这个空闲时间,且线程数大于最小空闲数的,都会被回收   

    7、参数设置

    (1)maxThreads的设置既与应用的特点有关,也与服务器的CPU核心数量有关。通过前面介绍可以知道,maxThreads数量应该远大于CPU核心数量;而且CPU核心数越大,maxThreads应该越大;应用中CPU越不密集(IO越密集),maxThreads应该越大,以便能够充分利用CPU。当然,maxThreads的值并不是越大越好,如果maxThreads过大,那么CPU会花费大量的时间用于线程的切换,整体效率会降低。

    (2)maxConnections的设置与Tomcat的运行模式有关。如果tomcat使用的是BIO,那么maxConnections的值应该与maxThreads一致;如果tomcat使用的是NIO,maxConnections值应该远大于maxThreads。

    (3)通过前面的介绍可以知道,虽然tomcat同时可以处理的连接数目是maxConnections,但服务器中可以同时接收的连接数为maxConnections+acceptCount 。acceptCount的设置,与应用在连接过高情况下希望做出什么反应有关系。如果设置过大,后面进入的请求等待时间会很长;如果设置过小,后面进入的请求立马返回connection refused

    四、线程池Executor

    Executor元素代表Tomcat中的线程池,可以由其他组件共享使用;要使用该线程池,组件需要通过executor属性指定该线程池。

    Executor是Service元素的内嵌元素。一般来说,使用线程池的是Connector组件;为了使Connector能使用线程池,Executor元素应该放在Connector前面。Executor与Connector的配置举例如下:

    1
    2
    <Executor name="tomcatThreadPool" namePrefix ="catalina-exec-" maxThreads="150" minSpareThreads="4" />
    <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" acceptCount="1000" />

    Executor的主要属性包括:

    • name:该线程池的标记
    • maxThreads:线程池中最大活跃线程数,默认值200(Tomcat7和8都是)
    • minSpareThreads:线程池中保持的最小线程数,最小值是25
    • maxIdleTime:线程空闲的最大时间,当空闲超过该值时关闭线程(除非线程数小于minSpareThreads),单位是ms,默认值60000(1分钟)
    • daemon:是否后台线程,默认值true
    • threadPriority:线程优先级,默认值5
    • namePrefix:线程名字的前缀,线程池中线程名字为:namePrefix+线程编号

     抄录网址

    1. 详解tomcat的连接数与线程池
    2. 详解Tomcat线程池原理及参数释义
    3. tomcat 线程池
    4. 详解Tomcat线程池原理及参数释义
  • 相关阅读:
    02SpringMvc_springmvc快速入门小案例(XML版本)
    01SpringMvc_初识工作流程
    07JavaIO详解_字符流
    06JavaIO详解_IO流中的设计模式-装饰者模式
    05JavaIO详解_仿照IO源码自己去实现一个IO流(为了加深印象,本身没有价值)
    作为程序员,我到底在恐慌什么
    android:layout_weight详解
    Android 遍历界面控件
    一个Activity中使用两个layout实例
    Android获取文件的MD5值
  • 原文地址:https://www.cnblogs.com/haimishasha/p/10747901.html
Copyright © 2020-2023  润新知