跳到主要内容

【翻译】性能之争:Web MVC vs Webflux

1. 前言

最近在学响应式编程的时候,突然让我想到,新的编程范式就一定会比传统的编程范式好吗?响应式编程的性能提升在哪个方面的呢?

文章以下内容翻译来源于:Spring Benchmark – Web MVC vs Webflux

作者粗略的比较了传统的Spring MVC和WebFlux的性能特点,因为比较的内容只是一个简单的hello world接口,而现实生活中的业务请求复杂度会高很多。

原文博客时间在2022年8月,还是比较有参考价值的。 如果你也使用notion,可以使用这个链接直接收藏。

2. 正文

在Java的世界中,每个处理过程从设计上来说都是一个基于CPU线程的概念,通过阻塞操作和命令式代码来实现。因此,最初的Java Web服务器采用了每个请求一个线程的方式,遵循Servlet规范。然而,基于线程的编程方式存在一些限制,因为CPU一次只能处理有限数量的线程,大约在一万个左右。

在当今的商业环境中,某些与互联网相连的业务应用需要支持百万级的吞吐量,这已经成为一个关键需求。如果我们继续采用每个请求一个线程的方式,CPU就需要活跃地运行大量线程来处理请求。但是云主机通常提供1-2个CPU,甚至只有不到1个CPU的资源。因此,出现了基于异步事件循环、非阻塞的Web服务器,以较少的线程处理并发,并且能够在较少的硬件资源下进行扩展。

我一直想尝试使用异步Web服务器构建Java Web服务应用,但直到现在仍然没有机会,因为在我的工作场所,我们并没有处理那么高的流量。

因此,我进行了一个快速尝试,比较了基于每个请求一个线程的引擎和基于异步Web服务器的性能差异。我使用了Spring Web MVC和Spring WebFlux进行简单快速的比较。Spring Web MVC基于Servlet Web服务器,而WebFlux基于异步的Netty。

以下是我用于运行比较的框架版本、库和工具:

  • JDK:18.0.1 64位 Temurin Eclipse Adoptium
  • org.springframework.boot:spring-boot-starter-parent 版本:2.7.2
  • io.gatling:gatling-maven-plugin 版本:4.2.4
  • io.gatling.highcharts:gatling-charts-highcharts 版本:3.8.3
  • visualvm:2.1.4

我在一台装有英特尔第12代 Core i7 处理器(20个CPU线程)的机器上运行了测试。

3. 项目结构

首先,创建一个父项目来存放通用库的版本。

<project ....>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>web</module>
<module>webflux</module>
<module>gatling</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>spring-web-vs-webflux</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<java.version>18</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

然后我们创建一个简单的子项目用于Spring Web MVC:

<project ....>
<parent>
<artifactId>spring-web-vs-webflux</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

带有基本主类:

package org.example.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Bayu Utomo
* @date 16/8/2022 9:51 pm
*/
@SpringBootApplication
public class SpringWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringWebApplication.class, args);
}
}

还有一个接口:

package org.example.web;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Bayu Utomo
* @date 16/8/2022 10:03 pm
*/
@RestController
public class MainController {
@GetMapping("/test")
public ResponseEntity<String> getPerson() {
return ResponseEntity.ok().body("Test OK!");
}
}

接下来,我们创建另一个简单的子项目用于Spring WebFlux:

<project ....>
<parent>
<artifactId>spring-web-vs-webflux</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>webflux</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

main class:

package org.example.webflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Bayu Utomo
* @date 16/8/2022 9:51 pm
*/
@SpringBootApplication
public class SpringWebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(SpringWebfluxApplication.class, args);
}
}

controller

package org.example.webflux;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Bayu Utomo
* @date 16/8/2022 10:03 pm
*/
@RestController
public class MainController {
@GetMapping("/test")
public ResponseEntity<String> getPerson() {
return ResponseEntity.ok().body("Test OK!");
}
}

WebFlux和Web MVC都将使用它们的默认应用程序属性。正如所看到的,WebFlux提供了与控制器路由相关的相同注释机制,无需实现Mono和指定路由的功能方式。

这里我们没有进行JSON处理,因为我想将比较仅限于HTTP引擎处理本身。

最后,我们为性能测试创建Gatling子项目:

<project ....>
<parent>
<artifactId>spring-web-vs-webflux</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gatling</artifactId>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>3.8.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>4.2.4</version>
<configuration>
<simulationClass>gatling.BasicSimulationTest</simulationClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

然后我们创建一个模拟类:

package gatling;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.core.loop.Repeat;
import io.gatling.javaapi.http.*;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
/**
* @author Bayu Utomo
* @date 17/8/2022 9:44 am
*/
public class BasicSimulationTest extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("http://localhost:{givePortNumberAsNeeded}")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // 6
.doNotTrackHeader("1")
.acceptLanguageHeader("en-US,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0");
ScenarioBuilder scn = scenario("BasicSimulation")
.repeat(100)
.on(exec(http("spring_{giveSpecificNameAsNeeded}")
.get("/test")
.check(status().is(200)))
);
{
setUp(
scn.injectOpen(atOnceUsers(10000))
).protocols(httpProtocol);
}
}

