Spring?WebClient實(shí)戰(zhàn)示例
WebClient實(shí)戰(zhàn)
本文代碼地址:https://github.com/bigbirditedu/webclient
Spring Webflux 是 Spring Framework 5.0 的新特性,是隨著當(dāng)下流行的 Reactive Programming 而誕生的高性能框架。傳統(tǒng)的 Web 應(yīng)用框架,比如我們所熟知的 Struts2,Spring MVC 等都是基于 Servlet API 和 Servlet 容器之上運(yùn)行的,本質(zhì)上都是阻塞式的。Servlet 直到 3.1 版本之后才對(duì)異步非阻塞進(jìn)行了支持。而 WebFlux天生就是一個(gè)典型的異步非阻塞框架,其核心是基于 Reactor 相關(guān) API 實(shí)現(xiàn)的。相比傳統(tǒng)的 Web 框架,WebFlux 可以運(yùn)行在例如 Netty、Undertow 以及 Servlet 3.1 容器之上,其運(yùn)行環(huán)境比傳統(tǒng) Web 框架更具靈活性。
WebFlux 的主要優(yōu)勢(shì)有:
- 非阻塞性:WebFlux 提供了一種比 Servlet 3.1 更完美的異步非阻塞解決方案。非阻塞的方式可以使用較少的線程以及硬件資源來處理更多的并發(fā)。
- 函數(shù)式編程:函數(shù)式編程是 Java 8 重要的特性,WebFlux 完美支持。
webclient的HTTP API請(qǐng)參考:https://github.com/bigbirditedu/webclient
服務(wù)端性能對(duì)比
比較的是Spring MVC 與 Spring WebFlux 作為HTTP 應(yīng)用框架誰(shuí)的性能更好。
Spring WebFlux
先看看Spring WebFlux
引入pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
編寫http接口
@RestController @RequestMapping("/webflux") public class WebFluxController { public static AtomicLong COUNT = new AtomicLong(0); @GetMapping("/hello/{latency}") public Mono<String> hello(@PathVariable long latency) { System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); System.out.println("Page count:" + COUNT.incrementAndGet()); Mono<String> res = Mono.just("welcome to Spring Webflux").delayElement(Duration.ofSeconds(latency));//阻塞latency秒,模擬處理耗時(shí) System.out.println("End: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); return res; } }
啟動(dòng)服務(wù)器
可以看到webflux 默認(rèn)選擇Netty作為服務(wù)器
使用JMeter進(jìn)行壓測(cè):File->新建測(cè)試計(jì)劃->添加用戶線程組->在線程組上添加一個(gè)取樣器,選擇Http Request
配置Http請(qǐng)求,并在HTTP Request上添加監(jiān)聽器;這里不做復(fù)雜的壓測(cè)分析,選擇結(jié)果樹和聚合報(bào)告即可
設(shè)置http請(qǐng)求超時(shí)時(shí)間
設(shè)置并發(fā)用戶數(shù),60秒內(nèi)全部啟起來;
不斷調(diào)整進(jìn)行測(cè)試;每次開始前先Clear All清理一下舊數(shù)據(jù),再點(diǎn)save保存一下,再點(diǎn)Start開始
1000用戶,99線大約24毫秒的延遲
2000用戶,99線大約59毫秒的延遲
3000用戶,99線大約89毫秒的延遲
4000用戶
webflux到4000并發(fā)用戶時(shí)還是很穩(wěn)
Spring MVC
再來看看SpringMVC的性能
引入pom文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
編寫http接口
@RestController @RequestMapping("/springmvc") public class SpringMvcController { public static AtomicLong COUNT = new AtomicLong(0); @GetMapping("/hello/{latency}") public String hello(@PathVariable long latency) { System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); System.out.println("Page count:" + COUNT.incrementAndGet()); try { //阻塞latency秒,模擬處理耗時(shí) TimeUnit.SECONDS.sleep(latency); } catch (InterruptedException e) { e.printStackTrace(); return "Exception during thread sleep"; } System.out.println("End:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); return "welcome to Spring MVC"; } }
啟動(dòng)服務(wù)器??梢钥吹絊pringMVC默認(rèn)選擇Tomcat作為服務(wù)器
設(shè)置請(qǐng)求路徑
100用戶
200用戶
300用戶
從300用戶開始,響應(yīng)時(shí)間就開始增加
400用戶
500用戶
550用戶
本例中,傳統(tǒng)Web技術(shù)(Tomcat+SpringMVC)在處理550用戶并發(fā)時(shí),就開始有超時(shí)失敗的
600用戶
在處理600用戶并發(fā)時(shí),失敗率就已經(jīng)很高;用戶并發(fā)數(shù)更高時(shí)幾乎都會(huì)處理不過來,接近100%的請(qǐng)求超時(shí)。
1000用戶
2000用戶
3000用戶
4000用戶
客戶端性能比較
我們來比較一下HTTP客戶端的性能。
先建一個(gè)單獨(dú)的基于Springboot的Http Server工程提供標(biāo)準(zhǔn)的http接口供客戶端調(diào)用。
/** * Http服務(wù)提供方接口;模擬一個(gè)基準(zhǔn)的HTTP Server接口 */ @RestController public class HttpServerController { @RequestMapping("product") public Product getAllProduct(String type, HttpServletRequest request, HttpServletResponse response) throws InterruptedException { long start = System.currentTimeMillis(); System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); //輸出請(qǐng)求頭 Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String head = headerNames.nextElement(); System.out.println(head + ":" + request.getHeader(head)); } System.out.println("cookies=" + request.getCookies()); Product product = new Product(type + "A", "1", 56.67); Thread.sleep(1000); //設(shè)置響應(yīng)頭和cookie response.addHeader("X-appId", "android01"); response.addCookie(new Cookie("sid", "1000101111")); System.out.println("End:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); System.out.println("cost:" + (System.currentTimeMillis() - start) + product); return product; } @RequestMapping("products") public List<Product> getAllProducts(String type) throws InterruptedException { long start = System.currentTimeMillis(); List<Product> products = new ArrayList<>(); products.add(new Product(type + "A", "1", 56.67)); products.add(new Product(type + "B", "2", 66.66)); products.add(new Product(type + "C", "3", 88.88)); Thread.sleep(1000); System.out.println("cost:" + (System.currentTimeMillis() - start) + products); return products; } @RequestMapping("product/{pid}") public Product getProductById(@PathVariable String pid, @RequestParam String name, @RequestParam double price) throws InterruptedException { long start = System.currentTimeMillis(); Product product = new Product(name, pid, price); Thread.sleep(1000); System.out.println("cost:" + (System.currentTimeMillis() - start) + product); return product; } @RequestMapping("postProduct") public Product postProduct(@RequestParam String id, @RequestParam String name, @RequestParam double price) throws InterruptedException { long start = System.currentTimeMillis(); Product product = new Product(name, id, price); Thread.sleep(1000); System.out.println("cost:" + (System.currentTimeMillis() - start) + product); return product; } @RequestMapping("postProduct2") public Product postProduct(@RequestBody Product product) throws InterruptedException { long start = System.currentTimeMillis(); Thread.sleep(1000); System.out.println("cost:" + (System.currentTimeMillis() - start) + product); return product; } @RequestMapping("uploadFile") public String uploadFile(MultipartFile file, int age) throws InterruptedException { long start = System.currentTimeMillis(); System.out.println("age=" + age); String filePath = ""; try { String filename = file.getOriginalFilename(); //String extension = FilenameUtils.getExtension(file.getOriginalFilename()); String dir = "D:\\files"; filePath = dir + File.separator + filename; System.out.println(filePath); if (!Files.exists(Paths.get(dir))) { new File(dir).mkdirs(); } file.transferTo(Paths.get(filePath)); } catch (IOException e) { e.printStackTrace(); } Thread.sleep(1000); System.out.println("cost:" + (System.currentTimeMillis() - start)); return filePath; } }
Tip
其它客戶端代碼請(qǐng)?jiān)L問:https://github.com/bigbirditedu/webclient
webclient
和測(cè)試服務(wù)端時(shí)單獨(dú)依賴不同的服務(wù)器相比,這次同時(shí)引入兩個(gè)依賴。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
引入starter-web是為了啟動(dòng)Tomcat服務(wù)器,測(cè)試時(shí)統(tǒng)一使用Tomcat服務(wù)器跑http客戶端應(yīng)用程序;
引入starter-webflux是為了單獨(dú)使用webclient api,而不是為了使用Netty作為Http服務(wù)器;
500用戶(超時(shí)時(shí)間設(shè)置6秒)
1000用戶(超時(shí)時(shí)間設(shè)置6秒)
1100用戶(超時(shí)時(shí)間設(shè)置6秒)
可以看到已經(jīng)開始有響應(yīng)超時(shí)的了
1200用戶(超時(shí)時(shí)間設(shè)置10秒)
resttemplate(不帶連接池)
500用戶(超時(shí)時(shí)間設(shè)置6秒)
1000用戶并發(fā)(超時(shí)時(shí)間設(shè)置6秒)
1100用戶并發(fā)(超時(shí)時(shí)間設(shè)置6秒)
1200用戶(超時(shí)時(shí)間設(shè)置10秒),有少量響應(yīng)超時(shí)
resttemplate(帶連接池)
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency>
500用戶(超時(shí)時(shí)間設(shè)置6秒)
1000用戶(超時(shí)時(shí)間設(shè)置6秒)
1100用戶(超時(shí)時(shí)間設(shè)置6秒)
和 不帶連接池相比,錯(cuò)誤率減少
1200用戶(超時(shí)時(shí)間設(shè)置10秒),效果比不帶連接池的resttemplate好點(diǎn),但是響應(yīng)耗時(shí)普遍還是比帶連接池的webclient高
綜合來看,是否使用http連接池對(duì)于單個(gè)接口影響有限,池的效果不明顯;在多http地址、多接口路由時(shí)連接池的效果可能更好。
webclient連接池
默認(rèn)情況下,WebClient使用連接池運(yùn)行。池的默認(rèn)設(shè)置是最大500個(gè)連接和最大1000個(gè)等待請(qǐng)求。如果超過此配置,就會(huì)拋異常。
reactor.netty.internal.shaded.reactor.pool.PoolAcquirePendingLimitException: Pending acquire queue has reached its maximum size of 1000
報(bào)錯(cuò)日志顯示已經(jīng)達(dá)到了默認(rèn)的掛起隊(duì)列長(zhǎng)度限制1000,因此我們可以自定義線程池配置,以獲得更高的性能。
關(guān)于Reactor Netty連接池請(qǐng)參考Netty官方和Spring官方的文檔:
https://projectreactor.io/docs/netty/snapshot/reference/index.html#_connection_pool_2
1000用戶(超時(shí)時(shí)間設(shè)置6秒)
1100用戶(超時(shí)時(shí)間設(shè)置6秒)
帶連接池的效果好些,沒有出現(xiàn)失敗的
1200用戶(超時(shí)時(shí)間設(shè)置10秒),響應(yīng)延遲比默認(rèn)配置的webclient好些
webclient阻塞方式獲取結(jié)果;不自定義webclient線程池配置,2000用戶(JMeter不配置超時(shí)時(shí)間)
webclient+CompletableFuture方式獲取結(jié)果;不自定義webclient線程池配置,2000用戶(JMeter不配置超時(shí)時(shí)間)
雖然測(cè)試效果幾乎沒有差別,但是我們要清楚地知道調(diào)用block方法是會(huì)引發(fā)實(shí)時(shí)阻塞的,會(huì)一定程度上增加對(duì)CPU的消耗;
實(shí)際開發(fā)中通常是為了使用異步特性才用webclient,如果用block方式就白瞎了webclient了,還不如直接用restTemplate。
2000用戶性能比較
pooled webclient
rest
pooled rest
3000用戶性能比較
pooled webclient
rest
pooled rest
webclient 的HTTP API
WebClient 作為一個(gè) HTTP 客戶端工具,其提供了標(biāo)準(zhǔn) HTTP 請(qǐng)求方式,支持 Get、Post、Put、Delete、Head 等方法,可以作為替代 resttemplate 的一個(gè)強(qiáng)有力的工具。
API演示代碼地址:https://github.com/bigbirditedu/webclient
小結(jié)
使用webClient在等待遠(yuǎn)程響應(yīng)的同時(shí)不會(huì)阻塞本地正在執(zhí)行的線程 ;本地線程處理完一個(gè)請(qǐng)求緊接著可以處理下一個(gè),能夠提高系統(tǒng)的吞吐量;而restTemplate 這種方式是阻塞的,會(huì)一直占用當(dāng)前線程資源,直到http返回響應(yīng)。如果等待的請(qǐng)求發(fā)生了堆積,應(yīng)用程序?qū)?chuàng)建大量線程,直至耗盡線程池所有可用線程,甚至出現(xiàn)OOM。另外頻繁的CPU上下文切換,也會(huì)導(dǎo)致性能下降。
但是作為上述兩種方式的調(diào)用方(消費(fèi)者)而言,其最終獲得http響應(yīng)結(jié)果的耗時(shí)并未減少。比如文章案例中,通過瀏覽器訪問后端的的兩個(gè)接口(SpringMVC、SpringWebFlux)時(shí),返回?cái)?shù)據(jù)的耗時(shí)相同。即最終獲取(消費(fèi))數(shù)據(jù)的地方還會(huì)等待。
使用webclient替代restTemplate的好處是可以異步等待http響應(yīng),使得線程不需要阻塞;單位時(shí)間內(nèi)有限資源下支持更高的并發(fā)量。但是建議webclient和webflux配合使用,使整個(gè)流程全異步化;如果單獨(dú)使用webclient,筆者實(shí)測(cè),和resttemplate差別不大!歡迎留言指教!
到此這篇關(guān)于Spring WebClient實(shí)戰(zhàn)示例的文章就介紹到這了,更多相關(guān)Spring WebClient 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring框架中Bean的三種配置和實(shí)例化方法總結(jié)
在Spring框架中,Bean的配置和實(shí)例化是很重要的基礎(chǔ)內(nèi)容,掌握各種配置方式,才能靈活管理Bean對(duì)象,本文將全面介紹Bean的別名配置、作用范圍配置,以及構(gòu)造器實(shí)例化、工廠實(shí)例化等方式2023-10-10SpringBoot獲取客戶端的IP地址的實(shí)現(xiàn)示例
在Web應(yīng)用程序中,獲取客戶端的IP地址是一項(xiàng)非常常見的需求,本文主要介紹了SpringBoot獲取客戶端的IP地址的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09spring boot與ktor整合的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于spring boot與ktor整合的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Mybatis(ParameterType)傳遞多個(gè)不同類型的參數(shù)方式
這篇文章主要介紹了Mybatis(ParameterType)傳遞多個(gè)不同類型的參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04