8 异步和非阻塞
Ratpack 旨在实现“异步”和“非阻塞”请求处理。其内部 IO(例如 HTTP 请求和响应传输)都是以非阻塞方式执行的(感谢 Netty)。这种方法可以提高吞吐量,降低资源使用率,更重要的是,在负载下具有更可预测的行为。由于 Node.js 平台的出现,这种编程模型最近越来越流行。Ratpack 是基于与 Node.js 相同的非阻塞、事件驱动模型构建的。
异步编程以其棘手著称。Ratpack 的主要价值主张之一是,它提供了构建和抽象来驯服异步野兽,从而在保持实现简单的情况下提高性能。
1.8 与阻塞框架和容器的比较
Java Servlet API 是大多数 JVM Web 框架和容器以及大多数 JDK 的基础,它与同步编程模型有着本质上的联系。大多数 JVM 程序员都非常熟悉和习惯这种编程模型。在此模型中,当需要执行 IO 时,调用线程将简单地休眠,直到操作完成且结果可用。此模型需要一个相当大的线程池。在 Web 应用程序环境中,这通常意味着每个请求都绑定到大型线程池中的一个线程,并且应用程序可以处理“X”个并行请求,其中“X”是线程池的大小。
Servlet API 的 3.0 版本确实支持异步请求处理。但是,将异步支持作为一种可选功能进行改造与完全异步方法是截然不同的。Ratpack 从一开始就是异步的。
这种模型的好处是,同步编程毫无疑问“更简单”。这种模型的缺点,与非阻塞模型相比,是它需要更多的资源使用并导致更低的吞吐量。为了并行处理更多请求,需要增加线程池的大小。这会为计算资源造成更大的争用,并且更多周期会浪费在管理这些线程的调度上,更不用说内存消耗的增加。现代操作系统和 JVM 非常擅长管理这种争用;但是,它仍然是可扩展性的瓶颈。此外,它需要更大的资源分配,这对于现代按需付费的部署环境来说是一个严重的问题。
异步、非阻塞模型不需要大型线程池。这是可能的,因为线程永远不会阻塞以等待 IO。如果需要执行 IO,调用线程将注册某种回调,该回调将在 IO 完成时被调用。这允许线程在 IO 发生时用于其他处理。在此模型下,线程池的大小根据可用的处理核心数进行调整。由于线程始终忙于计算,因此没有必要拥有更多线程。
许多 Java API(
InputStream
、JDBC
等)都基于阻塞 IO 模型。Ratpack 提供了一种机制来使用此类 API,同时最大限度地减少阻塞成本(如下所述)。
Ratpack 从本质上来说在两个关键方面是异步的…
- HTTP IO 是事件驱动/非阻塞的(感谢 Netty)
- 请求处理被组织成异步函数的管道
HTTP IO 是事件驱动的,在使用 Ratpack 时,这在很大程度上是透明的。Netty 只做它应该做的事情。
第二点是 Ratpack 的关键特征。它不希望您的代码是同步的。许多具有可选异步支持的 Web 框架具有严重的约束和陷阱,这些约束和陷阱在尝试执行复杂的(即真实世界)异步操作时会变得显而易见。Ratpack 从一开始就是异步的。此外,它提供了构建和抽象,可以促进复杂的异步处理。
2.8 执行阻塞操作(例如 IO)
大多数应用程序都需要执行某种阻塞 IO。许多 Java API 不提供异步选项(例如 JDBC)。Ratpack 提供了一种简单的机制,可以在单独的线程池中执行阻塞操作。这避免了阻塞请求处理(即计算)线程(这是一件好事),但也由于线程争用而产生一些开销。如果您必须使用阻塞 IO API,那么不幸的是没有其他选择。
让我们考虑一个虚构的数据存储 API。可以想象,与实际数据存储的通信需要 IO(或者如果它在内存中,那么访问它需要等待一个或多个锁,这具有相同的阻塞效果)。不能在请求处理线程上调用 API 方法,因为它们会阻塞。相反,我们需要使用“阻塞”API…
import ratpack.core.handling.InjectionHandler;
import ratpack.core.handling.Context;
import ratpack.exec.Blocking;
import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;
import java.util.Collections;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class Example {
// Some API that performs blocking operations
public static interface Datastore {
int deleteOlderThan(int days) throws IOException;
}
// A handler that uses the API
public static class DeletingHandler extends InjectionHandler {
void handle(final Context context, final Datastore datastore) {
final int days = context.getPathTokens().asInt("days");
Blocking.get(() -> datastore.deleteOlderThan(days))
.then(i -> context.render(i + " records deleted"));
}
}
// Unit test
public static void main(String... args) throws Exception {
HandlingResult result = RequestFixture.handle(new DeletingHandler(), fixture -> fixture
.pathBinding(Collections.singletonMap("days", "10"))
.registry(r -> r.add(Datastore.class, days -> days))
);
assertEquals("10 records deleted", result.rendered(String.class));
}
}
作为阻塞操作提交的函数将在单独的线程池中异步执行(即Blocking.get()
方法立即返回一个承诺)。它返回的结果将在请求处理(即计算)线程上进行处理。
有关详细信息,请参阅 Blocking#get() 方法。
3.8 执行异步操作
该 Promise#async(Upstream
import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.Promise;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class Example {
public static void main(String... args) throws Exception {
EmbeddedApp.fromHandler(ctx ->
Promise.async((f) ->
new Thread(() -> f.success("hello world")).start()
).then(ctx::render)
).test(httpClient -> {
assertEquals("hello world", httpClient.getText());
});
}
}
4.8 异步组合和避免回调地狱
异步编程的挑战之一在于组合。非平凡的异步编程可以很快地陷入一种被称为“回调地狱”的现象,这是用来描述多层嵌套回调的难以理解性的术语。
优雅而干净地将异步操作组合到复杂的流程中,是目前快速创新的领域。Ratpack 并不试图提供一个异步组合框架。相反,它旨在集成并提供适配器来适应此任务的专用工具。这种方法的一个例子是 Ratpack 与 RxJava 的集成.
一般来说,集成是将 Ratpack 的 Promise
类型与目标框架的组合原语进行适配。