雖然現在springboot微服務縱橫都是用的jar包,但是還有很多使用的Tomcat。tomcat是servlet的容器,也是springboot默認集成的容器,有必要對他的網絡線程模型做一下了解。
(一) tomcat網絡處理線程模型
- ① BIO同步Servlet
一個請求,一個工作線程,CPU利用率低,tomcat7以下才使用這種,新版本不再使用,tomcat8默認NIO
- ② APR 異步Servlet
apr(Apache 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就是控制堆積數量的。windows這塊,操作系統堆滿了,tcp這塊也堆滿了直接關閉了請求了。
linux這塊,不僅僅有隊列,還有個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的參數配置
JAVA -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個線程。
(二)示例
- ① 代碼
WebDemoApplication
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基本不是單獨使用的,基本要跟Nginx配合的,ngxin負責限流+日志記錄。