來源:叢林medium.com
大量的文章評估了一系列技術(shù)(包括 Node.js、Deno、Bun、Rust、Go、Spring、Python 等)在簡單的“hello world”場景中的性能。雖然這些文章獲得了好評,但有一個共同點(diǎn):忽略了現(xiàn)實(shí)場景開發(fā)中的復(fù)雜性 。
本文旨在通過現(xiàn)實(shí)場景的視角剖析各種技術(shù),在這種特殊情況下,我們深入研究以下常見用例:
從 authorization header 中提取一個JWT。
驗(yàn)證JWT并從聲明中提取用戶的電子郵件。
使用提取的電子郵件執(zhí)行MySQL查詢。
最后,返回用戶的記錄。
雖然這個場景看起來似乎也很簡單,但它概括了 Web 開發(fā)領(lǐng)域中經(jīng)常遇到的現(xiàn)實(shí)挑戰(zhàn)。
介紹
在本文中,我們將深入探討所有同級產(chǎn)品之間的友好比較,即具有「物理線程、虛擬線程和 Webflux 的 SpringBoot」 ,重點(diǎn)關(guān)注它們在特定用例場景中的性能。我們已經(jīng)探索了標(biāo)準(zhǔn) SpringBoot 應(yīng)用程序如何與 webflux 相媲美,但現(xiàn)在,我們引入一個關(guān)鍵的區(qū)別:
帶有虛擬線程的 Spring Boot
我們熟悉 SpringBoot,但有一點(diǎn)不同——它在虛擬線程而不是傳統(tǒng)的物理線程上運(yùn)行。虛擬線程是并發(fā)領(lǐng)域的游戲規(guī)則改變者。這些輕量級線程簡化了開發(fā)、維護(hù)和調(diào)試高吞吐量并發(fā)應(yīng)用程序的復(fù)雜任務(wù)。
雖然虛擬線程仍然在底層操作系統(tǒng)線程上運(yùn)行,但它們帶來了顯著的效率改進(jìn)。當(dāng)虛擬線程遇到阻塞 I/O 操作時,Java 運(yùn)行時會暫時掛起它,從而釋放關(guān)聯(lián)的操作系統(tǒng)線程來為其他虛擬線程提供服務(wù)。這個優(yōu)雅的解決方案優(yōu)化了資源分配并增強(qiáng)了整體應(yīng)用程序響應(yīng)能力。
考慮到這些有趣的設(shè)置,讓我們更深入地研究我們的性能比較。撰寫本文是為了解決最常見的請求之一,即查看物理、虛擬和 Webflux 在實(shí)際用例中的比較。
測試環(huán)境及軟件版本
我們的性能測試是在配備 16GB RAM 的 MacBook Pro M1 上進(jìn)行的,確保了可靠的測試平臺。用于這些測試的軟件堆棧包括:
SpringBoot 3.1.3(在Java 20上運(yùn)行)
啟用預(yù)覽模式以獲得虛擬線程的強(qiáng)大功能
jjwt用于JWT驗(yàn)證和解碼,增強(qiáng)我們應(yīng)用程序的安全性。
mysql-connector-java 用于執(zhí)行 MySQL 查詢,維護(hù)數(shù)據(jù)完整性和一致性。
負(fù)載測試和 JWT
為了評估我們的應(yīng)用程序在不同負(fù)載下的性能,我們使用了開源負(fù)載測試工具 Bombardier。我們的測試場景涉及預(yù)先創(chuàng)建的 100000 個 JWT 列表。在測試過程中,Bombardier 從該池中隨機(jī)選擇 JWT,并將它們包含在 HTTP 請求的授權(quán)標(biāo)頭中。
MySQL 數(shù)據(jù)庫架構(gòu)
用于這些性能測試的 MySQL 數(shù)據(jù)庫有一個名為 users 的表。該表設(shè)計(jì)有 6 列,足以模擬我們應(yīng)用程序中的真實(shí)數(shù)據(jù)交互,使我們能夠評估它們的響應(yīng)能力和可擴(kuò)展性。
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)
用戶數(shù)據(jù)庫已準(zhǔn)備好包含 100000 條用戶記錄的初始數(shù)據(jù)集。
mysql>selectcount(*)fromusers; +----------+ |count(*)| +----------+ |99999| +----------+ 1rowinset(0.01sec)
在我們對 SpringBoot 物理線程、虛擬線程和 Webflux 進(jìn)行友好性能評估的背景下,了解關(guān)鍵的數(shù)據(jù)關(guān)系至關(guān)重要。具體來說,在JSON Web Token(JWT)有效負(fù)載中,每個電子郵件條目直接對應(yīng)于存儲在 MySQL 數(shù)據(jù)庫中的一條用戶記錄。
代碼
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
實(shí)體類
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); } }
結(jié)果
為了評估性能,我們進(jìn)行了一系列嚴(yán)格的測試。每個測試由100萬個請求組成,我們評估了它們在不同并發(fā)連接級別(50、100和300)下的性能。
現(xiàn)在,讓我們深入研究結(jié)果,以圖表形式呈現(xiàn):
所用時間對比 每秒請求數(shù) 最小延遲 10%延遲 25%延遲 平均延遲 中位數(shù)延遲 75%延遲 90%延遲 99%延遲 最高延遲 平均CPU使用率 平均內(nèi)存使用率
分析
在此設(shè)置中,即使用MySQL驅(qū)動程序時,虛擬線程提供的性能最低、Webflux保持遙遙領(lǐng)先。
審核編輯:湯梓紅
-
JAVA
+關(guān)注
關(guān)注
19文章
2973瀏覽量
104907 -
開源
+關(guān)注
關(guān)注
3文章
3381瀏覽量
42604 -
MySQL
+關(guān)注
關(guān)注
1文章
820瀏覽量
26651 -
線程
+關(guān)注
關(guān)注
0文章
505瀏覽量
19715 -
SpringBoot
+關(guān)注
關(guān)注
0文章
174瀏覽量
187
原文標(biāo)題:SpringBoot 物理線程、虛擬線程、Webflux 性能全面對比!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論