几个月前,我们发布了 Encore.ts — TypeScript 的开源后端框架。
由于已经有很多框架,我们想分享我们做出的一些不常见的设计决策以及它们如何带来卓越的性能数据。
我们之前发布的基准测试显示 Encore.ts 比 Express 快 9 倍,比 Fastify 快 2 倍。
这次我们将 Encore.ts 与 ElysiaJS 和 Hono 这两个现代高性能 TypeScript 框架进行了基准测试。
我们对带有和不带有模式验证的每个框架进行了基准测试,使用 TypeBox 与 ElsyiaJS 和 Hono 进行验证,因为它是这些框架原生支持的验证库。 (Encore.ts 有自己的内置类型验证,可以端到端工作。)
对于每个基准测试,我们选取五次运行中的最佳结果。每次运行都是通过 150 个并发工作线程发出尽可能多的请求来执行,时间超过 10 秒。负载生成是使用 oha 执行的,oha 是一个基于 Rust 和 Tokio 的 HTTP 负载测试工具。
废话不多说,让我们看看数字!
(查看 GitHub 上的基准代码。)
除了性能之外,Encore.ts 还实现了这一点,同时保持了与 Node.js 100% 的兼容性。
这怎么可能?通过我们的测试,我们确定了性能的三个主要来源,所有这些都与 Encore.ts 的工作原理有关。
Node.js 使用单线程事件循环运行 JavaScript 代码。尽管其具有单线程性质,但实际上它具有相当大的可扩展性,因为它使用非阻塞 I/O 操作,并且底层 V8 JavaScript 引擎(也为 Chrome 提供支持)经过了极其优化。
但是您知道什么比单线程事件循环更快吗?多线程。
Encore.ts由两部分组成:
使用 Encore.ts 编写后端时使用的 TypeScript SDK。
高性能运行时,具有用 Rust 编写的多线程异步事件循环(使用 Tokio 和 Hyper)。
Encore Runtime 处理所有 I/O,例如接受和处理传入的 HTTP 请求。它作为一个完全独立的事件循环运行,利用底层硬件支持的尽可能多的线程。
一旦请求被完全处理和解码,它就会被移交给 Node.js 事件循环,然后从 API 处理程序获取响应并将其写回客户端。
(在你说之前:是的,我们在你的事件循环中放置了一个事件循环,这样你就可以在事件循环时进行事件循环。)
Encore.ts,顾名思义,是专为 TypeScript 设计的。但你实际上无法运行 TypeScript:它首先必须通过剥离所有类型信息来编译为 JavaScript。这意味着运行时类型安全更难实现,这使得验证传入请求之类的事情变得困难,导致像 Zod 这样的解决方案在运行时定义 API 模式变得流行。
Encore.ts 的工作方式有所不同。借助 Encore,您可以使用本机 TypeScript 类型定义类型安全的 API:
import { api } from "encore.dev/api"; interface BlogPost { id: number; title: string; body: string; likes: number; } export const getBlogPost = api( { method: "GET", path: "/blog/:id", expose: true }, async ({ id }: { id: number }) => Promise{ // ... }, );
Encore.ts 然后解析源代码以了解每个 API 端点期望的请求和响应架构,包括 HTTP 标头、查询参数等。然后对模式进行处理、优化并存储为 Protobuf 文件。
当 Encore Runtime 启动时,它会读取此 Protobuf 文件并预先计算请求解码器和响应编码器,并使用每个 API 端点期望的确切类型定义,针对每个 API 端点进行优化。事实上,Encore.ts 甚至直接在 Rust 中处理请求验证,确保无效请求永远不必接触 JS 层,从而减轻许多拒绝服务攻击。
Encore 对请求模式的理解从性能角度来看也证明是有益的。像 Deno 和 Bun 这样的 JavaScript 运行时使用与 Encore 基于 Rust 的运行时类似的架构(事实上,Deno 也使用 Rust Tokio Hyper),但缺乏 Encore 对请求模式的理解。因此,他们需要将未处理的 HTTP 请求交给单线程 JavaScript 引擎执行。
另一方面,Encore.ts 在 Rust 内部处理更多的请求处理,并且只移交解码后的请求对象。通过在多线程 Rust 中处理更多的请求生命周期,JavaScript 事件循环可以专注于执行应用程序业务逻辑,而不是解析 HTTP 请求,从而产生更大的性能提升。
细心的读者可能已经注意到了一个趋势:性能的关键是尽可能多地从单线程 JavaScript 事件循环中卸载工作。
我们已经了解了 Encore.ts 如何将大部分请求/响应生命周期卸载给 Rust。那么还有什么可做的呢?
嗯,后端应用程序就像三明治。您有硬壳顶层,您可以在其中处理传入的请求。中间有美味的配料(当然,也就是你的业务逻辑)。在底部有硬壳数据访问层,您可以在其中查询数据库、调用其他 API 端点等等。
我们对业务逻辑无能为力——毕竟我们想用 TypeScript 编写! — 但是让所有数据访问操作占用我们的 JS 事件循环并没有多大意义。如果我们将它们移至 Rust,我们将进一步释放事件循环,以便能够专注于执行我们的应用程序代码。
这就是我们所做的。
使用 Encore.ts,您可以直接在源代码中声明基础设施资源。
例如,定义一个 Pub/Sub 主题:
import { Topic } from "encore.dev/pubsub"; interface UserSignupEvent { userID: string; email: string; } export const UserSignups = new Topic("user-signups", { deliveryGuarantee: "at-least-once", }); // To publish: await UserSignups.publish({ userID: "123", email: "[email protected]" });
“那么它使用哪种 Pub/Sub 技术?”
— 他们所有人!
Encore Rust 运行时包括最常见的 Pub/Sub 技术的实现,包括 AWS SQS SNS、GCP Pub/Sub 和 NSQ,以及更多计划中的技术(Kafka、NATS、Azure 服务总线等)。您可以在应用程序启动时在运行时配置中按资源指定实现,或者让 Encore 的 Cloud DevOps 自动化为您处理。
除了 Pub/Sub 之外,Encore.ts 还包括 PostgreSQL 数据库、Secrets、Cron Jobs 等的基础设施集成。
所有这些基础设施集成都在 Encore.ts Rust 运行时中实现。
这意味着,一旦您调用 .publish(),有效负载就会被移交给 Rust,Rust 负责发布消息,并在必要时重试,等等。数据库查询、订阅 Pub/Sub 消息等也是如此。
最终结果是,使用 Encore.ts,几乎所有非业务逻辑都从 JS 事件循环中卸载。
本质上,通过 Encore.ts,您可以“免费”获得真正的多线程后端,同时仍然能够在 TypeScript 中编写所有业务逻辑。
此性能是否重要取决于您的用例。如果您正在构建一个小型爱好项目,那么它主要是学术性的。但如果您将生产后端发送到云,它可能会产生相当大的影响。
较低的延迟对用户体验有直接影响。显而易见的是:更快的后端意味着更快的前端,这意味着更快乐的用户。
更高的吞吐量意味着您可以使用更少的服务器为相同数量的用户提供服务,这直接对应于更低的云费用。或者,相反,您可以使用相同数量的服务器为更多用户提供服务,确保您可以进一步扩展而不会遇到性能瓶颈。
虽然我们有偏见,但我们认为 Encore 为在 TypeScript 中构建高性能后端提供了一个非常优秀、最好的解决方案。它速度快、类型安全,并且与整个 Node.js 生态系统兼容。
而且它都是开源的,因此您可以查看代码并在 GitHub 上做出贡献。
或者尝试一下,让我们知道您的想法!
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3