性能调优 19. Tomcat 线程模型详解&性能调优
1. Tomcat I/O模型详解
1.1. Linux I/O模型详解
1.1.1. I/O要解决什么问题
I/O:在计算机内存与外部设备之间拷贝数据的过程。
程序通过CPU向外部设备发出读指令,数据从外部设备拷贝至内存需要一段时间,这段时间CPU就没事情做了,程序就会两种选择:
1. 让出CPU资源,让其干其它事情。
2. 继续让CPU不停地查询数据是否拷贝完成。
到底采取何种选择就是I/O模型需要解决的事情了。
以网络数据读取为例来分析,会涉及两个对象,一个是调用这个I/O操作的用户线程,另一个是操作系统内核。一个进程启动会在内存开辟地址空间分为用户空间和内核空间,基于安全上的考虑,用户程序只能访问用户空间,内核程序可以访问整个进程空间,只有内核可以直接访问各种硬件资源,比如磁盘和网卡。JAVA中用户线程要访问硬件,需要上下文切换到内核线程,JAVA程序通过本地方法调用内核内置的方法,来操作硬件。比如读磁盘数据,先借助内核程序将磁盘数据读到内核空间,在拷贝到用户空间。
当用户线程发起 I/O 调用后,网络数据读取操作会经历两个步骤:
数据准备阶段: 用户线程等待内核将数据从网卡拷贝到内核空间。
数据拷贝阶段: 内核将数据从内核空间拷贝到用户空间(应用进程的缓冲区,JAVA程序运行对应的堆)。
不同的I/O模型对于这2个步骤有着不同的实现步骤。
1.1.2. Linux的I/O模型分类
Linux 系统下的 I/O 模型有 5 种:
同步阻塞I/O(bloking I/O)jdk1.4前用的
同步非阻塞I/O(non-blocking I/O)
I/O多路复用(multiplexing I/O)
信号驱动式I/O(signal-driven I/O)
异步I/O(asynchronous I/O)
其中信号驱动式IO在实际中并不常用
下面图是说明这些IO模型是同步和非同步,堵塞和非堵塞的。
阻塞或非阻塞是指应用程序在发起 I/O 操作时,是立即返回还是等待。
同步或异步是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发。
注意
1. 信号驱动式I/O,JAVA层面还没有实现。
1.2. Tomcat支持的 I/O 模型
下面是Tomcat 支持的 I/O 模型,这些模型都是基于前面说到IO模型基础上创建的。
注意
1. Linux 内核没有很完善地支持异步 I/O 模型,因此 JVM 并没有采用原生的 Linux 异步 I/O,而是在应用层面通过epoll 模拟了异步 I/O 模型。因此在 Linux 平台上,Java NIO 和 Java NIO2 底层都是通过 epoll 来实现的,但是 Java NIO 更加简单高效。
2. APR是早期没有NIO。通过本地方法调用操作系统动态链接库来实现该效果,需要安装Apache的可移植库。
3. AIO支持长连接,在Windows下支持挺好,因为Windows实现了真正的异步I/O模型,Linux下支持不好因为Linux没有真正实现异步I/O模型,底层还是通过epoll实现。
1.3. 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 更加简单高效。
Tomcat指定IO模型只需修改server.xml的Connercort节点protocol属性。
1.4. 网络编程模型Reactor线程模型
1.4.1. 介绍
Reactor模式:堆模式或者反应器模式。
向反应器注册一个事件处理器,表示自己对某些事件感兴趣,时机来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应。这种控制逆转又称为“好莱坞法则”(不要调用我,让我来调用你)。
主要步骤
注册感兴趣的事件->扫描是否有感兴趣的事件发生->事件发生后做出相应的处理。
直接使用I/O模型,在代码逻辑处理效率上还是不够的。比如NIO下,启动服务器主线程,不断进行轮询,等待连接上来,连接上来后对连接通道读取数据,然后在做业务逻辑。可能存在没有请求数据,导致空轮询操作,都在一个线程执行,明显会影响下个轮询进行连接处理的操作。因此需要网络编程模型结合IO模型,设计使用。
Reactor的模型是网络服务器端用来处理高并发网络 IO 请求的一种编程模型。
该模型主要有三类处理事件:即连接事件、写事件、读事件。三个关键角色,acceptor负责连接事件,handler负责读写事件,reactor负责事件监听和事件分发。
1.4.2. 单 Reactor 单线程
由上图可以看出,单Reactor单线程模型中的 reactor、acceptor 和 handler以及后续业务处理逻辑的功能都是由一个线程来执行的。reactor 负责监听客户端事件和事件分发,一旦有连接事件发生,它会分发给 acceptor,由 acceptor 负责建立连接,然后创建一个 handler。如果是读写事件,reactor 将事件分发给 handler 进行处理。handler 负责读取客户端请求,进行业务处理,并最终给客户端返回结果。
早期的redis就使用这种模型。
1.4.3. 单 Reactor 多线程
该模型中,reactor、acceptor 和 handler 的功能由一个线程来执行,与此同时,会有一个线程池,由若干 worker 线程组成。在监听客户端事件、连接事件处理方面,这个类型和单 rector 单线程是相同的,但是不同之处在于,在单 reactor 多线程类型中,handler 只负责读取请求和写回结果,而具体的业务处理由 worker 线程来完成。
1.4.4. 主从 Reactor 多线程
在这个类型中,会有一个主 reactor 线程、多个子 reactor 线程和多个 worker 线程组成的一个线程池。其中,主 reactor 负责监听客户端事件,并在同一个线程中让 acceptor 处理连接事件。一旦连接建立后,主 reactor 会把连接分发给子 reactor 线程,由子 reactor 负责这个连接上的后续事件处理。那么,子reactor 会监听客户端连接上的后续事件,有读写事件发生时,它会让在同一个线程中的 handler 读取请求和返回结果,而和单 reactor 多线程类似,具体业务处理,它还是会让线程池中的 worker 线程处理。
1.5. Tomcat NIO的实现方式
在 Tomcat 中,EndPoint 组件的主要工作就是处理 I/O,在NIO下EndPoint对应的类是 NioEndpoint。其利用 Java NIO(同步非堵塞模型) API 实现了多路复用 I/O 模型。
NioEndpoint同时基于主从Reactor多线程网络编程模型(Tomcat9使用一主一从方式)下,结合多路复用 I/O 模型,解决IO效率慢问题。
1. LimitLatch:连接控制器,它负责控制最大连接数(在Tomcat代码中计数设置),NIO 模式下默认是 10000(tomcat9中是8192),当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减 1。
注意
到达最大连接数后操作系统底层还是会接收客户端连接(socket初始化时候设置),但用户层已经不再接收。
2. Acceptor:Acceptor跑在一个单独的线程里,它在一个死循环里调用 accept 方法来堵塞接收新连接,一旦有新的连接请求到来,accept 方法返回一个 Channel 对象,接着把 Channel 对象交给 Poller 去处理(这边运行Acceptor的线程在Rector中就是主线程)。
ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 SynchronizedQueue 里,这是个典型的生产者 - 消费者模式,Acceptor 与 Poller 线程之间通过 SynchronizedQueue 通信。
3. Poller:本质是一个 Selector,也跑在单独线程里。Poller 在内部维护一个 Channel 数组,它在一个死循环里不断检测 Channel 的数据就绪状态,一旦有 Channel 可读,就生成一个 SocketProcessor 任务对象扔给 Executor 去处理(这边运行Poller 的下半场就是从Rector线程,处理Socket读写逻辑的线程池的线程就是work线程)。
4. Executor:就是线程池,负责运行SocketProcessor 任务类,SocketProcessor 的 run 方法会调用 Http11Processor 来读取和解析请求数据。Http11Processor 是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel 写出。
NIOEndpoint的初始化,会创建,处理Sokcet连接的工作线程池(可以在对应的Connector下配置使用的线程池,该线程池在Service节点定义)。
主要处理流程
1. 一个连接上来,最大连接数LimitLatch加1。
2. Acceptor的主线程获取连接上来的通道SocketChannel 对象,封装在一个 PollerEvent 对象中,并将PollerEvent 对象压入 Poller 的 SynchronizedQueue 里Acceptor 与 Poller 线程之间通过 SynchronizedQueue 通信。
3. 通过PollerEvent对象,拿到通道注册读写事件到Poller线程的选择器上。
4. 读写事件就绪,就生成一个 SocketProcessor任务对象扔给Executor去处理。Http11Processor 来读取和解析请求数据。Http11Processor 是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel 写出。
1.7. Tomcat9 异步IO(NIO2)实现
NIO 和 NIO2 最大的区别是,一个是同步一个是异步。异步最大的特点是,应用程序不需要自己去触发数据从内核空间到用户空间的拷贝。
Nio2Endpoint 中没有 Poller 组件,也就是没有 Selector,不需要读写事件就绪后,用户去主动触发读写。在异步 I/O 模式下,Selector 的工作交给内核来做了。
主要流程
1. Nio2EndPoint启动会创建一个公共的Nio2Acceptor线程用来处理客户端连接。
2. 连接上来会调用Nio2Acceptor的completed方法处理连接,将**连接上来的Socket封装成SocektProcessor对象去执行。
3. 执行SocketProcessor对象时,持有的Http11Processor对象向Nio2SocketWrapper对象发出读写请求并注册回调函数,立即返回。
Nio2SocketWrapper构造方法中指定读写的回调对象readCompletionHandler,和writeCompletionHandler,读写就绪时候就调用这些对象的completed方法处理。Nio2SocketWrappe对象在回调函数里创建新的SocketProcessor对象继续处理,这次Http11Processor对象就能读写数据了。
2. Tomcat性能调优
2.1. Tomcat9参数配置
https://tomcat.apache.org/tomcat-9.0-doc/config/http.html
2.2. 如何监控Tomcat的性能
2.2.1. Tomcat 的关键指标
Tomcat 的关键指标有吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。
前三个指标是我们最关心的业务指标,Tomcat 作为服务器,就是要能够又快有好地处理请求,因此吞吐量要大、响应时间要短,并且错误数要少。
后面三个指标是跟系统资源有关的,当某个资源出现瓶颈就会影响前面的业务指标,比如线程池中的线程数量不足会影响吞吐量和响应时间;但是线程数太多会耗费大量 CPU,也会影响吞吐量;当内存不足时会触发频繁地 GC,耗费 CPU,最后也会反映到业务指标上来。
2.2.2. 通过 JConsole 监控 Tomcat
2.2.2.1. 准备
JConsole是一款基于JMX的可视化监控和管理工具
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"
注意这种方式只能通过脚本开启,因为脚本要加载这个配置文件。
重启 Tomcat,这样 JMX 的监听端口 8011 就开启了,接下来通过 JConsole 来连接这个端口
运行jconsole ip:端口,连接就行,如果成功连接上会显示正在连接并进入主界面。
jconsole 127.0.0.1:8011
我们可以看到 JConsole 的主界面:
2. 在IDEA下直接配置启动参数,直接启动源码开启也行
主要参数
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8011
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
我们可以看到 Jconsole 的主界面:
2. 在IDEA下直接配置启动参数,直接启动源码开启也行
主要参数
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8011
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
2.2.2.2. 吞吐量、响应时间、错误数
在 MBeans 标签页下选择 GlobalRequestProcessor,这里有 Tomcat 请求处理的统计信息。你会看到 Tomcat 中的各种连接器,展开“http-nio-8080”,你会看到这个连接器上的统计信息。
其中
maxTime 表示最长的响应时间,
processingTime 表示平均响应时间,
requestCount 表示吞吐量,
errorCount 就是错误数。
可以用Apache Jemeter进行压测。
2.2.2.3. 线程池
选择“线程”标签页,可以看到当前 Tomcat 进程中有多少线程,如下图所示:
图的左下方是线程列表,右边是线程的运行栈,这些都是非常有用的信息。如果大量线程阻塞,通过观察线程栈,能看到线程阻塞在哪个函数,有可能是 I/O 等待,或者是死锁。
在线程里头可以看到Acceptor和Poller线程,catalina开头就是处理socket连接的线程,可以自己指定后缀名称等。
2.2.2.4. CPU
在主界面可以找到 CPU 使用率指标,请注意这里的 CPU 使用率指的是 Tomcat 进程占用的 CPU,不是主机总的 CPU 使用率。
2.2.2.5. JVM 内存
选择“内存”标签页,你能看到 Tomcat 进程的 JVM 内存使用情况。
2.2.3. 命令行查看 Tomcat 指标
极端情况下如果 Web 应用占用过多 CPU 或者内存,又或者程序中发生了死锁,导致 Web 应用对外没有响应,监控系统上看不到数据,这个时候需要我们登陆到目标机器,通过命令行来查看各种指标。
Linux系统下
1. 首先通过 ps 命令找到 Tomcat 进程,拿到进程 ID。
ps -ef|grep tomcat
2. 接着查看进程状态的大致信息。
cat /proc/<pid>/status
3. 监控进程的 CPU 和内存资源使用情况。
top -p pid
4. 查看 Tomcat 的网络连接,比如 Tomcat 在 8080 端口上监听连接请求,通过下面的命令查看连接列表。
netstat -na | grep tomcat的端口
5. 可以分别统计处在“已连接”状态和“TIME_WAIT”状态的连接数
netstat -na | grep ESTAB | grep tomcat端口 | wc -l
6. 通过 ifstat 来查看网络流量,大致可以看出 Tomcat 当前的请求数和负载状况。
注意,这些命令同样适用于别的中间件调优。
2.3. 线程池的并发调优
2.3.1. 介绍
线程池调优指的是给 Tomcat 的线程池设置合适的参数,使得 Tomcat 能够又快又好地处理请求。
2.3.2. sever.xml中配置线程池
这里面最核心的就是如何确定 maxThreads 的值,如果这个参数设置小了,Tomcat 会发生线程饥饿,并且请求的处理会在队列中排队等待,导致响应时间变长;如果 maxThreads 参数值过大,同样也会有问题,因为服务器的 CPU 的核数有限,线程数太多会导致线程在 CPU 上来回切换,耗费大量的切换开销。
理论上我们可以通过公式 线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间
),计算出一个理想值,7这是IO密集型下线程池设置最大线程数的理论值,这个值只具有指导意义,因为它受到各种资源的限制,实际场景中,我们需要在理想值的基础上进行压测,来获得最佳线程数。
4. 还有种优化就是设置能同时处理上来的请求。Tomcat在应用层面,对请求连接同时,最大限制10000连接,Tomcat9改成8192在这逻辑判断处理前,ServerSocke还设置了堵塞队列,用来处理上来的连接,连接上来太多会放到堵塞队列里先等待。默认是100,
所以理论最大同时处理连接是堵塞队列等待数+Tomcat的能处理的连接数。比如Tomcat9下就是100+8192。
可以在server.xml配置。
maxConnections:与tomcat建立的最大socket连接数,默认8192。
acceptCount: 操作系统底层请求队列的最大长度 ,默认值为100。不建议改,因为需要修改操作系统的值,操作系统最大128,会根据这个值和配置参数两者之间取最小值。所以要生效,必须修改操作系统默认128的值到更大。
2.3.3. SpringBoot中调整Tomcat参数
方式1:Springboot配置内置Tomcat的线程池参数可以在 yml中配置 (对应属性配置类则是ServerProperties)。
方式2:SpringBoot中的TomcatConnectorCustomizer类可用于对内置Tomcat的Connector进行定制化修改。
原文地址:https://blog.csdn.net/xingwanganfang/article/details/143097265
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!