跳到主要内容
Deno 2.4 已发布,带来 deno bundle、字节/文本导入、OTel 稳定版等功能
了解更多
Benchmarking cold start times in AWS across runtimes

JavaScript 运行时在 AWS Lambda 冷启动性能基准测试

我们构建了 Deno,一个现代、简单、安全、零配置的 JavaScript(和 TypeScript)运行时,旨在彻底简化 Web 开发,而性能是这一使命的关键支柱。

在我们的基准测试中,我们发现 Deno 在 AWS Lambda 中运行时的冷启动时间始终低于其他 JavaScript 运行时。在这篇文章中,我们将分享我们运行这些基准测试的方法、结果,以及如何在无服务器环境中进一步优化 Deno 运行生产级 JavaScript 的技巧。

为什么冷启动很重要

冷启动是指运行时从头开始启动,因此冷启动时间被定义为用户等待其首次请求未初始化运行时并获得响应的总时间。冷启动直接影响应用程序的性能,特别是实时游戏、视频流、高频交易、电子商务等对延迟敏感的应用程序。例如,如果一个检索电子商务产品详情的 AWS Lambda 函数遇到延迟,将导致糟糕的用户体验和较低的转化率。

由于较高的冷启动时间会不成比例地影响用户体验和潜在收入,冷启动时间性能的一个重要指标是尾部延迟,即高百分位延迟。(虽然您的服务平均延迟可能为 10 毫秒,但用户记住的往往是偶尔出现的 100 毫秒延迟。)在我们的分析中,我们将检查冷启动时间的分布,以衡量整体性能。

方法论

为了评估代表真实世界应用程序的冷启动时间,我们使用了常见的无服务器云设置:带有 AWS Lambda 的 Docker 容器。这是一种流行的方法,因为它提供了控制、灵活性和一致性:Docker 容器简化了跨环境的标准化设置,允许完全控制依赖项(包括系统级),可以非常大(高达 10GB),并且可以使用 AWS Lambda 中包含的版本之外的任何运行时版本。

在 AWS Lambda 上运行 JavaScript 还有其他方法。AWS 还提供了自己的托管 Node 运行时,针对 Lambda 环境进行了优化。尽管这并非完全对等的比较,但我们将其基准测试结果作为参考纳入其中。AWS 还创建了 LLRT(低延迟运行时)以最大限度地减少启动时间,但由于其 Node.js 兼容性支持不完整,我们无法在其上运行基准测试应用程序。

由于大多数无服务器用例都是后端,我们使用三种常用框架对 API 服务器进行了基准测试:Express、Fastify 和 Hono。在每个框架中,我们都实现了一个简单的链接缩短器。为了使它们代表真实世界的用例,每个应用程序都导入了多个依赖项,并同时使用了服务器端逻辑和服务器端渲染。

Docker 镜像是使用 AWS Lambda Web Adapter 创建的,它无需更改应用程序代码即可部署到 Lambda。为了进一步减少冷启动时间,我们在准备 Docker 镜像时首先运行了程序。这会安装依赖项并初始化各种运行时缓存,这些缓存会成为 Docker 镜像的一部分,因此当程序在 AWS Lambda 中执行时,它们可以立即使用。

所有 Lambda 函数都使用了通用配置

  • 区域:us-west2
  • CPU 架构:x86_64
  • 内存:512MB

请注意,AWS Lambda 也可以在 Graviton (ARM) 处理器上运行,Deno 对此提供支持。然而,在 Graviton 上的冷启动时间被测出在所有基准测试的运行时(包括 Deno、Node 和 Bun)中都较慢(查看原始结果)。因此,本文的分析将侧重于 x86_64。

我们在基准测试中使用的运行时及其版本是

  • Deno: 1.45.2
  • Node
    • 使用 Docker: 22.5.1
    • 使用 Lambda 托管的 Node 运行时: 20.14.0 *
  • Bun: 1.1.19

