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

本贴最后更新于 246 天前,其中的信息可能已经时移世异

原文发布于:【翻译】性能之争:Web MVC vs Webflux,欢迎使用 RSS 订阅获取最新更新。

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

Gatling test execution for web mvc

The spring webflux :

Gatling text execution for 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

Web MVC startup time

Spring WebFlux 启动时间:

Webflux startup time

Webflux startup time

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

Web MVC JVM utilization

Web MVC JVM utilization

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

Webflux JVM utilization

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

threads created for web MVC

threads created for web MVC

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

threads created for webflux

threads created for webflux

5. 重复测试

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

Summary of the tests

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

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?

一些高赞的回答

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...