Tomcat 的链接数与线程池

前言

在使用tomcat时,常常会遇到链接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的链接器(Connector)。html

在前面的文章 详解Tomcat配置文件server.xml 中写到过:Connector的主要功能,是接收链接请求,建立Request和Response对象用于和请求端交换数据;而后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会经过Connector将响应返回给客户端。linux

能够说,Servlet容器处理请求,是须要Connector进行调度和控制的,Connector是Tomcat处理请求的主干,所以Connector的配置和使用对Tomcat的性能有着重要的影响。这篇文章将从Connector入手,讨论一些与Connector有关的重要问题,包括NIO/BIO模式、线程池、链接数等。数据库

根据协议的不一样,Connector能够分为HTTP Connector、AJP Connector等,本文只讨论HTTP Connector。apache

1、Nio、Bio、APR

一、Connector的protocol

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

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

二、如何指定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)。并发

三、BIO/NIO有何不一样

不管是BIO,仍是NIO,Connector处理请求的大体流程是同样的:socket

在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数目远大于最大线程数,并发性能大大提升。

2、3个参数:acceptCount、maxConnections、maxThreads

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

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

一、acceptCount

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

二、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。

三、maxThreads

请求处理线程的最大数量。默认值是200(Tomcat7和8都是的)。若是该Connector绑定了Executor,这个值会被忽略,由于该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。

maxThreads规定的是最大的线程数目,并非实际running的CPU数量;实际上,maxThreads的大小比CPU核心数量要大得多。这是由于,处理请求的线程真正用于计算的时间可能不多,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。所以,在某一时刻,只有少数的线程真正的在使用物理CPU,大多数线程都在等待;所以线程数远大于物理核心数才是合理的。

换句话说,Tomcat经过使用比CPU核心数量多得多的线程数,可使CPU忙碌起来,大大提升CPU的利用率。

四、参数设置

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

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

(3)经过前面的介绍能够知道,虽然tomcat同时能够处理的链接数目是maxConnections,但服务器中能够同时接收的链接数为maxConnections+acceptCount 。acceptCount的设置,与应用在链接太高状况下但愿作出什么反应有关系。若是设置过大,后面进入的请求等待时间会很长;若是设置太小,后面进入的请求立马返回connection refused。

3、线程池Executor

Executor元素表明Tomcat中的线程池,能够由其余组件共享使用;要使用该线程池,组件须要经过executor属性指定该线程池。

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

<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+线程编号
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="800" acceptCount="1000"/>

tomcate -->config --> server.xml

 

其中最后两个参数意义以下:

maxThreads:tomcat起动的最大线程数,即同时处理的任务个数,默认值为200

acceptCount:当tomcat起动的线程数达到最大时,接受排队的请求个数,默认值为100

 

这两个值如何起做用,请看下面三种状况

状况1:接受一个请求,此时tomcat起动的线程数没有到达maxThreads,tomcat会起动一个线程来处理此请求。

状况2:接受一个请求,此时tomcat起动的线程数已经到达maxThreads,tomcat会把此请求放入等待队列,等待空闲线程。

状况3:接受一个请求,此时tomcat起动的线程数已经到达maxThreads,等待队列中的请求个数也达到了acceptCount,此时tomcat会直接拒绝这次请求,返回connection refused

maxThreads如何配置

通常的服务器操做都包括量方面:1计算(主要消耗cpu),2等待(io、数据库等)

第一种极端状况,若是咱们的操做是纯粹的计算,那么系统响应时间的主要限制就是cpu的运算能力,此时maxThreads应该尽可能设的小,下降同一时间内争抢cpu的线程个数,能够提升计算效率,提升系统的总体处理能力。

第二种极端状况,若是咱们的操做纯粹是IO或者数据库,那么响应时间的主要限制就变为等待外部资源,此时maxThreads应该尽可能设的大,这样才能提升同时处理请求的个数,从而提升系统总体的处理能力。此状况下由于tomcat同时处理的请求量会比较大,因此须要关注一下tomcat的虚拟机内存设置和linux的open file限制。

我在测试时遇到一个问题,maxThreads我设置的比较大好比3000,当服务的线程数大到必定程度时,通常是2000出头,单次请求的响应时间就会急剧的增长,

百思不得其解这是为何,四处寻求答案无果,最后我总结的缘由多是cpu在线程切换时消耗的时间随着线程数量的增长愈来愈大,

cpu把大多数时间都用来在这2000多个线程直接切换上了,固然cpu就没有时间来处理咱们的程序了。

之前一直简单的认为多线程=高效率。。其实多线程自己并不能提升cpu效率,线程过多反而会下降cpu效率。

当cpu核心数<线程数时,cpu就须要在多个线程直接来回切换,以保证每一个线程都会得到cpu时间,即一般咱们说的并发执行。

因此maxThreads的配置绝对不是越大越好。

现实应用中,咱们的操做都会包含以上两种类型(计算、等待),因此maxThreads的配置并无一个最优值,必定要根据具体状况来配置。

最好的作法是:在不断测试的基础上,不断调整、优化,才能获得最合理的配置。

acceptCount的配置,我通常是设置的跟maxThreads同样大,这个值应该是主要根据应用的访问峰值与平均值来权衡配置的。

若是设的较小,能够保证接受的请求较快相应,可是超出的请求可能就直接被拒绝

若是设的较大,可能就会出现大量的请求超时的状况,由于咱们系统的处理能力是必定的。

相关文章
相关标签/搜索