电子说
来源:丛林 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> desc users; +--------+--------------+------+-----+---------+-------+ | 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 | | +--------+--------------+------+-----+---------+-------+ 6 rows in set (0.00 sec)
用户数据库已准备好包含 100000 条用户记录的初始数据集。
mysql> select count(*) from users; +----------+ | count(*) | +----------+ | 99999 | +----------+ 1 row in set (0.01 sec)
在我们对 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
实体类
package com.example.demo; import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity @Table(name = "users") public class User { @Id private String email; private String first; private String last; private String city; private String county; private int age; public String getId() { return email; } public void setId(String email) { this.email = email; } public String getFirst() { return first; } public void setFirst(String name) { this.first = name; } public String getLast() { return last; } public void setLast(String name) { this.last = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCounty() { return county; } public void setCounty(String county) { this.county = county; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
启动类
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; import org.springframework.context.annotation.Bean; @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
Controller层
package com.example.demo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Autowired; import java.util.Optional; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Claims; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.Key; import com.example.demo.UserRepository; import com.example.demo.User; @RestController public class UserController { @Autowired UserRepository userRepository; private SignatureAlgorithm sa = SignatureAlgorithm.HS256; private String jwtSecret = System.getenv("JWT_SECRET"); @GetMapping("/") public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) { String jwtString = authHdr.replace("Bearer",""); Claims claims = Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); Optionaluser = userRepository.findById((String)claims.get("email")); return user.get(); } }
接口类
package com.example.demo; import org.springframework.data.repository.CrudRepository; import com.example.demo.User; public interface UserRepository extends CrudRepository{ }
Springboot(虚拟线程)
其余代码基本照搬上述 「物理线程」 , 启动类修改如下:
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; import org.springframework.context.annotation.Bean; import java.util.concurrent.Executors; @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } @Bean public TomcatProtocolHandlerCustomizer> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { 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
启动类
package webfluxdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; import org.springframework.web.reactive.config.EnableWebFlux; import io.r2dbc.spi.ConnectionFactory; @EnableWebFlux @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
Controller层代码
package webfluxdemo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.http.HttpHeaders; import webfluxdemo.User; import webfluxdemo.UserService; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Claims; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.Key; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController @RequestMapping("/") public class UserController { @Autowired UserService userService; private SignatureAlgorithm sa = SignatureAlgorithm.HS256; private String jwtSecret = System.getenv("JWT_SECRET"); @GetMapping("/") @ResponseStatus(HttpStatus.OK) public MonogetUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) { String jwtString = authHdr.replace("Bearer",""); Claims claims = Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); return userService.findById((String)claims.get("email")); } }
接口类
package webfluxdemo; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import webfluxdemo.User; public interface UserRepository extends R2dbcRepository{ }
Service层代码
package webfluxdemo; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import webfluxdemo.User; import webfluxdemo.UserRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service public class UserService { @Autowired UserRepository userRepository; public MonofindById(String id) { return userRepository.findById(id); } }
结果
为了评估性能,我们进行了一系列严格的测试。每个测试由100万个请求组成,我们评估了它们在不同并发连接级别(50、100和300)下的性能。
现在,让我们深入研究结果,以图表形式呈现:
所用时间对比
每秒请求数
最小延迟
10%延迟
25%延迟
平均延迟
中位数延迟
75%延迟
90%延迟
99%延迟
最高延迟
平均CPU使用率
平均内存使用率
分析
在此设置中,即使用MySQL驱动程序时,虚拟线程提供的性能最低、Webflux保持遥遥领先。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !