1. 程式人生 > >(7)Spring WebClient與RestTemplate性能對比——響應式Spring的道法

(7)Spring WebClient與RestTemplate性能對比——響應式Spring的道法

響應式編程 Spring WebFlux

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

1.4.2 調用帶有延遲的服務負載分析

由於微服務架構的盛行,大型系統內服務間基於HTTP API進行調用的會相當頻繁。Netflix的系統有500+的微服務,感受一下~

我們的測試如下圖所示,服務A調用服務B的API,從服務A發出請求到接收到響應,期間可能存在延遲,比如網絡不穩定、服務B不穩定,或因為所請求的API本身執行時間略長等等。對於作為HTTP客戶端的服務A來說,是否能夠異步地處理對服務B的請求與響應,也會帶來明顯的性能差異。我們通過簡單的場景模擬一下:

<img height=100em src="https://leanote.com/api/file/getImage?fileId=5a97c87cab644114e400174e"/>

通過上一個測試,我們已經確定WebFlux-with-latency的API /hello/{latency}能夠在高並發下,仍然以穩定的latency~latency+5ms的延遲做出響應,因此用來作為被調用的服務B,模擬帶有延遲的服務。這樣如果測試結果出現明顯的差異,那麽可以排除服務B的原因。

本次測試我們創建兩個服務A的項目:restTemplate-as-callerwebClient-as-caller

。它們也都提供URL為/hello/{latency}的API,在API的實現上都是通過Http請求服務A的/hello/{latency},返回的數據作為自己的響應。區別在於:restTemplate-as-caller使用RestTemplate作為Http客戶端,webClient-as-caller使用WebClient作為Http客戶端。

1)restTemplate-as-caller

使用Spring Initializr創建一個依賴“Web”的項目(也就是WebMVC項目),POM依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

端口號設置為8093,然後開發/hello/{latency}

HelloController.java

    @RestController
    public class HelloController {
        private final String TARGET_HOST = "http://localhost:8092";
        private RestTemplate restTemplate;

        public HelloController() {  // 1
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
            connectionManager.setDefaultMaxPerRoute(1000);
            connectionManager.setMaxTotal(1000);
            this.restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(
                    HttpClientBuilder.create().setConnectionManager(connectionManager).build()
            ));

        }

        @GetMapping("/hello/{latency}")
        public String hello(@PathVariable int latency) {
            return restTemplate.getForObject(TARGET_HOST + "/hello/" + latency, String.class);
        }
    }
  1. 由於測試過程中,RestTemplate會發出大量請求,我們在Controller的構造方法中創建一個基於Http連接池構造的RestTemplate,否則可能會把系統能給的端口用盡而出錯;
  2. 使用RestTemplate請求服務B,並將響應返回。

啟動服務WebFlux-with-latencyrestTemplate-as-caller

這個測試我們並不需要分析1000~10000的不同用戶量場景下的響應時長的變化趨勢,只是驗證RestTemplate的阻塞性,所以直接測試一下6000用戶,測試結果如下:

技術分享圖片

吞吐量為1651req/sec,95%響應時長為1622ms。

與1.4.1中mvc-with-latency的6000用戶的結果類似,可見RestTemplate確實是會阻塞的。好吧,其實寫個小@Test就能測出來是不是阻塞的,不過我的用意不僅限於此,下邊我們進行一個響應式改造。首先請回憶前邊介紹的兩個內容:

  1. 不知道你是否還記得在1.3.3.1的最後提過,用Spring WebMVC + Reactor(spring-boot-starter-web+reactor-core)也可以像WebFlux一樣實現基於註解的響應式編程;
  2. 在1.3.2.5介紹過如何利用elastic的調度器將阻塞的調用轉化為異步非阻塞的。

基於此,我們來改一下代碼。首先在pom.xml中增加reactor-core

        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>3.1.4.RELEASE</version>
        </dependency>

