(6)Spring WebFlux性能測試——響應式Spring的道法術器

本系列文章索引《響應式Spring的道法術器》
前情提要 響應式流 | Reactor 3快速上手 | Spring WebFlux快速上手
本文源碼java

1.4 從負載測試看異步非阻塞的優點

前面老是「安利」異步非阻塞的好處,下面咱們就實實在在感覺一下響應式編程在高併發環境下的性能提高。異步非阻塞的優點體如今I/O操做方面,不管是文件I/O、網絡I/O,仍是數據庫讀寫,均可能存在阻塞的狀況。react

咱們的測試內容有三:git

  1. 首先分別建立基於WebMVC和WebFlux的Web服務,來對比觀察異步非阻塞能帶來多大的性能提高,咱們模擬一個簡單的帶有延遲的場景,而後啓動服務使用gatling進行測試,並進行分析;
  2. 因爲如今微服務架構應用愈來愈普遍,咱們基於第一步的測試項目進一步觀察調用存在延遲的服務的狀況下的測試數據,其實主要是針對客戶端的測試:阻塞的RestTemplate和非阻塞的WebClient
  3. 針對MongoDB的同步和異步數據庫驅動進行性能測試和分析。

說明:本節進行的並不是是嚴謹的基於性能調優的需求的,針對具體業務場景的負載測試。本節測試場景簡單而直接,各位朋友GET到個人點便可。
此外:因爲本節主要是進行橫向對比測試,所以不須要特定的硬件資源配置,不過仍是建議在Linux環境下進行測試,我最初是在Win10上跑的,當用戶數上來以後出現了很多請求失敗的狀況,下邊的測試數據是在一臺系統爲Deepin Linux(Debian系)的筆記本上跑出來的。github

那麼咱們就開始搭建測試環境吧~ (關於Spring WebFlux 不熟悉的話,請參考Spring WebFlux快速上手)。shell

1.4.1 帶有延遲的負載測試分析

1)搭建待測試項目數據庫

咱們分別基於WebMVC和WebFlux建立兩個項目:mvc-with-latencyWebFlux-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 ~";
        }
    }
  1. 利用sleep來模擬業務場景中發生阻塞的狀況。

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
        }
    }
  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))
    }

如上,這個測試的場景是:

  • 指定的用戶量是在30秒時間內勻速增長上來的;
  • 每一個用戶重複請求30次指定的URL,中間會隨機間隔1~2秒的思考時間。

其中URL和用戶量經過base.urltest.pathsim.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)的線程變化狀況:

(6)Spring WebFlux性能測試——響應式Spring的道法術器

如圖(分辨率問題顯示不太好)是剛啓動無任何請求進來的時候,默認執行線程有10個,總的線程數31-33個。

好比,當進行用戶數爲2500個的測試時,執行線程增長到了200個,總的線程數峯值爲223個,就是增長的這190個執行線程。以下:

(6)Spring WebFlux性能測試——響應式Spring的道法術器

因爲在負載過去以後,執行線程數量會隨機減小回10個,所以看最大線程編號估算線程個數的話並不靠譜,咱們能夠用「峯值線程數-23」獲得測試過程當中的執行線程個數。

4)負載測試

首先咱們測試mvc-with-latency

  • -Dbase.url=http://localhost:8091/
  • -Dtest.path=hello/100(延遲100ms);
  • -Dsim.users=1000/2000/3000/.../10000。

測試數據以下(Tomcat最大線程數200,延遲100ms):

(6)Spring WebFlux性能測試——響應式Spring的道法術器

(6)Spring WebFlux性能測試——響應式Spring的道法術器

由以上數據可知:

  1. 用戶量在接近3000的時候,線程數達到默認的最大值200;
  2. 線程數達到200前,95%的請求響應時長是正常的(比100ms多一點點),以後呈直線上升的態勢;
  3. 線程數達到200後,吞吐量增幅逐漸放緩。

這裏咱們不可貴出緣由,那就是當全部可用線程都在阻塞狀態的話,後續再進入的請求只能排隊,從而當達到最大線程數以後,響應時長開始上升。咱們以6000用戶的報告爲例:

