原作者 https://smartan123.github.io/book/?file=001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E9%9D%A2%E8%AF%95%E9%A2%98%E9%9B%86%E9%94%A6#%E4%B8%80%E3%80%81tomcat%E6%9C%89%E5%93%AA%E4%BA%9B%E9%85%8D%E7%BD%AE%E9%A1%B9%E5%8F%AF%E4%BB%A5%E4%BC%98%E5%8C%96%EF%BC%9F
怕丢失做cv拷贝整理
一、tomcat有哪些配置项可以优化?
1、server.xml文件中禁用ajp协议(新版中默认是屏蔽的),减少不必要的线程开销
<!--<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />-->
2、server.xml文件修改元素,使用线程池提高性能
<!‐‐将注释打开(注释没打开的情况下默认10个线程,最小10,最大200)‐‐>
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐"
maxThreads="500" minSpareThreads="50"
prestartminSpareThreads="true" maxQueueSize="100"/>
<!‐‐
参数说明:
maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 1000,根据硬件设施和业
务来判断
minSpareThreads:Tomcat 初始化时创建的线程数,默认设置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化 minSpareThreads 的
参数值,如果不等于 true,minSpareThreads 的值就没啥效果了
maxQueueSize,最大的等待队列数,超过则拒绝请求
‐‐>
<!‐‐在Connector中设置executor属性指向上面的执行器‐‐>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
3、server.xml文件中修改连接器,可以使用NIO2通道提高性能
<Connector executor="tomcatThreadPool" port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" />
二、tomcat堆栈中有哪些常见线程?分别有什么用途?
1、main线程
main线程是tomcat的主要线程,其主要作用是通过启动包来对容器进行点火:
main线程一路启动了Catalina,StandardServer[8005],StandardService[Catalina],StandardEngine[Catalina]
engine内部组件都是异步启动,engine这层才开始继承ContainerBase,engine会调用父类的startInternal()方法,里面由startStopExecutor线程提交FutureTask任务,异步启动子组件StandardHost,
StandardEngine[Catalina].StandardHost[localhost]
main->Catalina->StandardServer->StandardService->StandardEngine->StandardHost,黑体开始都是异步启动。
->启动Connector
main的作用就是把容器组件拉起来,然后阻塞在8005端口,等待关闭。
2、localhost-startStop线程
Tomcat容器被点火起来后,并不是傻傻的按照次序一步一步的启动,而是在engine组件中开始用该线程提交任务,按照层级进行异步启动,对于每一层级的组件都是采用startStop线程进行启动,我们观察一下idea中的线程堆栈就可以发现:启动异步,部署也是异步
这个startstop线程实际代码调用就是采用的JDK自带线程池来做的,启动位置就是ContainerBase的组件父类的startInternal():
这个startstop线程实际代码调用就是采用的JDK自带线程池来做的,启动位置就是ContainerBase的组件父类的startInternal():
因为从Engine开始往下的容器组件都是继承这个ContainerBase,所以相当于每一个组件启动的时候,除了对自身的状态进行设置,都会启动startChild线程启动自己的孩子组件。
而这个线程仅仅就是在启动时,当组件启动完成后,那么该线程就退出了,生命周期仅仅限于此。
3、AsyncFileHandlerWriter线程
顾名思义,该线程是用于异步文件处理的,它的作用是在Tomcat级别构架出一个输出框架,然后不同的日志系统都可以对接这个框架,因为日志对于服务器来说,是非常重要的功能。
如下,就是juli的配置:
该线程主要的作用是通过一个LinkedBlockingDeque来与log系统对接,该线程启动的时候就有了,全生命周期。
4、ContainerBackgroundProcessor线程
Tomcat在启动之后,不能说是死水一潭,很多时候可能会对Tomcat后端的容器组件做一些变化,例如部署一个应用,相当于你就需要在对应的Standardhost加上一个StandardContext,也有可能在热部署开关开启的时候,对资源进行增删等操作,这样应用可能会重新reload。
也有可能在生产模式下,对class进行重新替换等等,这个时候就需要在Tomcat级别中有一个线程能实时扫描Tomcat容器的变化,这个就是ContainerbackgroundProcessor线程了:
(本地源码StandardContext类的5212行启动)
我们可以看到这个代码,也就是在ContainerBase中:
这个线程是一个递归调用,也就是说,每一个容器组件其实都有一个backgroundProcessor,而整个Tomcat就点起一个线程开启扫描,扫完儿子,再扫孙子(实际上来说,主要还是用于StandardContext这一级,可以看到StandardContext这一级:
我们可以看到,每一次backgroundProcessor,都会对该应用进行一次全方位的扫描,这个时候,当你开启了热部署的开关,一旦class和资源发生变化,立刻就会reload。
tomcat9中已经被Catalina-Utility线程替代。
5、acceptor线程
Connector(实际是在AbstractProtocol类中)初始化和启动之时,启动了Endpoint,Endpoint就会启动poller线程和Acceptor线程。Acceptor底层就是ServerSocket.accept()。返回Socket之后丢给NioChannel处理,之后通道和poller线程绑定。
acceptor->poller->exec
无论是NIO还是BIO通道,都会有Acceptor线程,该线程就是进行socket接收的,它不会继续处理,如果是NIO的,无论是新接收的包还是继续发送的包,直接就会交给Poller,而BIO模式,Acceptor线程直接把活就给工作线程了:
如果不配置,Acceptor线程默认开始就开启1个,后期再随着压力增大而增长:
上述启动代码在AbstractNioEndpoint的startAcceptorThreads方法中。
6、ClientPoller线程
NIO和APR模式下的Tomcat前端,都会有Poller线程:
对于Poller线程实际就是继续接着Acceptor进行处理,展开Selector,然后遍历key,将后续的任务转交给工作线程(exec线程),起到的是一个缓冲,转接,和NIO事件遍历的作用,具体代码体现如下(NioEndpoint类):
上述的代码在NioEndpoint的startInternal中,默认开始开启2个Poller线程,后期再随着压力增大增长,可以在Connector中进行配置。
7、exe线程(默认10个)
也就是SocketProcessor线程,我们可以看到,上述几个线程都是定义在NioEndpoint内部线程类。NIO模式下,Poller线程将解析好的socket交给SocketProcessor处理,它主要是http协议分析,攒出Response和Request,然后调用Tomcat后端的容器:
该线程的重要性不言而喻,Tomcat主要的时间都耗在这个线程上,所以我们可以看到Tomcat里面有很多的优化,配置,都是基于这个线程的,尽可能让这个线程减少阻塞,减少线程切换,甚至少创建,多利用。
下面就是NIO模式下创建的工作线程:
实际上也是JDK的线程池,只不过基于Tomcat的不同环境参数,对JDK线程池进行了定制化而已,本质上还是JDK的线程池。
8、NioBlockingSelector.BlockPoller(默认2个)
Nio方式的Servlet阻塞输入输出检测线程。实际就是在Endpoint初始化的时候启动selectorPool,selectorPool再启动selector,selector内部启动BlokerPoller线程。
该线程在前面的NioBlockingPool中讲得很清楚了,其NIO通道的Servlet输入和输出最终都是通过NioBlockingPool来完成的,而NioBlockingPool又根据Tomcat的场景可以分成阻塞或者是非阻塞的,对于阻塞来讲,为了等待网络发出,需要启动一个线程实时监测网络socketChannel是否可以发出包,而如果不这么做的话,就需要使用一个while空转,这样会让工作线程一直损耗。
只要是阻塞模式,并且在Tomcat启动的时候,添加了—D参数 org.apache.tomcat.util.net.NioSelectorShared 的话,那么就会启动这个线程。
大体上启动顺序如下:
//bind方法在初始化就完成了
Endpoint.bind(){
//selector池子启动
selectorPool.open(){
//池子里面selector再启动
blockingSelector.open(getSharedSelector()){
//重点这句
poller = new BlockPoller();
poller.selector = sharedSelector;
poller.setDaemon(true);
poller.setName("NioBlockingSelector.BlockPoller-"+ (threadCounter.getAndIncrement()));
//这里启动
poller.start();
}
}
}
9、AsyncTimeout线程
该线程为tomcat7及之后的版本才出现的,注释其实很清楚,该线程就是检测异步request请求时,触发超时,并将该请求再转发到工作线程池处理(也就是Endpoint处理)。
AsyncTimeout线程也是定义在AbstractProtocol内部的,在start()中启动。AbstractProtocol是个极其重要的类,他持有Endpoint和ConnectionHandler**这两个tomcat前端非常重要的类
10、其他线程(例如ajp相关线程)
ajp工作线程处理的是ajp协议的相关请求,这个请求主要是用于http apache服务器和tomcat之间的数据交换,该数据交换用的就是ajp协议,和exec工作线程差不多,默认也是启动10个,端口号是8009。优化时如果没有用到http apache的话就可以把这个协议关掉。
Tomcat本身还有很多其它的线程,远远不止这些,例如如果开启了sendfile,那么对sendfile就是开启一个线程来进行操作,这种功能的线程开启还有很多。
Tomcat作为一款优秀的服务器,不可能就只有1个线程,而是多个线程之间相互配合完成功能,而且很多功能尽量异步处理,尽可能的减少线程切换。所以线程并不是越多越好,因此线程的控制也尤为关键。
三、Tomcat 的 bio 模式改为 nio 模式,是否能提高服务器的吞吐量?为什么在配置一样的情况下,两种模式压出来的吞吐量差不多?
这种情况主要就是要看是不是整个系统都异步化了,因为tomcat的nio只是将网络io异步化了,就是接收和读写异步化了,但是网络报文接受完后还是要交给业务线程池,如果你的业务是阻塞的或者较耗时的话是没办法提升整个系统的吞吐量的,除非将整个项目都异步化,现在压测cpu如果还没有打满的话就可以继续优化,但如果bio都能打满cpu就说明已经到物理极限了,只能在代码层去优化了。
四、对比nio,bio一开始能接收的量比nio大,什么原因?
bio接收请求是线程池里面的线程接收的,也就是说你的线程池如果设为600,就有600个线程能接收,自然就会满打满算,但是nio是只有cpu数个线程负责接收的(默认10个)。
五、nio的优势是什么?是不是 nio 模式下 tomcat 默认能保持10000条连接,而bio模式则达不到?
简单地说,nio模式最大化压榨了CPU,把时间片更好利用起来。通俗地说,bio hold住连接不干活也占用线程,nio hold住连接不干活也没关系,让需要处理的连接先执行就行了。
六、nio模式是不是更适合做tcp长连接,用少量线程hold住大量的连接,节省资源?但tomcat现在都是短连接,nio抗并发并没有比bio强吗?
nio适合大量长连接,而且大部分是只hold住但不处理的场景,如果你能将项目异步化的话nio肯定比bio扛得连接多。bio模式其实压测时是打不满CPU的,所以采用nio来压榨CPU,如果bio都能打满CPU,那就没必要设计nio 和异步化了,因为已经达到物理极限了,没有办法继续压榨了,只能去优化代码。
七、bio模式下将最大线程数不断调大,直到打满CPU,这种情况和nio异步比较,更倾向于哪一种?
bio模式达到峰值后会导致接收不了连接,操作系统层的连接队列满了则会拒绝连接。另外一个是,系统不可能开很多线程,bio开太多线程可能会直接卡死,线程切换花销很大,主要是要将阻塞的环节异步出来,这样线程就能高效干活了。nio模式还是比bio高效很多,因为bio模式光网络读写就可能阻塞很长时间了,而nio负责网络io的异步化,而其他步骤的异步化要自己另外考虑。
八、Tomcat中的NIO2通道是如何保证高性能的?
nio2通道是基于java AIO,采用的是proactor模式,是纯异步模式,这比NIO基于reacactor模式效率要高。所有的操作都是由操作系统回调异步完成。
九、研究过tomcat的NioEndpoint源码吗?请阐述下Reactor多线程模型在tomcat中的实现。
tomcat的底层网络NIO通信基于主从Reactor多线程模型。
它有三大线程组分别用于处理不同的逻辑:
Acceptor线程:等待和接收客户端连接。在接收到连接后,创建SocketChannel并将其注册到poller线程。 poller线程:将SocketChannel放到selector上注册读事件,轮询selector,获取就绪的SelectionKey,并将就绪的SelectionKey(或SocketChannel)委托给工作线程。 工作线程:执行真正的业务逻辑。 备注:Acceptor线程和poller线程之间有一个SocketChannel队列,Acceptor线程负责将SocketChannel推送到队列,poller线程负责从队列取出SocketChannel。poller线程从队列取出SocketChannel后,紧接着会把它放到selector上注册读事件。
主从Reactor多线程模型 主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
它的线程模型如下图所示:
工作流程总结如下
从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接; Acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作; 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理I/O的读写操作。