虽然现在springboot微服务纵横都是用的jar包,但是还有很多使用的。tomcat是servlet的容器,也是springboot默认集成的容器,有必要对他的网络线程模型做一下了解。
(一) tomcat网络处理线程模型
- ① BIO同步Servlet
一个请求,一个工作线程,CPU利用率低,tomcat7以下才使用这种,新版本不再使用,tomcat8默认NIO
- ② APR 异步Servlet
apr( Portable Runtime/Apache可以执行运行库),Apache Http服务器的支持库。JINI的行还是调用Apache Http服务器的核心动态链接库来处理文件读取或者网络传输操作,tomcat 默认监听指定路径,如果有apr安全,则自动启用。
- ③ NIO异步Servlet
tomcat8开始,默认NIO方式,非阻塞读取请求信息,非堵塞处理下一个请求,完全异步。
- ④ NIO处理流程
- Acceptor接收器接受套接字。
- 接收器从缓存中检索nioChannel对象。
- Pollerthread将nioChannel注册到它的选择器IO事件中。
- 轮询器将nioChannel分配给一个work线程来处理请求。
- SocketProcessor完成对请求的处理和返回客户端。
- ⑤ 参数调优
不能靠经验猜测,需要不断调试,找出适应应用程序的合理配置。
- ConnectionTime ,默认 20s,适当调整减少时间。
- maxThreads处理连接的最大线程数,默认200,建议增大,但是不是越大越好的。
- acceptCount(backlog)等待接收accept的请求数量限制,默认100,建议增大,socket参数 min(accept,、proc/sys/net/core/omaxconn),如果acceptCount设置是100.操作系统设置的10,就按照10来,请求限制就是10。
- maxConnections最大连接处理器,默认 nio 1w,apr 8192, 建议不要调整。
- ⑥ 连接数调整
tomcat能够接受到的连接数,acceptCount和connections,一个用户请求连接到accept queue队列,代表捂手成功,通过tcp的形式,收到一个通知给tomcat。 tomcat收到请求数量是根据1万,这1万个请求正在处理,线程,如果tomcat已经满了,请求都堆积到操作系统里面,操作系统acceptCount就是控制堆积数量的。这块,操作系统堆满了,tcp这块也堆满了直接关闭了请求了。
这块,不仅仅有队列,还有个tcp握手过程中的一个syn queue,linux也会在syn queue,这属于系统内核。
总共连接数 = acceptCount+ connections
connections: Tomcat能接收的请求限制。
acceptCount: 超过Tomcat能接收的请求数以后,堆积在操作系统的数量(windows 和 linux 略有不同)。
什么时候需要调整connections?如何调整?
connections小于maxThread的时候,需要调大,最好是比预期的最高并发数要大20%;反 正是堆积到tomcat的work处理线程池中(堆积占)。举个简单的例子:cpu数量是5 maxThread是5,结果连接数据connections只有3,这不是浪费的了吗?直接调整connections的数量。
什么时候需要调整acceptCount?
想受理更多用户请求,却又不想堆积在tomcat中,利用操作系统来高效的堆积,可以调整为 最高并发数 connections; 实际上不需要调整,tomcat默认100,linux默认128;最好是把连接控制交给应用程序,这 样方便管理。这是操作系统层面的,调整的比较少。一般都是调整connections。
SpringBoot的参数配置
-jar webdemo1.1.0.jar --server.tomcat.maxconnections=1 -- server.tomcat.acceptCount=1
- ⑦ 线程数调整
并发处理线程数调整
线程太少,CPU利用率过低,程序的吞吐量变小,资源浪费,容易堆积。
线程太多,上下文频繁切换,性能反而变低。
线程数调为多少合适?
场景代入:服务器配置2核,不考虑内存问题。
收到请求,java代码执行耗时50ms,等待数 据返回50ms。
理想的线程数量= (1 + 代码阻塞时间/代码执行时间) * cpu数量 。
实际情况是跑起代码,压测环境进行调试。不断调整线程数,将CPU打到80~90%的利用率。
SpringBoot的参数配置
java -jar web-demo-1.1.0.jar --server.tomcat.maxThreads=500
- ⑧ 场景描述
有一家足疗店,只有两个足浴的位置。
假设一个足浴技师,接待一个客人需要30分钟,接待一个客人后,休息30分钟。
请问:这家需要几个技师?4个技师
一个线程在一个cpu里面执行,请求执行需要50毫秒,休息50毫秒,理想的线程数量就是
cpu的数量 * (1+ 50ms/50ms),1个cpu就是2个线程。
(二)示例
- ① 代码
WebDemolication
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Random; import java.util.concurrent.Callable; @SpringBootApplication @RestController @EnableAsync public class WebDemoApplication { public static void main(String[] args) { SpringApplication.run(WebDemoApplication.class, args); } // 这个方法固定延时3秒,用于测试线程/连接数量控制 @RequestMapping("/testCount") public String testCount() throws InterruptedException { Thread.sleep(3000);// connections acceptCount return "Success"; } @RequestMapping("/test") public String benchmark() throws InterruptedException { System.out.println("访问test:" + Thread.currentThread().getName()); // 这段代码,一直运算。 for (int i = 0; i < 200000; i++) { new Random().nextInt(); } // 50毫秒的数据库等待,线程不干活 Thread.sleep(50L); return "Success"; } // 异步支持 @RequestMapping("/testAsync") public Callable<String> benchmarkAsync() throws InterruptedException { return new Callable<String>() { @Override public String call() throws Exception { System.out.println("访问testAsync:" + Thread.currentThread().getName()); // 这段代码,一直运算。 for (int i = 0; i < 200000; i++) { new Random().nextInt(); } // 50毫秒的数据库等待,线程不干活 Thread.sleep(50L); return "Success"; } }; } }
application.yml
server: port: 8080
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.study.chapter-3</groupId> <artifactId>web-demo</artifactId> <version>1.1.0</version> <name>web-demo</name> <description>Tomcat调优代码</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- ② 下载jmeter
https://mirrors.tuna.tsinghua.edu.cn/apache//jmeter/binaries/
jmeter的测试脚本,使用的时候保存成jmx,使用的时候加载这个jmx
<?xml version="1.0" encoding="UTF-8"?> <jmeterTestPlan version="1.2" properties="5.0" jmeter="5.0 r1840935"> <hashTree> <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划" enabled="true"> <stringProp name="TestPlan.comments"></stringProp> <boolProp name="TestPlan.functional_mode">false</boolProp> <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp> <boolProp name="TestPlan.serialize_threadgroups">false</boolProp> <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true"> <collectionProp name="Arguments.arguments"/> </elementProp> <stringProp name="TestPlan.user_define_classpath"></stringProp> </TestPlan> <hashTree> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="数量测试" enabled="true"> <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true"> <boolProp name="LoopController.continue_forever">false</boolProp> <stringProp name="LoopController.loops">1</stringProp> </elementProp> <stringProp name="ThreadGroup.num_threads">10</stringProp> <stringProp name="ThreadGroup.ramp_time">1</stringProp> <boolProp name="ThreadGroup.scheduler">false</boolProp> <stringProp name="ThreadGroup.duration"></stringProp> <stringProp name="ThreadGroup.delay"></stringProp> </ThreadGroup> <hashTree> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true"> <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true"> <collectionProp name="Arguments.arguments"/> </elementProp> <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp> <stringProp name="HTTPSampler.port">8080</stringProp> <stringProp name="HTTPSampler.protocol">http</stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.path">/testCount</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.use_keepalive">true</boolProp> <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> <stringProp name="HTTPSampler.embedded_url_re"></stringProp> <stringProp name="HTTPSampler.connect_timeout"></stringProp> <stringProp name="HTTPSampler.response_timeout"></stringProp> </HTTPSamplerProxy> <hashTree/> <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="察看结果树" enabled="true"> <boolProp name="ResultCollector.error_logging">false</boolProp> <objProp> <name>saveConfig</name> <value class="SampleSaveConfiguration"> <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> <code>true</code> <message>true</message> <threadName>true</threadName> <dataType>true</dataType> <encoding>false</encoding> <assertions>true</assertions> <subresults>true</subresults> <responseData>false</responseData> <samplerData>false</samplerData> <xml>false</xml> <fieldNames>true</fieldNames> <responseHeaders>false</responseHeaders> <requestHeaders>false</requestHeaders> <responseDataOnError>false</responseDataOnError> <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> <assertionsResultsToSave>0</assertionsResultsToSave> <bytes>true</bytes> <sentBytes>true</sentBytes> <url>true</url> <threadCounts>true</threadCounts> <idleTime>true</idleTime> <connectTime>true</connectTime> </value> </objProp> <stringProp name="filename"></stringProp> </ResultCollector> <hashTree/> </hashTree> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="参数调优测试用例" enabled="true"> <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true"> <boolProp name="LoopController.continue_forever">false</boolProp> <stringProp name="LoopController.loops">10</stringProp> </elementProp> <stringProp name="ThreadGroup.num_threads">1000</stringProp> <stringProp name="ThreadGroup.ramp_time">1</stringProp> <boolProp name="ThreadGroup.scheduler">false</boolProp> <stringProp name="ThreadGroup.duration"></stringProp> <stringProp name="ThreadGroup.delay"></stringProp> </ThreadGroup> <hashTree> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true"> <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true"> <collectionProp name="Arguments.arguments"/> </elementProp> <stringProp name="HTTPSampler.domain">192.168.100.241</stringProp> <stringProp name="HTTPSampler.port">8080</stringProp> <stringProp name="HTTPSampler.protocol">http</stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.path">test</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.use_keepalive">true</boolProp> <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> <stringProp name="HTTPSampler.embedded_url_re"></stringProp> <stringProp name="HTTPSampler.connect_timeout"></stringProp> <stringProp name="HTTPSampler.response_timeout"></stringProp> </HTTPSamplerProxy> <hashTree/> <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true"> <boolProp name="ResultCollector.error_logging">false</boolProp> <objProp> <name>saveConfig</name> <value class="SampleSaveConfiguration"> <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> <code>true</code> <message>true</message> <threadName>true</threadName> <dataType>true</dataType> <encoding>false</encoding> <assertions>true</assertions> <subresults>true</subresults> <responseData>false</responseData> <samplerData>false</samplerData> <xml>false</xml> <fieldNames>true</fieldNames> <responseHeaders>false</responseHeaders> <requestHeaders>false</requestHeaders> <responseDataOnError>false</responseDataOnError> <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> <assertionsResultsToSave>0</assertionsResultsToSave> <bytes>true</bytes> <sentBytes>true</sentBytes> <url>true</url> <threadCounts>true</threadCounts> <idleTime>true</idleTime> <connectTime>true</connectTime> </value> </objProp> <stringProp name="filename"></stringProp> </ResultCollector> <hashTree/> <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="察看结果树" enabled="true"> <boolProp name="ResultCollector.error_logging">false</boolProp> <objProp> <name>saveConfig</name> <value class="SampleSaveConfiguration"> <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> <code>true</code> <message>true</message> <threadName>true</threadName> <dataType>true</dataType> <encoding>false</encoding> <assertions>true</assertions> <subresults>true</subresults> <responseData>false</responseData> <samplerData>false</samplerData> <xml>false</xml> <fieldNames>true</fieldNames> <responseHeaders>false</responseHeaders> <requestHeaders>false</requestHeaders> <responseDataOnError>false</responseDataOnError> <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> <assertionsResultsToSave>0</assertionsResultsToSave> <bytes>true</bytes> <sentBytes>true</sentBytes> <url>true</url> <threadCounts>true</threadCounts> <idleTime>true</idleTime> <connectTime>true</connectTime> </value> </objProp> <stringProp name="filename"></stringProp> </ResultCollector> <hashTree/> </hashTree> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="异步Servlet参数调优测试用例" enabled="true"> <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true"> <boolProp name="LoopController.continue_forever">false</boolProp> <stringProp name="LoopController.loops">1</stringProp> </elementProp> <stringProp name="ThreadGroup.num_threads">1000</stringProp> <stringProp name="ThreadGroup.ramp_time">2</stringProp> <boolProp name="ThreadGroup.scheduler">false</boolProp> <stringProp name="ThreadGroup.duration"></stringProp> <stringProp name="ThreadGroup.delay"></stringProp> </ThreadGroup> <hashTree> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true"> <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true"> <collectionProp name="Arguments.arguments"/> </elementProp> <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp> <stringProp name="HTTPSampler.port">8080</stringProp> <stringProp name="HTTPSampler.protocol">http</stringProp> <stringProp name="HTTPSampler.contentEncoding"></stringProp> <stringProp name="HTTPSampler.path">testAsync</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.auto_redirects">false</boolProp> <boolProp name="HTTPSampler.use_keepalive">true</boolProp> <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> <stringProp name="HTTPSampler.embedded_url_re"></stringProp> <stringProp name="HTTPSampler.connect_timeout"></stringProp> <stringProp name="HTTPSampler.response_timeout"></stringProp> </HTTPSamplerProxy> <hashTree/> <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true"> <boolProp name="ResultCollector.error_logging">false</boolProp> <objProp> <name>saveConfig</name> <value class="SampleSaveConfiguration"> <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> <code>true</code> <message>true</message> <threadName>true</threadName> <dataType>true</dataType> <encoding>false</encoding> <assertions>true</assertions> <subresults>true</subresults> <responseData>false</responseData> <samplerData>false</samplerData> <xml>false</xml> <fieldNames>true</fieldNames> <responseHeaders>false</responseHeaders> <requestHeaders>false</requestHeaders> <responseDataOnError>false</responseDataOnError> <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> <assertionsResultsToSave>0</assertionsResultsToSave> <bytes>true</bytes> <sentBytes>true</sentBytes> <url>true</url> <threadCounts>true</threadCounts> <idleTime>true</idleTime> <connectTime>true</connectTime> </value> </objProp> <stringProp name="filename"></stringProp> </ResultCollector> <hashTree/> </hashTree> </hashTree> </hashTree> </jmeterTestPlan>
- ③ 测试效果windows 和linux 机制不一样
一台双核4g的虚拟机,里面已经安装好了jdk8。上边代码的jar上传到虚拟机上。
java -jar web-demo-1.1.0.jar --server.tomcat.maxThreads=10 --server.tomcat.maxConnections=2 --server.tomcat.acceptCount=3
jmeter加载上边写的测试脚本,修改服务器IP
linux环境下,最大连接数是2,acceptCount=3,来了10个线程进行操作,每次操作2个,最后应该处理5个,因为2+3 =5,但是linux有等待机制。所以全部都处理完了。
试试windows的环境下,启动命令跟linux一样。最大连接数是2,acceptCount=3,来了10个线程进行操作,每次操作2个,windows确实就处理了5个,剩余的直接抛弃掉了。
- ④ 1000个线程访问linux的程序
最大线程设置成4个。
java -jar web-demo-1.1.0.jar --server.tomcat.maxThreads=4
cpu利用率60.9% 有异常数据0.27%,响应的平均时间21s。
最大线程设置成200个。
java -jar web-demo-1.1.0.jar --server.tomcat.maxThreads=200
cpu利用率很高,但是异常数据很高。吞吐量变高99.9/sec。
就是不停的更换这个maxThreads 查看jmeter的结果,因为我是虚机很难很好的测试出结果
PS:请求多,CPU占用率高了,如果能接受很慢的响应,就加大。 否则就集群分流认清现实,优化代码才是王道,配置只能是锦上添花!tomcat基本不是单独使用的,基本要跟配合的,ngxin负责限流+日志记录。
胜象大百科