基准测试不同 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 还提供自己的托管 Node 运行时,该运行时针对 Lambda 环境进行了优化。尽管不是同类比较,我们还是将其中的基准测试作为参考。AWS 还创建了 LLRT(低延迟运行时)以最大限度地减少启动时间,但由于 Node.js 兼容性支持不完整,我们无法在其上运行基准测试应用程序。
由于大多数无服务器用例都是后端,我们使用三个常用框架对 API 服务器进行了基准测试:Express、Fastify 和 Hono。在每个框架中,我们都实现了一个简单的链接缩短器。为了使它们代表真实的用例,每个框架都导入了多个依赖项,并使用了服务器端逻辑和服务器端渲染。
Docker 镜像 是使用 AWS Lambda Web Adapter 创建的,它不需要更改应用程序代码即可部署到 Lambda。为了进一步减少冷启动时间,我们在准备 Docker 镜像时首先运行程序。这将安装依赖项并初始化各种运行时缓存,这些缓存成为 Docker 镜像的一部分,因此它们在程序在 AWS Lambda 中执行时立即可用。
所有 Lambda 函数都使用通用配置
- 区域:us-west2(美国西部 2)
- CPU 架构:x86_64
- 内存:512MB
请注意,AWS Lambda 也可以在 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
* *请注意,我们还使用 AWS Lambda 托管 Node 运行时进行了基准测试,尽管它没有使用与其他设置相同的 Docker 设置,但将其作为比较点。*
为了模拟冷启动,我们通过修改环境变量在每次调用之间更新 Lambda 配置。通过解析每次调用后 CloudWatch 事件中的“初始化持续时间”来观察冷启动延迟。您可以在此处查看运行基准测试的脚本。
请注意,“初始化持续时间”专门测量 Lambda 初始化阶段 的延迟,这可能无法完全捕获客户端体验到的整体端到端冷启动延迟。具体来说,“初始化持续时间”不包括 Lambda 将代码工件复制到 Lambda 沙盒所需的时间或客户端的网络往返时间。
我们认为,“初始化持续时间”是代表本文中定义的应用程序服务器的冷启动延迟的可靠代理,因为
- 整体 Docker 镜像大小相对较小(小于 85MB)
- 不同运行时之间的冷启动时间差异主要由运行时和 npm 模块初始化决定,这可以通过
Init Duration
来衡量(参见Linux 虚拟机上的启动时间部分,其中包含一个单独的基准测试来证实这一点)。 - 使用 Lambda 函数 URL 即可轻松测量,无需额外的依赖项。
基准测试
我们对每种运行时和框架组合进行了 20 次基准测试。每次迭代中,我们都强制进行冷启动并测量 Init Duration
。您可以在此处查看原始结果。
Express
基准测试使用了这个 Express URL 缩短器应用程序,它使用 Express 4.19.2 版本。
在 Docker 上,Deno 的 Init Duration
时间比 Node 和 Bun 更快。
基准测试使用了这个 Fastify URL 缩短器应用程序,它使用 Fastify 4.28 版本。
在 Docker 上,Deno 的 Init Duration
时间比 Node 和 Bun 更快。
基准测试使用了这个 Hono URL 缩短器应用程序,它使用 Hono 4.4.6 版本。
在 Docker 上,Deno 的 Init Duration
时间比 Node 和 Bun 更快。
总体而言,在 AWS Lambda 中使用 Docker 时,Deno 的冷启动时间始终比 Node 和 Bun 快。
我们希望通过在 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 快。
在我们的基准测试中,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 的基准测试中,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 的基准测试中,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 始终产生最快的冷启动时间。
在 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
标志来使迁移更加顺畅。