然後RestTemplate的調用轉為異步:

    @GetMapping("/hello/{latency}")
    public Mono<String> hello(@PathVariable int latency) {
        return Mono.fromCallable(() -> restTemplate.getForObject(TARGET_HOST + "/hello/" + latency, String.class))
                .subscribeOn(Schedulers.elastic());
    }

再次測試,發現結果有了明顯改善:

技術分享圖片

吞吐量為2169 req/sec,95%響應時長為121ms。

但是,使用Schedulers.elastic()其實就相當於將每一次阻塞的RestTemplate調用調度到不同的線程裏去執行,效果如下:

技術分享圖片

因為不僅有處理請求的200個線程,還有Schedulers.elastic()給分配的工作線程,所以總的線程數量飆到了1000多個!不過在生產環境中,我們通常不會直接使用彈性線程池,而是使用線程數量可控的線程池,RestTemplate用完所有的線程後,更多的請求依然會造成排隊的情況。

這一點使用Schedulers.newParallel()的調度器一測便知。

    @RestController
    public class HelloController {
        private final String TARGET_HOST = "http://localhost:8092";
        private RestTemplate restTemplate;
        private Scheduler fixedPool;

        public HelloController() {
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
            connectionManager.setDefaultMaxPerRoute(1000);
            connectionManager.setMaxTotal(1000);
            this.restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(
                    HttpClientBuilder.create().setConnectionManager(connectionManager).build()
            ));
            fixedPool = Schedulers.newParallel("poolWithMaxSize", 400); // 1
        }

        @GetMapping("/hello/{latency}")
    //    public String hello(@PathVariable int latency) {
    //        return restTemplate.getForObject(TARGET_HOST + "/hello/" + latency, String.class);
    //    }
        public Mono<String> hello(@PathVariable int latency) {
            return Mono.fromCallable(() -> restTemplate.getForObject(TARGET_HOST + "/hello/" + latency, String.class))
                    .subscribeOn(fixedPool);    // 2
        }
    }
  1. 創建一個有最大400個線程的線程池poolWithMaxSize
  2. 調度到這個線程池上。

測試時查看線程數:

技術分享圖片

可見,最多有400個名為poolWithMaxSize的線程,RestTemplate就工作在這些線程上,相比請求處理線程多了一倍。看一下最終的測試結果:

技術分享圖片

吞吐量2169req/sec,與彈性線程池的那次相同;95%響應時長為236ms,雖然達不到彈性線程池的效果,但是比完全同步阻塞的方式(RestTemplate在請求處理線程中執行)要好多了。

我們再看看非阻塞的WebClient表現如何吧。

2)webClient-as-caller

webClient-as-caller基於WebFlux的依賴,端口號8094,不多說,直接看Controller:

    @RestController
    public class HelloController {
        private final String TARGET_HOST = "http://localhost:8092";
        private WebClient webClient;

        public HelloController() {
            this.webClient = WebClient.builder().baseUrl(TARGET_HOST).build();
        }

        @GetMapping("/hello/{latency}")
        public Mono<String> hello(@PathVariable int latency) {
            return webClient
                    .get().uri("/hello/" + latency)
                    .exchange()
                    .flatMap(clientResponse -> clientResponse.bodyToMono(String.class));
        }

跑一下6000用戶的測試:

技術分享圖片

吞吐量2195 req/sec,95%響應時長109ms。

關鍵的是,WebClient不需要大量並發的線程就可以漂亮地搞定這件事兒了:

技術分享圖片

3)總結

WebClient同樣能夠以少量而固定的線程數處理高並發的Http請求,在基於Http的服務間通信方面,可以取代RestTemplate以及AsyncRestTemplate。

異步非阻塞的Http客戶端,請認準——WebClient~

下一節,介紹一下微服務先行者、全球最大的視頻服務平臺Netflix使用異步的Http客戶端來改造其微服務網關的案例。

(7)Spring WebClient與RestTemplate性能對比——響應式Spring的道法