跳转至主要内容
Benchmarking cold start times in AWS across runtimes

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

我们构建了 Deno,一个现代、简单、安全、零配置的 JavaScript(和 TypeScript)运行时,旨在从根本上简化 Web 开发,而性能是该任务的关键支柱。

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

为什么冷启动很重要

冷启动是指运行时从头开始启动,因此冷启动时间定义为用户必须等待首次请求未初始化运行时得到响应的总时间。冷启动直接影响应用程序的性能,尤其是在实时游戏、视频流、高频交易、电子商务等延迟敏感型应用程序中。例如,如果检索电子商务产品详细信息的 AWS Lambda 函数出现延迟,将导致糟糕的用户体验和更低的转化率。

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

方法

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

还有其他在 AWS Lambda 上运行 JavaScript 的方法。AWS 还提供了针对 Lambda 环境优化的自有托管 Node 运行时。尽管不是苹果对苹果的比较,但我们仍然包含了来自它的基准测试,仅供参考。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 (ARM) 处理器。然而,经测量,Graviton 上的冷启动时间在所有基准测试的运行时(包括 Deno、Node 和 Bun)中都较慢(查看原始结果)。因此,这篇文章中的分析将侧重于 x86_64。

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

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

*注意,尽管未使用与其他设置相同的 Docker 设置,但我们也使用了 AWS Lambda 托管 Node 运行时进行了基准测试,作为比较点。

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

请注意,Init Duration 专门测量 Lambda 初始化阶段延迟,这可能无法完全捕获客户端体验到的总体端到端冷启动延迟。具体而言,Init Duration 不包括 Lambda 将代码工件复制到 Lambda 沙箱所需的时间,也不包括来自客户端的网络 RTT。

我们认为 Init Duration 是代表本文中定义的应用程序服务器的冷启动延迟的可靠代理,因为

  • 总体 Docker 镜像大小相对较小(小于 85MB)
  • 运行时之间的冷启动时间差异主要由 Init Duration 测量的运行时和 npm 模块初始化决定(有关确认这一点的单独基准测试,请参阅Linux VM 上的启动时间部分)
  • 易于使用 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 VM 上的启动时间

我们想通过在 Linux VM 上直接手动运行应用程序并测量其启动延迟来验证我们的 Lambda 基准测试结果。为此,我们在 Google Cloud us-west1 区域配置了一个 e2-medium VM,安装了 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

在我们的基准测试中,VM 环境中 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

在我们对在 VM 中运行 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

在我们对在 VM 中运行 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

根据我们在 VM 设置中的基准测试,与 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 标志使您的迁移更顺畅。