Tomcat 线程模型&性能调优

Linux I/O模型详解

I/O要解决什么问题

I/O:在计算机内存与外部设备之间拷贝数据的过程。

程序通过CPU向外部设备发出读指令,数据从外部设备拷贝至内存需要一段时间,这段时间CPU就没事情做了,程序就会两种选择:

  1. 让出CPU资源,让其干其他事情

  2. 继续让CPU不停地查询数据是否拷贝完成

到底采取何种选择就是I/O模型需要解决的事情了。

以网络数据读取为例来分析,会涉及两个对象,一个是调用这个I/O操作的用户线程,另一个是操作系统内核。一个进程的地址空间分为用户空间和内核空间,基于安全上的考虑,用户程序只能访问用户空间,内核程序可以访问整个进程空间,只有内核可以直接访问各种硬件资源,比如磁盘和网卡。

Tomcat 线程模型&性能调优_第1张图片

当用户线程发起 I/O 调用后,网络数据读取操作会经历两个步骤:

  • 数据准备阶段: 用户线程等待内核将数据从网卡拷贝到内核空间。
  • 数据拷贝阶段: 内核将数据从内核空间拷贝到用户空间(应用进程的缓冲区)。

Linux的I/O模型分类

Linux 系统下的 I/O 模型有 5 种:

  1. 同步阻塞I/O(bloking I/O)
  2. 同步非阻塞I/O(non-blocking I/O)
  3. I/O多路复用(multiplexing I/O)
  4. 信号驱动式I/O(signal-driven I/O)
  5. 异步I/O(asynchronous I/O)

其中信号驱动式IO在实际中并不常用

阻塞或非阻塞是指应用程序在发起 I/O 操作时,是立即返回还是等待。

同步或异步是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发


  1. BIO,一个请求来会阻塞,需要使用线程池来管理使得每个线程监听一个客户端,否则服务端一次只能控制一个客户端
  2. NIO,普通版nio建立连接以后不会阻塞,而是把所有连接放在一个数组,每次都需要自行遍历所有数组来查找是否有新消息输入,效率低
  3. 带有多路复用的NIO,多了一个selector,selector会监听所有的通道,不需要像前两种一样while(true)来轮训
  4. AIO,类似于多线程的异步调用,需要使用回调函数

Tomcat I/O 模型如何选型

I/O 调优实际上是连接器类型的选择,一般情况下默认都是 NIO,在绝大多数情况下都是够用的,除非你的 Web 应用用到了 TLS 加密传输,而且对性能要求极高,这个时候可以考虑 APR,因为 APR通过 OpenSSL 来处理 TLS 握手和加密 / 解密。OpenSSL 本身用 C 语言实现,它还对 TLS 通信做了优化,所以性能比 Java 要高。如果你的 Tomcat 跑在 Windows 平台上,并且 HTTP 请求的数据量比 较大,可以考虑 NIO2,这是因为 Windows 从操作系统层面实现了真正意义上的异步 I/O,如果传输的数据量比较大,异步 I/O 的效果就能显现出来。如果你的 Tomcat 跑在 Linux 平台上,建议使用NIO。因为在 Linux 平台上,Java NIO 和 Java NIO2 底层都是通过 epoll 来实现的,但是 Java NIO更加简单高效。

指定IO模型只需修改protocol配置

 

<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" 

connectionTimeout="20000" 

redirectPort="8443" /> 

网络编程模型Reactor线程模型

Reactor 模型是网络服务器端用来处理高并发网络 IO 请求的一种编程模型。

该模型主要有三类处理事件:即连接事件、写事件、读事件;三个关键角色:即 reactor、acceptor、 handler。acceptor负责连接事件,handler负责读写事件,reactor负责事件监听和事件分发。

单 Reactor 单线程

Tomcat 线程模型&性能调优_第2张图片

由上图可以看出,单Reactor单线程模型中的 reactor、acceptor 和 handler以及后续业务处理逻辑的

功能都是由一个线程来执行的。reactor 负责监听客户端事件和事件分发,一旦有连接事件发生,它会

分发给 acceptor,由 acceptor 负责建立连接,然后创建一个 handler。如果是读写事件,reactor 将

事件分发给 handler 进行处理。handler 负责读取客户端请求,进行业务处理,并最终给客户端返回

结果。


单 Reactor 多线程

Tomcat 线程模型&性能调优_第3张图片

该模型中,reactor、acceptor 和 handler 的功能由一个线程来执行,与此同时,会有一个线程池,

由若干 worker 线程组成。在监听客户端事件、连接事件处理方面,这个类型和单 rector 单线程是相

同的,但是不同之处在于,在单 reactor 多线程类型中,handler 只负责读取请求和写回结果,而具

体的业务处理由 worker 线程来完成。

主从 Reactor 多线程

Tomcat 线程模型&性能调优_第4张图片

