无需烦恼即可获得强大的性能
这篇博文的目的是为了消除对 RESTEasy Reactive 的一些困惑,并回答一些围绕它的常见问题。
确认
如果没有 Clement Escoffier 和 Stéphane Épardaud 的专家建议,这篇博文是不可能完成的。
命令式和响应式:电梯演讲
为了理解 RESTEasy Reactive 的重要性以及它与 RESTEasy Classic 的区别,我们先来概括一下 此处 首次提出的一个非常重要的信息。
总的来说,Java Web 应用程序使用命令式编程结合阻塞式 IO 操作。这种方式非常受欢迎,因为它更容易理解代码。事情是按顺序执行的。当应用程序收到一个请求时,框架会将此请求与一个工作线程关联起来。当请求处理需要与数据库或其他远程服务交互时,它会依赖于阻塞式 IO。线程被阻塞,等待响应,使通信成为同步的。在这种模型下,一个请求不会影响到另一个请求,因为它们在不同的线程上运行。即使一个线程在等待,其他在不同线程上运行的请求也不会显著减慢。
然而,在这种模型下,每个并发请求都需要一个线程,这限制了可实现的并发度。另一方面,响应式执行模型采用了异步开发模型和非阻塞 IO。在这种模型下,同一个线程可以处理多个请求。当请求的处理无法继续进行时(例如,因为它请求了一个远程服务,或与数据库交互),它会使用非阻塞 IO。它不会阻塞线程,而是调度该操作并传递一个续集,该续集将在操作完成后被调用[1]。这会立即释放线程,该线程可以用于处理另一个请求。当 IO 操作的结果可用时,请求的处理会恢复并继续执行。
这种模型允许使用单个 IO 线程来处理多个请求。有三个显著的好处。
-
首先,响应时间更短,因为它不必切换到另一个线程。
-
其次,它减少了内存消耗,因为它减少了线程的使用。
-
第三,您的并发度不再受线程数量的限制。
响应式模型更有效地利用硬件资源,但是……一个严重的陷阱潜伏着。如果请求的处理开始阻塞,事情会很快变得糟糕,因为没有其他请求可以被处理。为了避免这种情况,您需要学习如何编写异步和非阻塞代码,如何调度操作,如何编写续集,如何链式调用。基本上,我们需要一种方法来构建异步处理,并使用非阻塞 IO。毫无疑问,这构成了一个范式的转变。在 Quarkus 中,我们希望使这种转变尽可能容易,因此 RESTEasy Reactive 允许您选择一个端点是阻塞的还是非阻塞的(应用程序可以自由地随意混合和匹配阻塞和非阻塞方法)。所以不要被“响应式”这个词吓倒,基础设施是响应式的,但您的代码可以是响应式的,也可以是命令式的。这就是我们所说的响应式和命令式的统一。
从 RESTEasy Reactive 的角度来看这意味着什么
RESTEasy Reactive 默认在 IO 线程(也称为事件循环线程)上处理每个 HTTP 请求[2]。
下图展示了高层次的图景

这可以确保达到最大吞吐量,但这也意味着端点方法的实现应该及时完成,否则线程将被使用过长时间[3],其他请求将被排队,并导致吞吐量下降。
重要的是要理解,一个仅使用命令式代码的方法体只有在执行时间过长时才会成为问题——对于阻塞式 IO 操作几乎总是如此。
因此,当方法体执行某种阻塞式 IO 操作(甚至是一些需要时间完成的 CPU 密集型操作)时,需要将请求卸载到工作线程。在 RESTEasy Reactive 中,这是通过 `@Blocking` 注解以声明式的方式完成的——不需要响应式编程或复杂的 Java 并发相关代码。当您尝试在 IO 线程上使用阻塞式 IO 时,Quarkus 也会警告您。但是,如果方法体执行非阻塞 IO(或一些完成得非常快的 CPU 密集型操作),那么 RESTEasy Reactive 可以继续在 IO 线程上处理整个请求。
RESTEasy Reactive 是否仅限于使用响应式 API?
绝对不是!
尽管 RESTEasy Reactive 是从头开始构建的,用于执行非阻塞 IO 并从事件循环线程处理请求(从而避免不必要地使用工作线程池),但它可以轻松地与阻塞 IO 和任何提供阻塞 API(如 Hibernate)的代码一起工作,而不会阻塞事件循环。
您需要做的就是将 `@Blocking` 添加到您的端点方法或类上。就是这样!如果您使用 `@Blocking`,您就回到了常规的调度机制:一个工作线程用于执行您的方法。
高层次来看,它看起来是这样的

