Spring Boot 强化性能提升速度
对不起,我没有标题。 根据要求,我进行了Web MVC和WebFlux性能的比较。WebFlux与Web MVC相同,可使用Vue.js、Thymeleaf等进行屏幕呈现以及在REST API中使用。通过使用Reactor的持续传递风格(CPS)和函数式编程,WebFlux实现了非同步的非阻塞处理,可以使用较少的线程进行并发处理,并能够在较少的硬件资源上进行扩展的Web堆栈。
结局
如果服务器端的处理时间为50毫秒,那么WebFlux在10,000个请求下的正常响应时间为1.7秒。而Web MVC在默认的200个线程情况下,处理10,000个请求的理论最快时间为2.9秒。因此,可以认为WebFlux的性能是合理的。当将服务器端的处理时间设置为1秒时,尝试处理10,000个请求时,Web MVC出错无法处理,而WebFlux则在20秒内完成了处理。
环境和条件 hé
-
- 2 年ぐらい前の MacBook Pro
-
- 念の為、サーバとクライアントは別 VM で起動 (同じ VM でも結果は変わらんかった)
-
- クライアントは Spring WebClient で localhost に http アクセス
-
- Gatling や JMeter より WebClient のほうが多くの処理が可能
-
- 設定は Spring Boot デフォルト (なんも指定なし、バージョンは build.gradle 参照)
- サーバは Netty だと同時 700 リクエストあたりからエラーが発生するため Tomcat
你要选哪一个?
如果具备以下要求或理由,推荐使用WebFlux。
如果有以下要求或原因,则建议使用WebFlux。
-
- 同時実行性能が高くないと困る
-
- クラウドの費用をとにかく安くしたい (PG コスト高いかも)
-
- リアクティブプログラミングをやっときたい
- for 文禁止 Stream 推進派の人 (ちゃう?)
当WebFlux于2017年首次问世时,由于JDBC会阻塞处理,因此希望观望的人也很多。然而,现在可以选择非阻塞的数据库驱动选项(稍后会提到),并且已在Line和Cookpad等公司中被采用,其实绩方面也没有问题。然而,相对来说,除了相对较易处理的异步处理线程外,还需要理解非阻塞异步处理、传递式延续、背压等机制,并且通常会增加开发成本,降低可维护性等缺点。不管怎样,作为目前使用Spring Boot的开发人员,最好使其能够同时适应两种情况。
Spring WebFlux 概述
https://spring.pleiades.io/spring/docs/current/spring-framework-reference/web-reactive.html#header-spring
需要以非阻塞方式编写
有时候,我会不小心编写会阻塞代码的情况。在编写测量代码时,我甚至会不经意地编写了 `sleep` 函数,结果发现有问题。我们需要使用类似下面这样的非阻塞 API。在使用 Java 的情况下,就像使用服务器端的 node.js 一样,虽然结果不会很糟糕,但对于采用 WebFlux 的意义会变得模糊。
数据库输入/输出
普通的JDBC驱动会在等待SQL结果时阻塞。然而,如果将DB访问放在一个单独的线程中进行异步执行,最终会导致线程资源问题。因此,各个DB供应商提供了以非阻塞方式进行标准化的R2DBC驱动程序。Spring Data系列提供了通过通用存储库自动生成SQL的方法,但是在DatabaseClient类中可以直接编写SQL,并且可以通过流式处理结果,相比JdbcTemplate有明显改进。
Spring Data R2DBC 驱动程序(SQL Server、MySQL、Postgres、H2)
https://spring.pleiades.io/spring-data/r2dbc/docs/current/reference/html/#r2dbc.drivers
@Autowired DatabaseClient db;
// INSERT
db.insert().into(Person.class).using(person).then();
db.execute("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34).then();
// UPDATE
db.update().table(Person.class).using(person).then();
db.execute("UPDATE person SET name = 'Joe'").then();
// SELECT
String sql = "SELECT id, name FROM person WHERE 〜";
Flux<Person> all = db.execute(sql).as(Person.class).fetch().all();
Mono<Person> all = db.execute(sql).as(Person.class).fetch().one();
R2DBC 在 Web MVC 中的性能表现比 JDBC 更具并发能力。
在下述验证中,我们发现即使在普通的Web MVC项目中,相较于JDBC,R2DBC的吞吐量更高(我在我的MVC项目中也使用了R2DBC)。
HTTP 网络输入/输出
不一定要使用WebFlux,也不必使用RestTemplate,可以使用WebClient。我们在这次测量中也使用了它。
Spring Boot 3.2 (Spring 6.1) 将加入RestClient。这是一个具有流畅API的同步客户端。虽然 WebFlux 不需要使用响应式技术,但还是需要添加相关依赖,使用起来并不方便。然而,RestClient 则以虚拟线程为基础,并且将成为主要的HTTP客户端。
Spring 5.0时,RestTemplate已经进入维护模式,只接受变更和错误的轻微请求。请考虑使用提供最新API、支持同步、异步和流媒体场景的WebClient。
https://spring.pleiades.io/spring/docs/current/spring-framework-reference/web.html#webmvc-resttemplate
Spring WebClient是如何使用的
https://spring.pleiades.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client
Mono<Person> result = WebClient
.create("https://example.org")
.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
测量代码
目标控制器类
除了测试外,这个实现只有一个类,application.properties/yml 文件为空(默认值请参考Spring Boot的共有应用程序属性列表)。如果返回值是Mono或Flux,则使用WebFlux;否则使用Web MVC。参数”wait”是指在进行数据库访问等操作时所需的预计等待时间。
package demo;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@SpringBootApplication
@RestController
public class PerformanceController {
public static void main(String[] args) {
SpringApplication.run(PerformanceController.class, args);
}
@GetMapping("/webmvc")
String webmvc(long wait) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(wait);
return "Hello World";
}
@GetMapping("/webflux")
Mono<String> webflux(long wait) {
return Mono.just("Hello World").delayElement(Duration.ofMillis(wait));
}
}
考试代码
使用WebClient在非阻塞模式下一次性对http://localhost进行一万次访问。首先,循环部分本来是使用Flux.range和全局等待的方式编写的,但是由于CountDownLatch稍微快一点,所以改为了for循环。使用CountDownLatch总觉得有点输。
package demo;
import static org.assertj.core.api.Assertions.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AbstractStringAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
import com.google.common.base.Stopwatch;
// 計測用にサーバと別 VM 起動するためコメントアウト (有効にするとサーバも起動してくれる)
// @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
class PerformanceControllerTests {
@Test
void test() throws InterruptedException {
proc("webmvc" , 0, 50, 100, 200, 500);
proc("webflux", 0, 50, 100, 200, 500, 1000);
}
void proc(String type, int... waitList) throws InterruptedException {
final int COUNT = 10000;
for (int serverWait : waitList) {
String path = "/" + type + "?wait=" + serverWait;
call(path, COUNT);
Stopwatch watch = Stopwatch.createStarted();
call(path, COUNT);
if (serverWait == 0) continue;
System.out.printf("処理時間 %-7s サーバ %4d ms/回, クライアント %8s/%d回\n",
type, serverWait, watch, COUNT);
}
}
void call(String path, int loopCount) throws InterruptedException {
WebClient web = WebClient.create("http://localhost:8080");
RequestHeadersSpec<?> request = web.get().uri(path);
AbstractStringAssert<?> assertResponse = assertThat("Hello World");
CountDownLatch latch = new CountDownLatch(loopCount);
for (int i = 0; i < loopCount; i++) {
request
.retrieve()
.bodyToMono(String.class)
.doOnError(Assertions::fail)
.doOnTerminate(latch::countDown)
.subscribe(assertResponse::isEqualTo);
}
latch.await(1, TimeUnit.MINUTES);
}
}
创建.gradle文件
与Spring MVC的区别仅在于将spring-boot-starter-web更改为spring-boot-starter-webflux。如果要使用WebFlux而不是标准的Netty,或者同时使用MVC和WebFlux,则两者都要写入代码。库的版本通常不需要指定,因为它们已包含在Spring Boot的依赖管理中。写buildscript是为了在Spring Boot的版本指定中使用通配符。建议总是升级微版本。
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.+")
}
}
apply plugin: 'org.springframework.boot' // バージョンは上記でワイルドカード指定
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
test {
useJUnitPlatform()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' // Tomcat を使う
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springdoc:springdoc-openapi-ui:1.2.+' // Swagger (下記スクショ)
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.assertj:assertj-core'
testImplementation 'com.google.guava:guava:28.+'
}