SpringBoot效能比較:Spring MVC與WebFlux
在這裡我想談談曾經在專案中遇到的有趣的事情。我們為我們的客戶在AWS中編寫了一些輕量級微服務,它只是通過HTTP代理對某些底層服務的請求,並將其返回給客戶端。
乍一看,什麼可能比編寫REST代理服務更簡單?
所以,當然,我們從Spring Boot開始編寫簡單的RestControllers。我們做了POC,結果很好。第三方服務具有符合要求的服務響應時間SLA,我們使用此值進行效能測試,第三方服務的響應時間非常好〜大約10-100ms。我們還決定利用CPU作為我們的微服務的擴充套件策略,這個服務是在Docker中作為AWS ECS服務執行。我們在AWS中配置了自動擴充套件並上線了。
事實上,並非一切順利。執行經常超時,我們經常重啟AWS ECS任務。我們只是執行很少的任務,另外,看到CPU和記憶體消耗很低但我們的服務還是太慢,有時甚至有超時錯誤。
問題在於第三方服務。第三方服務響應時間變為500-1000ms。但它從來沒有超時問題,能夠處理更多的客戶端。
所以關鍵問題還是在於我們的服務。我們沒有在需要時擴充套件我們的應用程式。我們進行了500-1000毫秒的效能測試,並感到震驚。
CPU很低,記憶體很好,但我們只能處理200個請求/秒。
這是Servlet執行緒的連線問題,預設執行緒池是200,這就是為什麼我們在1000毫秒響應時間內有200個請求/秒的原因。
但我們需要一個彈性服務:我們應該處理與底層服務一樣多的請求。響應時間應與基礎服務幾乎相同。
我們研究它並找到了幾個選項:
- 增加執行緒池大小
- 使用Servlet的DeferredResult或CompletableFuture
- Spring與WebFlux反應
選項1:增加執行緒池大小
是的,這是一個很好的解決方法,但只是解決方法!我們不能將這個值設定為幾千,因為它是具有非常有限的記憶體的Docker。每個執行緒都需要堆疊記憶體。
另一個問題是,如果某些第三方服務的響應時間很長,例如,5秒,我們仍會遇到同樣的問題。吞吐量等於=執行緒池大小/響應時間。如果我們有1000個執行緒和5秒延遲,則吞吐量是200個請求/秒。CPU再次很低,服務有足夠的資源進行處理。
選項2:帶Servlet的DeferredResult或CompletableFurure(非阻塞)
Servlet 3.1支援非同步處理。為了使它工作,我們需要返回一些Promise,Servlet將以非同步方式處理它。
我們將DeferredResult與CompletableFurure進行了比較,結果相同。因此,我們同意測試CompletableFurure。
選項3:Spring與WebFlux反應
這是現在最熱門的話題。從Spring文件 :
“使用少量執行緒處理併發性並使用更少的硬體資源進行擴充套件的非阻塞Web堆疊”
測試
測試環境:
Spring Boot:2.1.2.RELEASE(最新)
Java:11 OpenJDK
節點:t2.micro(亞馬遜Linux)
程式碼:https ://github.com/Aleksandr-Filichkin/spring-mvc-vs-webflux
Http客戶端: Java 11 Http客戶端,Apache Http客戶端,Spring WebClient
Test-Service(我們的代理服務)公開了幾個GET端點進行測試。所有端點都有一個延遲(以毫秒為單位)引數,用於模擬第三方服務延遲。
@GetMapping(value = <font>"/sync"</font><font>) <b>public</b> String getUserSync(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"sync: "</font><font> + x).join(); } @GetMapping(value = </font><font>"/completable-future-java-client"</font><font>) <b>public</b> CompletableFuture<String> getUserUsingWithCFAndJavaClient(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"completable-future-java-client: "</font><font> + x); } @GetMapping(value = </font><font>"/completable-future-apache-client"</font><font>) <b>public</b> CompletableFuture<String> getUserUsingWithCFAndApacheCLient(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithApacheHttpClient(delay).thenApply(x -> </font><font>"completable-future-apache-client: "</font><font> + x); } @GetMapping(value = </font><font>"/webflux-java-http-client"</font><font>) <b>public</b> Mono<String> getUserUsingWebfluxJavaHttpClient(@RequestParam <b>long</b> delay) { CompletableFuture<String> stringCompletableFuture = sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"webflux-java-http-client: "</font><font> + x); <b>return</b> Mono.fromFuture(stringCompletableFuture); } @GetMapping(value = </font><font>"/webflux-webclient"</font><font>) <b>public</b> Mono<String> getUserUsingWebfluxWebclient(@RequestParam <b>long</b> delay) { <b>return</b> webClient.get().uri(</font><font>"/user/?delay={delay}"</font><font>, delay).retrieve().bodyToMono(String.<b>class</b>).map(x -> </font><font>"webflux-webclient: "</font><font> + x); } @GetMapping(value = </font><font>"/webflux-apache-client"</font><font>) <b>public</b> Mono<String> apache(@RequestParam <b>long</b> delay) { <b>return</b> Mono.fromCompletionStage(sendRequestWithApacheHttpClient(delay).thenApply(x -> </font><font>"webflux-apache-client: "</font><font> + x)); } </font>
User-Service(第三方服務)公開單個端點GET“/ user?delay = {delay}”。延遲(ms)引數用於延遲模擬。如果我們傳送/ user?delay = 10,則響應時間將為10 ms +網路延遲(AWS內部最小);
這個使用者服務是我們的第三方服務(使用者服務),它非常快,可以處理超過4000個請求/秒
對於效能測試,我們將使用Jmeter。我們將測試100,200,400,800個併發請求的服務,延遲10,100,500毫秒。每個實施總共12個測試。
重要的提示:
我們僅針對熱伺服器測量效能:在每次測試之前,我們的服務處理了100萬個請求(用於JIT編譯器和JVM優化)
測試程式碼https://github.com/Aleksandr-Filichkin/spring-mvc-vs-webflux
測試結果點選標題見原文
結論(在單核,1GB RAM伺服器例項上):
Spring Webflux在所有測試情況下都獲勝 ,包括使用WebClient和Apache clients情況!
當底層服務很慢(500ms)時,有最顯著的差異(比阻塞Servlet快4倍);它比使用CompetableFuture的非阻塞Servlet快15-20%;此外,與Servlet(20 vs 220)相比,它不會建立大量執行緒。
不幸的是,我們無法在任何地方使用WebFlux,因為我們需要非同步驅動程式/客戶端。否則,我們必須建立自定義執行緒池/包裝器。
Servlet阻塞方式僅適用於底層服務快速(10ms)的情況。
Servlet非阻塞方式是一個非常好的解決方案,對於底層服務很慢(500毫秒)的情況。只有在有大量請求的情況下,它才會輸給Webflux。
附註:
- 對於單核,1GB RAM伺服器例項,Java 11 Http Client比Apache Http客戶端慢(效能降低約30%)
- Spring WebClient與Apache Http Client(都使用netty)在單核,1GB RAM伺服器例項上具有相同的效能
- 當你只有一個核心和一個小記憶體時,WebFlux和Java 11 Http Client的組合執行時模型不能很好地工作(https://github.com/spring-projects/spring-framework/issues/22333 )