本系列文章索引《響應式Spring的道法術器》
前情提要 響應式流 | Reactor 3快速上手 | Spring WebFlux快速上手
本文源碼java
前面老是「安利」異步非阻塞的好處,下面咱們就實實在在感覺一下響應式編程在高併發環境下的性能提高。異步非阻塞的優點體如今I/O操做方面,不管是文件I/O、網絡I/O,仍是數據庫讀寫,均可能存在阻塞的狀況。react
咱們的測試內容有三:git
RestTemplate
和非阻塞的WebClient
;說明:本節進行的並不是是嚴謹的基於性能調優的需求的,針對具體業務場景的負載測試。本節測試場景簡單而直接,各位朋友GET到個人點便可。
此外:因爲本節主要是進行橫向對比測試,所以不須要特定的硬件資源配置,不過仍是建議在Linux環境下進行測試,我最初是在Win10上跑的,當用戶數上來以後出現了很多請求失敗的狀況,下邊的測試數據是在一臺系統爲Deepin Linux(Debian系)的筆記本上跑出來的。github
那麼咱們就開始搭建測試環境吧~ (關於Spring WebFlux 不熟悉的話,請參考Spring WebFlux快速上手)。shell
1)搭建待測試項目數據庫
咱們分別基於WebMVC和WebFlux建立兩個項目:mvc-with-latency
和WebFlux-with-latency
。編程
爲了模擬阻塞,咱們分別在兩個項目中各建立一個帶有延遲的/hello/{latency}
的API。好比/hello/100
的響應會延遲100ms。tomcat
mvc-with-latency
中建立HelloController.java
:服務器
@RestController public class HelloController { @GetMapping("/hello/{latency}") public String hello(@PathVariable long latency) { try { TimeUnit.MILLISECONDS.sleep(latency); // 1 } catch (InterruptedException e) { return "Error during thread sleep"; } return "Welcome to reactive world ~"; } }
WebFlux-with-latency
中建立HelloController.java
:網絡
@RestController public class HelloController { @GetMapping("/hello/{latency}") public Mono<String> hello(@PathVariable int latency) { return Mono.just("Welcome to reactive world ~") .delayElement(Duration.ofMillis(latency)); // 1 } }
delayElement
操做符來實現延遲。而後各自在application.properties
中配置端口號8091和8092:
server.port=8091
啓動應用。
2)編寫負載測試腳本
本節咱們採用gatling來進行測試。建立測試項目gatling-scripts
。
POM中添加gatling依賴和插件(目前gradle暫時尚未這個插件,因此只能是maven項目):
<dependencies> <dependency> <groupId>io.gatling.highcharts</groupId> <artifactId>gatling-charts-highcharts</artifactId> <version>2.3.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.gatling</groupId> <artifactId>gatling-maven-plugin</artifactId> <version>2.2.4</version> </plugin> </plugins> </build>
在src/test
下建立測試類,gatling使用scala語言編寫測試類:
import io.gatling.core.scenario.Simulation import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class LoadSimulation extends Simulation { // 從系統變量讀取 baseUrl、path和模擬的用戶數 val baseUrl = System.getProperty("base.url") val testPath = System.getProperty("test.path") val sim_users = System.getProperty("sim.users").toInt val httpConf = http.baseURL(baseUrl) // 定義模擬的請求,重複30次 val helloRequest = repeat(30) { // 自定義測試名稱 exec(http("hello-with-latency") // 執行get請求 .get(testPath)) // 模擬用戶思考時間,隨機1~2秒鐘 .pause(1 second, 2 seconds) } // 定義模擬的場景 val scn = scenario("hello") // 該場景執行上邊定義的請求 .exec(helloRequest) // 配置併發用戶的數量在30秒內均勻提升至sim_users指定的數量 setUp(scn.inject(rampUsers(sim_users).over(30 seconds)).protocols(httpConf)) }
如上,這個測試的場景是:
其中URL和用戶量經過base.url
、test.path
、sim.users
變量傳入,藉助maven插件,經過以下命令啓動測試:
mvn gatling:test -Dgatling.simulationClass=test.load.sims.LoadSimulation -Dbase.url=http://localhost:8091/ -Dtest.path=hello/100 -Dsim.users=300
就表示用戶量爲300的對http://localhost:8091/hello/100
的測試。
3)觀察線程數量
測試以前,咱們打開jconsole觀察應用(鏈接MVCWithLatencyApplication)的線程變化狀況:
如圖(分辨率問題顯示不太好)是剛啓動無任何請求進來的時候,默認執行線程有10個,總的線程數31-33個。
好比,當進行用戶數爲2500個的測試時,執行線程增長到了200個,總的線程數峯值爲223個,就是增長的這190個執行線程。以下:
因爲在負載過去以後,執行線程數量會隨機減小回10個,所以看最大線程編號估算線程個數的話並不靠譜,咱們能夠用「峯值線程數-23」獲得測試過程當中的執行線程個數。
4)負載測試
首先咱們測試mvc-with-latency
:
測試數據以下(Tomcat最大線程數200,延遲100ms):
由以上數據可知:
這裏咱們不可貴出緣由,那就是當全部可用線程都在阻塞狀態的話,後續再進入的請求只能排隊,從而當達到最大線程數以後,響應時長開始上升。咱們以6000用戶的報告爲例:
這幅圖是請求響應時長隨時間變化的圖,能夠看到大體能夠分爲五個段:
全部請求的響應時長分佈以下圖所示:
A/E段與C段的時長只差就是平均的排隊等待時間。在持續的高併發狀況下,大部分請求是處在C段的。並且等待時長隨請求量的提升而線性增加。
增長Servlet容器處理請求的線程數量能夠緩解這一問題,就像上邊把最大線程數量從默認的200增長的400。
最高200的線程數是Tomcat的默認設置,咱們將其設置爲400再次測試。在application.properties
中增長:
server.tomcat.max-threads=400
測試數據以下:
因爲工做線程數擴大一倍,所以請求排隊的狀況緩解一半,具體能夠對比一下數據:
這也再次印證了咱們上邊的分析。增長線程數確實能夠必定程度下提升吞吐量,下降因阻塞形成的響應延時,但此時咱們須要權衡一些因素:
咱們再來看一下對於WebFlux-with-latency
的測試數據:
reactor-http-nio-X
和parallel-X
的線程,我這是四核八線程的i7,因此X
從1-8),由於異步非阻塞條件下,程序邏輯是由事件驅動的,並不須要多線程併發;可見,非阻塞的處理方式規避了線程排隊等待的狀況,從而能夠用少許而固定的線程處理應對大量請求的處理。
除此以外,我又一步到位直接測試了一下20000用戶的狀況:
- 對
mvc-with-latency
的測試因爲出現了許多的請求fail而以失敗了結;- 而
WebFlux-with-latency
應對20000用戶已然面不改色心不慌,吞吐量達到7228 req/sec(我擦,正好是10000用戶下的兩倍,太巧了今天怎麼了,絕對是真實數據!),95%響應時長僅117ms。
最後,再給出兩個吞吐量和響應時長的圖,更加直觀地感覺異步非阻塞的WebFlux是如何一騎絕塵的吧:
綜上來講,結論就是相對於Servlet多線程的處理方式來講,Spring WebFlux在應對高併發的請求時,藉助於異步IO,可以以少許而穩定的線程處理更高吞吐量的請求,尤爲是當請求處理過程若是由於業務複雜或IO阻塞等致使處理時長較長時,對比更加顯著。
本文模擬的延遲時間較長,達到了100ms,雖然有些誇張,可是不可否認IO阻塞的嚴重性,還記得「1.2.1.1 IO有多慢?」中的比喻嗎?若是CPU執行一條指令的時間是1秒,那麼內存尋址就須要4分20秒,SSD尋址須要4.5天,磁盤尋址須要1個月。。。異步IO可以將CPU從「漫長」的等待中解放出來,再也不須要堆砌大量的線程來提升CPU利用率。這也是Spring WebFlux可以以少許線程處理更高吞吐量的緣由。
此時,咱們更加理解了Nodejs的驕傲,不過咱們大Java語言也有了Vert.x和如今的Spring WebFlux。
本節咱們進行服務器端的性能測試,下一節繼續分析Spring WebFlux的客戶端工具WebClient的性能表現,它會對微服務架構的系統帶來不小的性能提高呢!