在这个类型中,会有一个主 reactor 线程、多个子 reactor 线程和多个 worker 线程组成的一个线程

池。其中,主 reactor 负责监听客户端事件,并在同一个线程中让 acceptor 处理连接事件。一旦连接

建立后,主 reactor 会把连接分发给子 reactor 线程,由子 reactor 负责这个连接上的后续事件处

理。那么,子 reactor 会监听客户端连接上的后续事件,有读写事件发生时,它会让在同一个线程中

的 handler 读取请求和返回结果,而和单 reactor 多线程类似,具体业务处理,它还是会让线程池中

的 worker 线程处理。

Tomcat NIO实现

**Tomcat NIO实现

在 Tomcat 中,EndPoint 组件的主要工作就是处理 I/O,而 NioEndpoint 利用 Java NIO API 实现了

多路复用 I/O 模型。Tomcat的NioEndpoint 是基于主从Reactor多线程模型设计的

Tomcat 线程模型&性能调优_第5张图片

  • LimitLatch 是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000(tomcat9中8192),当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减 1。注意到达最大连接数后操作系统底层还是会接收客户端连接,但用户层已经不再接收。
  • Acceptor 跑在一个单独的线程里,它在一个死循环里调用 accept 方法来接收新连接,一旦有新的连接请求到来,accept 方法返回一个 Channel 对象,接着把 Channel 对象交给 Poller 去处理。
  • Poller 的本质是一个 Selector,也跑在单独线程里。Poller 在内部维护一个 Channel 数组,它在一个死循环里不断检测 Channel 的数据就绪状态,一旦有 Channel 可读,就生成一个 SocketProcessor 任务对象扔给Executor 去处理。
  • Executor 就是线程池,负责运行 SocketProcessor 任务类,SocketProcessor 的 run 方法会调Http11Processor 来读取和解析请求数据。Http11Processor 是应用层协议的封装,它会调用容器获得响应,再把响应通过Channel 写出。

Tomcat 异步IO实现

NIO 和 NIO2 最大的区别是,一个是同步一个是异步。异步最大的特点是,应用程序不需要自己去触发数据从内核空间到用户空间的拷贝。

Tomcat 线程模型&性能调优_第6张图片

Nio2Endpoint 中没有 Poller 组件,也就是没有 Selector。在异步 I/O 模式下,Selector 的工作交给内核来做了。

Tomcat性能调优

Tomcat 的关键指标

Tomcat 的关键指标有吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。前三个指标是

我们最关心的业务指标,Tomcat 作为服务器,就是要能够又快有好地处理请求,因此吞吐量要大、响

应时间要短,并且错误数要少。后面三个指标是跟系统资源有关的,当某个资源出现瓶颈就会影响前

面的业务指标,比如线程池中的线程数量不足会影响吞吐量和响应时间;但是线程数太多会耗费大量

CPU,也会影响吞吐量;当内存不足时会触发频繁地 GC,耗费 CPU,最后也会反映到业务指标上

来。


通过 JConsole 监控 Tomcat

  1. 开启 JMX 的远程监听端口

我们可以在 Tomcat 的 bin 目录下新建一个名为setenv.sh的文件(或者setenv.bat,根据你的操作系统类型),然后输入下面的内容:

export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.port=8011"
export JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=x.x.x.x"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.ssl=false"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.authenticate=false"

2)重启 Tomcat,这样 JMX 的监听端口 8011 就开启了,接下来通过 JConsole 来连接这个端口。

jconsole x.x.x.x:8011

3)我们可以看到 JConsole 的主界面:

Tomcat 线程模型&性能调优_第7张图片

线程池的并发调优

线程池调优指的是给 Tomcat 的线程池设置合适的参数,使得 Tomcat 能够又快又好地处理请求。

sever.xml中配置线程池

1 
10 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-Fox"
11 prestartminSpareThreads="true"
12 maxThreads="500" minSpareThreads="10" maxIdleTime="10000"/>
13 
14 <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"
15 connectionTimeout="20000"
16 redirectPort="8443" URIEncoding="UTF-8"/>

这里面最核心的就是如何确定 maxThreads 的值,如果这个参数设置小了,Tomcat 会发生线程饥

饿,并且请求的处理会在队列中排队等待,导致响应时间变长;如果 maxThreads 参数值过大,同样

也会有问题,因为服务器的 CPU 的核数有限,线程数太多会导致线程在 CPU 上来回切换,耗费大量

的切换开销。

理论上我们可以通过公式 线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间),计算出一

个理想值,这个值只具有指导意义,因为它受到各种资源的限制,实际场景中,我们需要在理想值的

基础上进行压测,来获得最佳线程数。

SpringBoot中调整Tomcat参数

yml中配置 (属性配置类:ServerProperties)

server:
 tomcat:
  threads:
   min-spare: 20
   max: 500
  connection-timeout: 5000ms

你可能感兴趣的:(tomcat学习,tomcat,java)