title

這幅圖是請求響應時長隨時間變化的圖,能夠看到大體能夠分爲五個段:

  • A. 有空閒線程可用,請求能夠在100ms+時間返回;
  • B. 線程已滿,新來的請求開始排隊,由於A和B階段是用戶量均勻上升的階段,因此排隊的請求愈來愈多;
  • C. 每秒請求量穩定下來,可是因爲排隊,維持一段時間的高響應時長;
  • D. 部分用戶的請求完成,每秒請求量逐漸降低,排隊狀況逐漸緩解;
  • E. 用戶量降至線程滿負荷且隊列消化後,請求在正常時間返回;

全部請求的響應時長分佈以下圖所示:

title

A/E段與C段的時長只差就是平均的排隊等待時間。在持續的高併發狀況下,大部分請求是處在C段的。並且等待時長隨請求量的提升而線性增加。

增長Servlet容器處理請求的線程數量能夠緩解這一問題,就像上邊把最大線程數量從默認的200增長的400。

最高200的線程數是Tomcat的默認設置,咱們將其設置爲400再次測試。在application.properties中增長:

server.tomcat.max-threads=400

測試數據以下:

(6)Spring WebFlux性能測試——響應式Spring的道法術器

(6)Spring WebFlux性能測試——響應式Spring的道法術器

因爲工做線程數擴大一倍,所以請求排隊的狀況緩解一半,具體能夠對比一下數據:

  1. 「最大線程數200用戶5000」的「95%響應時長」剛好與「最大線程數400用戶10000」徹底一致,我對天發誓,這絕對絕對是真實數據,更加巧合的是,吞吐量也剛好是1:2的關係!有此巧合也是由於測試場景太簡單粗暴,哈哈;
  2. 「95%響應時長」的曲線斜率也是兩倍的關係。

這也再次印證了咱們上邊的分析。增長線程數確實能夠必定程度下提升吞吐量,下降因阻塞形成的響應延時,但此時咱們須要權衡一些因素:

  • 增長線程是有成本的,JVM中默認狀況下在建立新線程時會分配大小爲1M的線程棧,因此更多的線程異味着更多的內存;
  • 更多的線程會帶來更多的線程上下文切換成本。

咱們再來看一下對於WebFlux-with-latency的測試數據:

(6)Spring WebFlux性能測試——響應式Spring的道法術器

  • 這裏沒有統計線程數量,由於對於運行在異步IO的Netty之上的WebFlux應用來講,其工做線程數量始終維持在一個固定的數量上,一般這個固定的數量等於CPU核數(經過jconsole能夠看到有名爲reactor-http-nio-Xparallel-X的線程,我這是四核八線程的i7,因此X從1-8),由於異步非阻塞條件下,程序邏輯是由事件驅動的,並不須要多線程併發;
  • 隨着用戶數的增多,吞吐量基本呈線性增多的趨勢;
  • 95%的響應都在100ms+的可控範圍內返回了,並未出現延時的狀況。

可見,非阻塞的處理方式規避了線程排隊等待的狀況,從而能夠用少許而固定的線程處理應對大量請求的處理。

除此以外,我又一步到位直接測試了一下20000用戶的狀況:

  1. mvc-with-latency的測試因爲出現了許多的請求fail而以失敗了結;
  2. WebFlux-with-latency應對20000用戶已然面不改色心不慌,吞吐量達到7228 req/sec(我擦,正好是10000用戶下的兩倍,太巧了今天怎麼了,絕對是真實數據!),95%響應時長僅117ms。

最後,再給出兩個吞吐量和響應時長的圖,更加直觀地感覺異步非阻塞的WebFlux是如何一騎絕塵的吧:

(6)Spring WebFlux性能測試——響應式Spring的道法術器 (6)Spring WebFlux性能測試——響應式Spring的道法術器

綜上來講,結論就是相對於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的性能表現,它會對微服務架構的系統帶來不小的性能提高呢!

相關文章
相關標籤/搜索