1. 程式人生 > >(6)Spring WebFlux性能測試——響應式Spring的道法術器

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

響應式編程 Spring WebFlux

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

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

前面總是“安利”異步非阻塞的好處,下面我們就實實在在感受一下響應式編程在高並發環境下的性能提升。異步非阻塞的優勢體現在I/O操作方面,無論是文件I/O、網絡I/O,還是數據庫讀寫,都可能存在阻塞的情況。

我們的測試內容有三:

  1. 首先分別創建基於WebMVC和WebFlux的Web服務,來對比觀察異步非阻塞能帶來多大的性能提升,我們模擬一個簡單的帶有延遲的場景,然後啟動服務使用gatling進行測試,並進行分析;
  2. 由於現在微服務架構應用越來越廣泛,我們基於第一步的測試項目進一步觀察調用存在延遲的服務的情況下的測試數據,其實主要是針對客戶端的測試:阻塞的RestTemplate
    和非阻塞的WebClient
  3. 針對MongoDB的同步和異步數據庫驅動進行性能測試和分析。

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

那麽我們就開始搭建這套簡單粗暴的測試環境吧~

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

1)搭建待測試項目

我們分別基於WebMVC和WebFlux創建兩個項目:mvc-with-latency

WebFlux-with-latency

為了模擬阻塞,我們分別在兩個項目中各創建一個帶有延遲的/hello/{latency}的API。比如/hello/100的響應會延遲100ms。

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)的線程變化情況:

技術分享圖片

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

比如,當進行用戶數為2500個的測試時,執行線程增加到了200個,總的線程數峰值為223個,就是增加的這190個執行線程。如下:

技術分享圖片

由於在負載過去之後,執行線程數量會隨機減少回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):

技術分享圖片

技術分享圖片

由以上數據可知:

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

這裏我們不難得出原因,那就是當所有可用線程都在阻塞狀態的話,後續再進入的請求只能排隊,從而當達到最大線程數之後,響應時長開始上升。我們以6000用戶的報告為例:

技術分享圖片

這幅圖是請求響應時長隨時間變化的圖,可以看到大致可以分為五個段:

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

所有請求的響應時長分布如下圖所示:

技術分享圖片

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

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

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

server.tomcat.max-threads=400

測試數據如下:

技術分享圖片

技術分享圖片

由於工作線程數擴大一倍,因此請求排隊的情況緩解一半,具體可以對比一下數據:

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

這也再次印證了我們上邊的分析。增加線程數確實可以一定程度下提高吞吐量,降低因阻塞造成的響應延時,但此時我們需要權衡一些因素:

  • 增加線程是有成本的,JVM中默認情況下在創建新線程時會分配大小為1M的線程棧,所以更多的線程異味著更多的內存;
  • 更多的線程會帶來更多的線程上下文切換成本。

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

技術分享圖片

  • 這裏沒有統計線程數量,因為對於運行在異步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是如何一騎絕塵的吧:

技術分享圖片 技術分享圖片

此時,我們更加理解了Nodejs的驕傲,不過我們大Java語言也有了Vert.x和現在的Spring WebFlux。

本節我們進行服務器端的性能測試,下一節繼續分析Spring WebFlux的客戶端工具WebClient的性能表現,它會對微服務架構的系統帶來不小的性能提升呢!

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