來源:叢林medium.com
大量的文章評估了一系列技術(包括 Node.js、Deno、Bun、Rust、Go、Spring、Python 等)在簡單的“hello world”場景中的性能。雖然這些文章獲得了好評,但有一個共同點:忽略了現實場景開發中的復雜性 。
本文旨在通過現實場景的視角剖析各種技術,在這種特殊情況下,我們深入研究以下常見用例:
從 authorization header 中提取一個JWT。
驗證JWT并從聲明中提取用戶的電子郵件。
使用提取的電子郵件執行MySQL查詢。
最后,返回用戶的記錄。
雖然這個場景看起來似乎也很簡單,但它概括了 Web 開發領域中經常遇到的現實挑戰。
介紹
在本文中,我們將深入探討所有同級產品之間的友好比較,即具有「物理線程、虛擬線程和 Webflux 的 SpringBoot」 ,重點關注它們在特定用例場景中的性能。我們已經探索了標準 SpringBoot 應用程序如何與 webflux 相媲美,但現在,我們引入一個關鍵的區別:
帶有虛擬線程的 Spring Boot
我們熟悉 SpringBoot,但有一點不同——它在虛擬線程而不是傳統的物理線程上運行。虛擬線程是并發領域的游戲規則改變者。這些輕量級線程簡化了開發、維護和調試高吞吐量并發應用程序的復雜任務。
雖然虛擬線程仍然在底層操作系統線程上運行,但它們帶來了顯著的效率改進。當虛擬線程遇到阻塞 I/O 操作時,Java 運行時會暫時掛起它,從而釋放關聯的操作系統線程來為其他虛擬線程提供服務。這個優雅的解決方案優化了資源分配并增強了整體應用程序響應能力。
考慮到這些有趣的設置,讓我們更深入地研究我們的性能比較。撰寫本文是為了解決最常見的請求之一,即查看物理、虛擬和 Webflux 在實際用例中的比較。
測試環境及軟件版本
我們的性能測試是在配備 16GB RAM 的 MacBook Pro M1 上進行的,確保了可靠的測試平臺。用于這些測試的軟件堆棧包括:
SpringBoot 3.1.3(在Java 20上運行)
啟用預覽模式以獲得虛擬線程的強大功能
jjwt用于JWT驗證和解碼,增強我們應用程序的安全性。
mysql-connector-java 用于執行 MySQL 查詢,維護數據完整性和一致性。
負載測試和 JWT
為了評估我們的應用程序在不同負載下的性能,我們使用了開源負載測試工具 Bombardier。我們的測試場景涉及預先創建的 100000 個 JWT 列表。在測試過程中,Bombardier 從該池中隨機選擇 JWT,并將它們包含在 HTTP 請求的授權標頭中。
MySQL 數據庫架構
用于這些性能測試的 MySQL 數據庫有一個名為 users 的表。該表設計有 6 列,足以模擬我們應用程序中的真實數據交互,使我們能夠評估它們的響應能力和可擴展性。
mysql>descusers; +--------+--------------+------+-----+---------+-------+ |Field|Type|Null|Key|Default|Extra| +--------+--------------+------+-----+---------+-------+ |email|varchar(255)|NO|PRI|NULL|| |first|varchar(255)|YES||NULL|| |last|varchar(255)|YES||NULL|| |city|varchar(255)|YES||NULL|| |county|varchar(255)|YES||NULL|| |age|int|YES||NULL|| +--------+--------------+------+-----+---------+-------+ 6rowsinset(0.00sec)
用戶數據庫已準備好包含 100000 條用戶記錄的初始數據集。
mysql>selectcount(*)fromusers; +----------+ |count(*)| +----------+ |99999| +----------+ 1rowinset(0.01sec)
在我們對 SpringBoot 物理線程、虛擬線程和 Webflux 進行友好性能評估的背景下,了解關鍵的數據關系至關重要。具體來說,在JSON Web Token(JWT)有效負載中,每個電子郵件條目直接對應于存儲在 MySQL 數據庫中的一條用戶記錄。
代碼
SpringBoot(物理線程)
配置信息
server.port=3000 spring.datasource.url= jdbc//localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username= dbuser spring.datasource.password= dbpwd spring.jpa.hibernate.ddl-auto= update spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
實體類
packagecom.example.demo; importjakarta.persistence.Entity; importjakarta.persistence.Table; importjakarta.persistence.GeneratedValue; importjakarta.persistence.GenerationType; importjakarta.persistence.Id; @Entity @Table(name="users") publicclassUser{ @Id privateStringemail; privateStringfirst; privateStringlast; privateStringcity; privateStringcounty; privateintage; publicStringgetId(){ returnemail; } publicvoidsetId(Stringemail){ this.email=email; } publicStringgetFirst(){ returnfirst; } publicvoidsetFirst(Stringname){ this.first=name; } publicStringgetLast(){ returnlast; } publicvoidsetLast(Stringname){ this.last=name; } publicStringgetEmail(){ returnemail; } publicvoidsetEmail(Stringemail){ this.email=email; } publicStringgetCity(){ returncity; } publicvoidsetCity(Stringcity){ this.city=city; } publicStringgetCounty(){ returncounty; } publicvoidsetCounty(Stringcounty){ this.county=county; } publicintgetAge(){ returnage; } publicvoidsetAge(intage){ this.age=age; } }
啟動類
packagecom.example.demo; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; importorg.springframework.context.annotation.Bean; @SpringBootApplication publicclassUserApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(UserApplication.class,args); } }
Controller層
packagecom.example.demo; importorg.springframework.web.bind.annotation.GetMapping; importorg.springframework.web.bind.annotation.RequestHeader; importorg.springframework.http.ResponseEntity; importorg.springframework.http.HttpStatus; importorg.springframework.http.HttpHeaders; importorg.springframework.web.bind.annotation.RestController; importorg.springframework.beans.factory.annotation.Autowired; importjava.util.Optional; importio.jsonwebtoken.Jwts; importio.jsonwebtoken.Jws; importio.jsonwebtoken.Claims; importio.jsonwebtoken.SignatureAlgorithm; importio.jsonwebtoken.security.Keys; importjava.security.Key; importcom.example.demo.UserRepository; importcom.example.demo.User; @RestController publicclassUserController{ @Autowired UserRepositoryuserRepository; privateSignatureAlgorithmsa=SignatureAlgorithm.HS256; privateStringjwtSecret=System.getenv("JWT_SECRET"); @GetMapping("/") publicUserhandleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION)StringauthHdr){ StringjwtString=authHdr.replace("Bearer",""); Claimsclaims=Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); Optionaluser=userRepository.findById((String)claims.get("email")); returnuser.get(); } }
接口類
packagecom.example.demo; importorg.springframework.data.repository.CrudRepository; importcom.example.demo.User; publicinterfaceUserRepositoryextendsCrudRepository{ }
Springboot(虛擬線程)
其余代碼基本照搬上述 「物理線程」 , 啟動類修改如下:
packagecom.example.demo; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; importorg.springframework.context.annotation.Bean; importjava.util.concurrent.Executors; @SpringBootApplication publicclassUserApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(UserApplication.class,args); } @Bean publicTomcatProtocolHandlerCustomizer>protocolHandlerVirtualThreadExecutorCustomizer(){ returnprotocolHandler->{ protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; } }
SpringBoot(webflux)
server.port=3000 spring.r2dbc.url=r2dbc//localhost:3306/testdb?allowPublicKeyRetrieval=true&ssl=false spring.r2dbc.username=dbuser spring.r2dbc.password=dbpwd spring.r2dbc.pool.initial-size=10 spring.r2dbc.pool.max-size=10
啟動類
packagewebfluxdemo; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.context.annotation.Bean; importorg.springframework.core.io.ClassPathResource; importorg.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; importorg.springframework.r2dbc.connection.init.ResourceDatabasePopulator; importorg.springframework.web.reactive.config.EnableWebFlux; importio.r2dbc.spi.ConnectionFactory; @EnableWebFlux @SpringBootApplication publicclassUserApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(UserApplication.class,args); } }
Controller層代碼
packagewebfluxdemo; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.http.HttpStatus; importorg.springframework.web.bind.annotation.GetMapping; importorg.springframework.web.bind.annotation.PathVariable; importorg.springframework.web.bind.annotation.RequestBody; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RequestParam; importorg.springframework.web.bind.annotation.ResponseStatus; importorg.springframework.web.bind.annotation.RestController; importorg.springframework.web.bind.annotation.RequestHeader; importorg.springframework.http.HttpHeaders; importwebfluxdemo.User; importwebfluxdemo.UserService; importio.jsonwebtoken.Jwts; importio.jsonwebtoken.Jws; importio.jsonwebtoken.Claims; importio.jsonwebtoken.SignatureAlgorithm; importio.jsonwebtoken.security.Keys; importjava.security.Key; importreactor.core.publisher.Flux; importreactor.core.publisher.Mono; @RestController @RequestMapping("/") publicclassUserController{ @Autowired UserServiceuserService; privateSignatureAlgorithmsa=SignatureAlgorithm.HS256; privateStringjwtSecret=System.getenv("JWT_SECRET"); @GetMapping("/") @ResponseStatus(HttpStatus.OK) publicMonogetUserById(@RequestHeader(HttpHeaders.AUTHORIZATION)StringauthHdr){ StringjwtString=authHdr.replace("Bearer",""); Claimsclaims=Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); returnuserService.findById((String)claims.get("email")); } }
接口類
packagewebfluxdemo; importorg.springframework.data.r2dbc.repository.R2dbcRepository; importorg.springframework.stereotype.Repository; importwebfluxdemo.User; publicinterfaceUserRepositoryextendsR2dbcRepository{ }
Service層代碼
packagewebfluxdemo; importjava.util.Optional; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Service; importwebfluxdemo.User; importwebfluxdemo.UserRepository; importreactor.core.publisher.Flux; importreactor.core.publisher.Mono; @Service publicclassUserService{ @Autowired UserRepositoryuserRepository; publicMonofindById(Stringid){ returnuserRepository.findById(id); } }
結果
為了評估性能,我們進行了一系列嚴格的測試。每個測試由100萬個請求組成,我們評估了它們在不同并發連接級別(50、100和300)下的性能。
現在,讓我們深入研究結果,以圖表形式呈現:
所用時間對比 每秒請求數 最小延遲 10%延遲 25%延遲 平均延遲 中位數延遲 75%延遲 90%延遲 99%延遲 最高延遲 平均CPU使用率 平均內存使用率
分析
在此設置中,即使用MySQL驅動程序時,虛擬線程提供的性能最低、Webflux保持遙遙領先。
審核編輯:湯梓紅
-
JAVA
+關注
關注
19文章
2966瀏覽量
104702 -
開源
+關注
關注
3文章
3309瀏覽量
42471 -
MySQL
+關注
關注
1文章
804瀏覽量
26530 -
線程
+關注
關注
0文章
504瀏覽量
19675 -
SpringBoot
+關注
關注
0文章
173瀏覽量
177
原文標題:SpringBoot 物理線程、虛擬線程、Webflux 性能全面對比!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論