Web应用和Project Loom

工程 | Mark Thomas | 2023年2月27日 | ...

引言

Project Loom 旨在为 JRE 带来“易用、高吞吐量、轻量级并发”。Project Loom 引入的一项特性是虚拟线程。在这篇博文中,我们将通过部署在 Apache Tomcat 上的几个简单 Web 应用,探讨虚拟线程对 Web 应用的意义。

高吞吐量 / 轻量级

第一个实验比较了使用 Tomcat 标准线程池所带来的开销与使用基于虚拟线程 (Loom) 的执行器所带来的开销。本文末尾详细介绍了使用的测试环境。我们使用平均每秒请求数来考察不同响应大小和请求并发度下的性能。结果如下图所示。

loom-results-01

结果表明,通常,创建新虚拟线程来处理请求的开销小于从线程池获取平台线程的开销。

在线程池测试中看到的一个意外结果是,对于较小的响应体来说,更显著的是,2 个并发用户导致的平均每秒请求数少于单个用户。调查发现,额外的延迟发生在任务被传递给 Executor 到 Executor 调用任务的 run() 方法之间。对于 4 个并发用户,这种差异减小了;对于 8 个并发用户,这种差异几乎消失了。

在高并发水平下,当并发任务数超过可用的处理器核心数时,虚拟线程执行器再次表现出更高的性能。这在使用较小响应体的测试中更加明显。

易用性

第二个实验比较了使用 Servlet 异步 I/O 和标准线程池所获得的性能与使用简单阻塞 I/O 和基于虚拟线程的执行器所获得的性能。虚拟线程在这里的潜在好处是简单性。阻塞读写比等效的 Servlet 异步读写要简单得多——尤其是在考虑错误处理时。

Servlet 异步 I/O 通常用于访问响应延迟较大的外部服务。测试 Web 应用在 Service 类中模拟了这一点。与基于虚拟线程的执行器一起使用的 Servlet 以阻塞方式访问服务,而与标准线程池一起使用的 Servlet 使用 Servlet 异步 API 访问服务。其中不涉及任何网络 I/O,但这不应该影响结果。

最初的测试结果毫不奇怪地显示,由于时间主要由 5 秒的延迟决定,阻塞方式和异步方式之间没有可衡量的差异。为了在没有延迟影响的情况下探究差异,将延迟减少到零,并执行了一组类似于吞吐量测试的测试。结果显示在下图中。

loom-results-02

我们再次看到,虚拟线程通常性能更高,这种差异在低并发度下以及当并发度超过测试环境可用的处理器核心数时最为显著。

分析

基于虚拟线程的执行器与 Tomcat 标准线程池之间的差异并没有图表上看起来那么鲜明。这些测试旨在检查每种方法相关的开销,并且不代表实际应用场景。在实际应用中,测试中显示的差异与完成一个请求所需的时间相比,可能可以忽略不计。

Tomcat 标准线程池和基于虚拟线程的执行器之间性能差异的主要驱动因素是向线程池队列添加和移除任务时的争用。通过优化 Tomcat 当前使用的实现,有可能减少标准线程池队列中的争用,并提高吞吐量。

影响相对性能的次要因素是上下文切换。这可能是第二个实验中当并发度超过可用处理器核心数时观察到的性能差异的原因,因为虚拟线程的上下文切换开销小于标准线程池中的线程。

结论

使用基于虚拟线程的执行器是 Tomcat 标准线程池的一个可行替代方案。在容器开销方面,切换到虚拟线程执行器带来的好处是微不足道的。

在 Tomcat 上运行的经典 Spring MVC 等会遇到阻塞的 Web 应用,并且尚未切换到 Servlet 异步 API、响应式编程或其他异步 API 的,通过切换到基于虚拟线程的执行器应该会看到一些可扩展性方面的改进。根据 Web 应用的不同,这些改进可能无需更改 Web 应用代码即可实现。

已经切换到使用 Servlet 异步 API、响应式编程或其他异步 API 的 Web 应用,通过切换到基于虚拟线程的执行器,不太可能观察到可衡量的差异(正面或负面)。

从长远来看,虚拟线程的最大好处似乎是应用代码更简单。目前需要使用 Servlet 异步 API、响应式编程或其他异步 API 的一些用例,将能够通过使用阻塞 I/O 和虚拟线程来实现。需要注意的是,应用通常需要对不同的外部服务进行多次调用。这最有效地方法是并行执行,虽然像 Project Reactor 这样的框架为此提供了第一类支持,但 JRE 中与此等效的解决方案(结构化并发)仍处于孵化阶段,并且仅旨在协调多个 future,而不是以最便捷的方式相互声明或组合它们。

最后,Project Loom 仍处于预览模式。现在考虑在生产环境中使用虚拟线程还为时过早,但现在是时候将 Project Loom 和虚拟线程纳入您的规划中,以便在虚拟线程在 JRE 中正式可用时做好准备。

测试环境

测试环境包括以下内容

测试是在一台完全更新的 Ubuntu 22.04.1 LTS 机器上进行的,该机器配备 Intel i7-6950X 处理器和 32 GB 内存。

为了最大化测试之间的差异可见性,对默认配置进行了以下更改以最小化公共开销

  • 在单台机器上使用回环接口运行测试以最小化网络开销
  • 禁用访问日志,因为在高请求量下它是重要的磁盘 I/O 源
  • 将 maxKeepAliveRequests 设置为 -1 以减少建立和拆除 TCP 连接所花费的时间

测试 Web 应用 也被设计用于最小化公共开销并突出测试之间的差异。

使用的 server.xml 文件为

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />

  <Service name="Catalina">

    <Executor
        className="org.apache.catalina.core.LoomExecutor"
        name="loomExecutor"
        />

    <Connector 
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        port="8080"
        maxKeepAliveRequests="-1"
        />

    <Connector
        executor="loomExecutor"
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        port="8081"
        maxKeepAliveRequests="-1"
        />

    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

使用的 setenv.sh 文件为

#!/bin/sh
JAVA_OPTS=--enable-preview

订阅 Spring 时事通讯

保持与 Spring 时事通讯的联系

订阅

抢先一步

VMware 提供培训和认证,助您快速进步。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部