*请注意,我们还使用 AWS Lambda 托管的 Node 运行时进行了基准测试,尽管它没有使用与其他设置相同的 Docker 配置,但它作为一个比较点。

为了模拟冷启动,我们在每次调用之间通过修改环境变量来更新 Lambda 配置。通过解析每次调用后 CloudWatch 事件中的 Init Duration 来观察冷启动延迟。您可以在此处查看运行基准测试的脚本。

请注意,Init Duration 专门测量 Lambda 初始化阶段的延迟,这可能无法完全捕获客户端所经历的整体端到端冷启动延迟。具体来说,Init Duration 不包括 Lambda 将代码工件复制到 Lambda 沙箱所需的时间或客户端的网络 RTT(往返时间)。

我们认为 Init Duration 是一个可靠的指标,可以代表本博文中所定义的应用服务器的冷启动延迟,原因如下:

  • 整体 Docker 镜像大小相对较小(小于 85MB)
  • 运行时之间的冷启动时间差异主要由 Init Duration 测量的运行时和 npm 模块初始化决定(有关证实这一点的单独基准测试,请参见Linux 虚拟机上的启动时间部分)
  • 使用 Lambda 函数 URL 测量简单,无需额外依赖

基准测试

我们对运行时和框架的每种组合进行了 20 次基准测试。每次迭代,我们都强制进行冷启动并测量了 Init Duration。您可以在此处查看原始结果。

Express

基准测试使用了 这个 Express URL 缩短器应用程序,它使用了 Express 4.19.2 版本。

在 Docker 上,Deno 的 Init Duration 时间比 Node 和 Bun 更快。

Box chart of cold start time latencies for Deno, Bun, Node, and Lambda-managed Node for an Express app

小圆圈代表异常值,即低于第一四分位数或高于第三四分位数超过四分位距(数据中间 50% 的分布)1.5 倍的数据点。

Fastify

基准测试使用了 这个 Fastify URL 缩短器应用程序,它使用了 Fastify 4.28 版本。

在 Docker 上,Deno 的 Init Duration 时间比 Node 和 Bun 更快。

Box chart of cold start time latencies for Deno, Bun, Node, and Lambda-managed Node for a Fastify app

Hono

基准测试使用了 这个 Hono URL 缩短器应用程序,它使用了 Hono 4.4.6 版本。

在 Docker 上,Deno 的 Init Duration 时间比 Node 和 Bun 更快。

Box chart of cold start time latencies for Deno, Bun, Node, and Lambda-managed Node for a Hono app

总的来说,当在 AWS Lambda 中使用 Docker 时,Deno 的冷启动时间始终快于 Node 和 Bun。

Linux 虚拟机上的启动时间

我们希望通过在 Linux 虚拟机上直接手动运行应用程序并测量其启动延迟来验证我们的 Lambda 基准测试结果。为此,我们在 Google Cloud us-west1 区域配置了一台 e2-medium 虚拟机,安装了 Deno/Node/Bun 运行时,并使用 hyperfine 基准测试工具多次启动了同一组 URL 缩短器 API 服务器。由于我们只关注测量启动延迟,因此对 API 服务器应用程序代码进行了少量修改,使其在初始化后立即退出。

我们为每个 API 服务器运行了以下命令行

hyperfine --warmup 2 "deno run -A main.mjs" "node main.mjs" "bun main.mjs"

在所有基准测试框架中,Deno 的启动延迟都快于 Node 和 Bun。

Express

在我们的基准测试中,Deno 在虚拟机环境中的冷启动时间比 Bun 快约 33%,比 Node 快 36%。

Benchmark 1: deno run -A main.mjs
  Time (mean ± σ):     134.9 ms ±   2.6 ms    [User: 111.3 ms, System: 32.7 ms]
  Range (min … max):   130.6 ms … 139.9 ms    21 runs

Benchmark 2: node main.mjs
  Time (mean ± σ):     183.7 ms ±   3.9 ms    [User: 187.1 ms, System: 31.6 ms]
  Range (min … max):   177.9 ms … 194.5 ms    15 runs