RESTEasy Reactive 是否需要 Hibernate Reactive?
正如您可能从上一个问题的答案中猜到的那样,答案是否定的。
在 RESTEasy Reactive 与 Hibernate 一起使用的场景中,`@Blocking` 注解应该放在与 Hibernate 交互的端点方法上。
在 RESTEasy Reactive 与 Hibernate Reactive 一起使用的场景中,不需要在与 Hibernate Reactive 交互的端点方法上添加 `@Blocking` 注解。
使用 `@Blocking` 对性能有什么影响?
尽管当端点方法是非阻塞的时(即 HTTP 请求完全从事件循环线程提供服务),可以实现绝对最高的吞吐量,但即使使用了 `@Blocking`,仍然可以获得出色的性能。
在我们的基准测试中,我们看到使用 `@Blocking` 会将最大吞吐量降低约 30%[4]。
然而,使用 RESTEasy Reactive 中的 `@Blocking` 的端点方法仍然比使用 RESTEasy Classic 的相同方法实现了约 50% 的更高吞吐量。
为什么使用 `@Blocking` 的 RESTEasy Reactive 比 RESTEasy Classic 性能更好?
RESTEasy Reactive 能够通过以下方式获得相对于 RESTEasy Classic 的性能优势:
-
与 Eclipse Vert.x 在所有 IO 相关事务上进行非常紧密的集成。Vert.x 在 IO 操作方面得到了极好的优化,因此与它的紧密集成使 RESTEasy Reactive 能够受益于所有这些工作。您可能还记得,RESTEasy Classic 在 Quarkus 上也使用了 Vert.x,但在那种情况下,集成不够深入,因此无法充分利用 Vert.x 的强大功能。
-
将大量工作移至构建时。由于 RESTEasy Reactive 是从头开始构建以满足 Quarkus 的需求,因此它受益于与 Quarkus 的尽可能紧密的集成,并且可能是执行最多构建时工作的扩展。这反过来又可以创建服务每个请求的最佳数据管道,通过生成字节码来内联运行时操作,消除运行时反射(无论是调用方法还是确定类型)并减少内存分配来帮助 JIT 编译器。
-
避免使用 ThreadLocals,而是利用包含所有必要信息的上下文对象。ThreadLocals 是使数据可用于框架不同部分的便捷方法,但它们的频繁使用是有代价的,因此在 RESTEasy Reactive 中完全避免了它们。
-
以最佳方式利用 Arc 进行所有必要的注入。RESTEasy Classic 提供了一个抽象层来执行各种注入操作,而对于 Quarkus 的需求来说,这是完全不必要的,因为 Arc 以更好的性能提供了相同的功能。
与带有 Mutiny 的 RESTEasy Classic 相比如何?
您可能还记得,Quarkus 允许您在通过 `quarkus-resteasy-mutiny` 扩展使用 RESTEasy Classic 时使用 Mutiny 返回类型(Uni 和 Multi),因此您可能想知道这与使用 RESTEasy Reactive 相比如何。
关于 RESTEasy Classic 需要了解的主要一点是,它**始终**在工作线程上处理请求,因为它根本不使用事件循环概念。
下图最好地展示了这一点

因此,在使用 RESTEasy Classic 时,即使您返回的是 `Uni` 或 `Multi` 这样的响应式类型,初始请求仍然是在工作线程上处理的,并且虽然对库的调用可能导致非阻塞 IO,但一旦工作线程因等待 IO 而阻塞,RESTEasy Classic 就无法重用它。
因此,在 RESTEasy Classic 中使用响应式返回类型的优势是语法上的优势,而不是运行时上的优势——尽管使用了响应式类型,底层的硬件并没有被更有效地利用。
在使用 RESTEasy Reactive 返回 Mutiny 类型时,所有操作都在 IO 线程上进行(除非端点被注解为 `@Blocking`)。顺便说一句,您不需要外部扩展即可在 RESTEasy Reactive 中使用 Mutiny,它是内置的!
为了达到最佳性能,我必须使用新的 RESTEasy Reactive API 吗?
通过阅读 RESTEasy Reactive 文档,您很快就会遇到编写请求过滤器(@ServerRequestFilter)、响应过滤器(`@ServerResponseFilter`)和异常映射器(@ServerExceptionMapper)的新 API。您可能想知道它们的用法是否会像标准 JAX-RS API(`ContainerRequestFilter`、`ContainerResponseFilter` 和 `ExceptionMapper`)一样影响性能。
尽管新的 API 在使用 `@Context` 的情况下比使用旧 API 会带来微小的性能优势,但这种优势可以忽略不计,除非您试图榨取每一寸性能,否则不应担心。在为这两种 API 编写过滤器时,要记住的一件事是,建议使用 `org.jboss.resteasy.reactive.server.SimpleResourceInfo` 而不是 `javax.ws.rs.container.ResourceInfo`,因为后者会导致执行反射。
一个特殊的(尽管相当高级的)情况是,新的 API 在 `MessageBodyReader` 和 `MessageBodyWriter` 类的情况下确实会带来显着更好的性能。在读取 HTTP 请求和写入 HTTP 响应时,使用 ServerMessageBodyReader 和 `ServerMessageBodyWriter` 可以让 RESTEasy Reactive 优化服务请求的数据路径。
关于响应式路由呢?
Quarkus 已经提供了一种从 IO 线程处理 HTTP 请求的方法。响应式路由提供了一种声明式模型来实现 HTTP API。每个路由都可以通过 IO 线程(默认)或工作线程(使用 `@Blocking` 注解)调用。响应式路由可提供非常好的吞吐量和性能,正如 这篇 文章所强调的那样。响应式路由与 RESTEasy Reactive 相比如何?
我们从响应式路由收到的主要抱怨之一是关于开发模型:它与 RESTEasy 使用的模型非常不同。然而,响应式路由使我们能够验证在 Quarkus 之上使用端到端响应式模型的性能和效率优势。RESTEasy Reactive 可以看作是“下一代”:您获得了运行时优势,同时也使用了熟悉的开发模型。
总结
RESTEasy Reactive 是下一代 HTTP 框架。它统一了响应式(非阻塞 IO、异步 API)和命令式(得益于 `@Blocking` 注解)。它提高了原始性能,同时又不限制用户体验。其响应式/命令式二元性使其适合任何用例,从高并发 HTTP API 到更传统的事务性 CRUD 应用程序。
我们认为 RESTEasy Reactive 将在不久的将来成为 Quarkus 的默认 HTTP 层,并且我们完全致力于使其达到最佳性能水平,同时引入能激发开发者喜悦的新功能!
本着这种精神,我们希望这篇简短的博文能为您提供一些关于 RESTEasy Reactive 特别之处的见解,并消除您可能对其有的任何误解。