Spring Boot 强化性能提升速度

对不起,我没有标题。 根据要求,我进行了Web MVC和WebFlux性能的比较。WebFlux与Web MVC相同,可使用Vue.js、Thymeleaf等进行屏幕呈现以及在REST API中使用。通过使用Reactor的持续传递风格(CPS)和函数式编程,WebFlux实现了非同步的非阻塞处理,可以使用较少的线程进行并发处理,并能够在较少的硬件资源上进行扩展的Web堆栈。

结局

result.png

如果服务器端的处理时间为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

如果将来的Project Loom可以不需要特别意识到虚拟线程,并且能够将其作为线程处理,那么Spring Boot的默认最大线程数可能会增加到20万,这样无论是非阻塞还是继续,我们也不需要考虑,只需使用类似Web MVC这样的简洁普通代码就能够处理资源消耗较少。

需要以非阻塞方式编写

有时候,我会不小心编写会阻塞代码的情况。在编写测量代码时,我甚至会不经意地编写了 `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)。

r2dbc_chart2.png

HTTP 网络输入/输出

不一定要使用WebFlux,也不必使用RestTemplate,可以使用WebClient。我们在这次测量中也使用了它。

2023/11/02 补记
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.+'
}

自信

swagger.png
广告
将在 10 秒后关闭
bannerAds