正如所看到的,我们注入了10000个用户。每个用户将调用接口100次。因此,我们将有1000000个HTTP请求。我们在这里不会使用任何暂停、睡眠时间或限制执行时间。因此,我们会以最快的速度把请求发送到服务器。

4. 测试部分

执行测试的结果如下所示。

The spring web MVC :

Gatling test execution for web mvc

The spring webflux :

Gatling text execution for webflux

Gatling测试在WebFlux上稍微花费了更长的时间。注意,并不意味着WebFlux运行速度更慢。

从上面的表格中可以看出,Web Servlet可以完成更多的请求(显示OK的那一列),因为它有更多的线程(Spring默认设置为200个线程)来处理传入的请求。而WebFlux能够处理较少的请求。但是,如果您查看响应时间,总体而言,与Web MVC相比,WebFlux能够提供更快的响应时间。因为CPU线程中的上下文切换速度较慢,而事件循环编程则更快。即使线程较少,Web Servlet和WebFlux之间的差距仅为5%。此外,所有的KO都是因为连接被拒绝,意味着队列已满。由于有更多的线程可以从队列中获取请求,所以Web MVC的吞吐量更高。但是当CPU在不同线程之间切换时,与异步WebFlux相比,这会导致更长的处理时间来完成请求。由于传入请求立即被拒绝,所以WebFlux的吞吐量较低,这是因为队列已满。增加队列的大小,或许WebFlux可以处理更多的吞吐量。

两者的启动时间大致相似。

Spring Web MVC 启动时间:

Web MVC startup time

Spring WebFlux 启动时间:

Webflux startup time

下面是显示Spring Web MVC的JVM利用率的数据:

Web MVC JVM utilization

下面是显示Spring WebFlux的JVM利用率的数据:

Webflux JVM utilization

从上面的JVM利用率表格中可以看出,与Spring Web MVC相比,WebFlux的CPU峰值较为稳定。与Spring Web MVC相比,WebFlux还使用了较少的堆大小。

与Spring Web MVC相比,WebFlux使用的线程数量也要少得多(20个CPU线程 * 2 = 40),而Spring Web MVC使用了更多的线程(219个)。

对于Web Servlet MVC,会创建许多NIO线程来处理请求:

threads created for web MVC

threads created for web MVC

WebFlux在请求处理方面使用了最小数量的线程:

threads created for webflux

5. 重复测试

我按照第一次测试的相同规格重复执行了测试。然后第三次,我将Web Servlet MVC的线程数减少到与WebFlux大致相同的数量。以下是结果的摘要:

Summary of the tests

总体而言,与Web Servlet相比,WebFlux能够在更少的资源下处理几乎相同的吞吐量。WebFlux的响应时间也比Web Servlet更快。如果对WebFlux进行更大的队列配置,可能可以减少拒绝连接的数量。

第三次测试显示,如果我们拥有一个具有较高CPU数量的机器(记住我使用的是第12代英特尔处理器),较少的线程将会为Web Servlet提供更好的性能。但也会需要更多的堆内存,因为更多的请求将存储在队列中。但在云主机上,CPU非常有限。因此,对于Web Servlet来说,线程较少并不一定意味着比WebFlux(异步Web服务器)更快或更好。需要进一步进行测试来验证这一点。

从第三次测试中,NIO线程的数量减少了:

NIO threads from the third test were reduced

6. 总结

总的来说,使用异步Web服务器并不一定意味着比基于RPS/Servlet的Web服务器更快或具有更高的吞吐量。它意味着能够在更少的资源下处理大致相同的吞吐量。

TechEmpower进行了更好的基准测试,他们展示了其他异步Web服务器(如Vert.x)具有更好/更高的吞吐量性能。只是对于Spring WebFlux来说,尽管它是构建在Netty之上的(与Vert.x相同),但性能似乎低于通常异步Web服务器可以实现的水平。

正如所看到的,Spring-Tomcat比Spring WebFlux更快,尽管差距不是很大。

但异步Web服务器有一个问题。编程风格不再是Java设计的命令式风格。调试异步代码变得困难。或许等Java Loom被许多基于TPR的Web服务器正式采用,届时我们将再看看基准测试结果。

7. 扩展部分

以下内容已经不属于原来博客的内容。

在上面的比较结果中,似乎WebFlux落于下风,没有带来更好的性能,以及在TechEmpower的测试上webflux排名已经到了375,但是我在Reddit上又找到了一些不一样观点。

原帖:Why so much hate for Webflux?

一些高赞的回答