【翻译】性能之争: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?

一些高赞的回答

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×