本系列文章索引《響應式Spring的道法術器》
前情提要 Spring WebFlux快速上手 | Spring WebFlux性能測試
本文源碼java
因爲微服務架構的盛行,大型系統內服務間基於HTTP API進行調用的會至關頻繁。Netflix的系統有500+的微服務,感覺一下~react
咱們的測試以下圖所示,服務A調用服務B的API,從服務A發出請求到接收到響應,期間可能存在延遲,好比網絡不穩定、服務B不穩定,或由於所請求的API自己執行時間略長等等。對於做爲HTTP客戶端的服務A來講,是否可以異步地處理對服務B的請求與響應,也會帶來明顯的性能差別。咱們經過簡單的場景模擬一下:git
經過上一個測試,咱們已經肯定WebFlux-with-latency
的API /hello/{latency}
可以在高併發下,仍然以穩定的latency
~latency+5
ms的延遲作出響應,所以用來做爲被調用的服務B,模擬帶有延遲的服務。這樣若是測試結果出現明顯的差別,那麼能夠排除服務B的緣由。github
本次測試咱們建立兩個服務A的項目:restTemplate-as-caller
和webClient-as-caller
。它們也都提供URL爲/hello/{latency}
的API,在API的實現上都是經過Http請求服務A的/hello/{latency}
,返回的數據做爲本身的響應。區別在於:restTemplate-as-caller
使用RestTemplate
做爲Http客戶端,webClient-as-caller
使用WebClient
做爲Http客戶端。web
1)restTemplate-as-callerspring
使用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); } }
啓動服務WebFlux-with-latency
和restTemplate-as-caller
。併發
這個測試咱們並不須要分析1000~10000的不一樣用戶量場景下的響應時長的變化趨勢,只是驗證RestTemplate的阻塞性,因此直接測試一下6000用戶,測試結果以下:
吞吐量爲1651req/sec,95%響應時長爲1622ms。
與1.4.1中mvc-with-latency
的6000用戶的結果相似,可見RestTemplate確實是會阻塞的。好吧,其實寫個小@Test就能測出來是否是阻塞的,不過個人用意不只限於此,下邊咱們進行一個響應式改造。首先請回憶前邊介紹的兩個內容:
spring-boot-starter-web
+reactor-core
)也能夠像WebFlux同樣實現基於註解的響應式編程;基於此,咱們來改一下代碼。首先在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 } }
poolWithMaxSize
;測試時查看線程數:
可見,最多有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客戶端來改造其微服務網關的案例。