Benchmark 3: bun main.mjs
  Time (mean ± σ):     178.8 ms ±   3.0 ms    [User: 160.3 ms, System: 38.5 ms]
  Range (min … max):   173.0 ms … 183.8 ms    16 runs

Summary
  deno run -A main.mjs ran
    1.33 ± 0.03 times faster than bun main.mjs
    1.36 ± 0.04 times faster than node main.mjs

Fastify

在我们在虚拟机中运行 Fastify 的基准测试中,Deno 的冷启动时间比 Node 快约 39%,比 Bun 快 46%。

Benchmark 1: deno run -A main.mjs
  Time (mean ± σ):     187.3 ms ±   5.8 ms    [User: 145.7 ms, System: 52.4 ms]
  Range (min … max):   180.4 ms … 204.4 ms    15 runs

Benchmark 2: node main.mjs
  Time (mean ± σ):     261.1 ms ±   5.5 ms    [User: 285.2 ms, System: 38.4 ms]
  Range (min … max):   252.5 ms … 273.5 ms    11 runs

Benchmark 3: bun main.mjs
  Time (mean ± σ):     273.0 ms ±   7.0 ms    [User: 244.0 ms, System: 48.8 ms]
  Range (min … max):   265.0 ms … 292.5 ms    11 runs

Summary
  deno run -A main.mjs ran
    1.39 ± 0.05 times faster than node main.mjs
    1.46 ± 0.06 times faster than bun main.mjs

Hono

在我们在虚拟机中运行 Hono 的基准测试中,Deno 的冷启动时间比 Bun 快约 71%,比 Node 快 77%。

Benchmark 1: deno run -A main.mjs
  Time (mean ± σ):      57.6 ms ±   3.4 ms    [User: 40.8 ms, System: 22.8 ms]
  Range (min … max):    52.9 ms …  65.8 ms    45 runs

Benchmark 2: node main.mjs
  Time (mean ± σ):     102.0 ms ±   2.6 ms    [User: 93.6 ms, System: 22.9 ms]
  Range (min … max):    98.2 ms … 107.4 ms    27 runs

Benchmark 3: bun main.mjs
  Time (mean ± σ):      98.6 ms ±  11.3 ms    [User: 90.8 ms, System: 35.2 ms]
  Range (min … max):    86.8 ms … 117.9 ms    26 runs

Summary
  deno run -A main.mjs ran
    1.71 ± 0.22 times faster than bun main.mjs
    1.77 ± 0.11 times faster than node main.mjs

根据我们在虚拟机设置中的基准测试,与 Bun 和 Node 相比,Deno 仍然始终能产生最快的冷启动时间。

为无服务器环境优化 Deno

在 AWS Lambda 中,Deno 与其他运行时相比,始终表现出最快的冷启动时间。Deno 依赖各种运行时缓存来最大化启动性能。其中一些包括:

  • JSR 模块
  • npm 模块
  • 模块图
  • CJS 导出分析
  • 转译后的 TypeScript
  • V8 代码缓存(字节码缓存)

重要的是,这些缓存应在 Docker 镜像创建期间提前填充,以便您的应用程序在 AWS Lambda 中执行时可以立即使用。您可以通过在 Dockerfile 中添加以下行来确保这一点

RUN timeout 10s deno run --allow-net main.ts || [ $? -eq 124 ] || exit 1

这是一个很多人不知道的技巧。幸运的是,未来这些知识将变得不必要,因为它将成为 deno cache 本身的一部分。

下一步

在编写生产软件时,性能是一个关键的考量因素。Deno 一如既往地致力于提高性能,不仅在每个次要版本中改进运行时,还在我们的工具链中进行改进,例如最近在语言服务器方面的优化。敬请期待 Deno 在性能和优化方面的更多改进。

🚨️ Deno 2 即将发布 🚨️

Deno 2 中有一些小的破坏性变更,但您现在就可以通过使用 DENO_FUTURE=1 标志来使迁移更顺畅。