边缘函数速度提升5倍:从 V8 Isolates 转向 Firecracker MicroVMs
每天,Netlify 平台上约有十亿次 Edge Functions 调用。无论是 Sunweb 进行页面个性化、Loto-Québec 通过 Cookie 检查路由流量,还是数十万其他站点处理从个性化到路由再到身份验证的各种任务,它们都运行在完整的 JavaScript 运行时环境上,并随着客户流量的波动而动态扩展。
这带来了巨大的技术挑战,因为我们要尽可能降低延迟。为了处理每秒数万甚至数十万次边缘函数调用,系统必须在毫秒级时间内完成每个请求的处理、正确路由、计算资源分配,以及启动平台代码和客户代码。
过去几个月,我们的团队重建了 Edge Functions 的底层基础设施。期间,我们与 Unikraft 团队密切合作,他们甚至从自己的视角记录了一次开发体验。过去,请求会被转发到一个托管的执行服务;现在,它们直接运行在我们自身边缘网络内部的 MicroVM 上,中位数延迟降低了约 5 倍。这一转变不仅提升了安全性和可靠性,也为在边缘执行更复杂的计算任务打开了更多可能。
这并不改变 Edge Functions 的编写或使用方式——URL 导入、npm 包、Node 内置模块、netlify.toml 声明、本地开发——一切操作都和以前一样。只是现在它更快、更具韧性。在这篇文章中,我们希望分享更多关于新架构的内容,以及我们在构建一个能够以低性能开销处理高流量新型计算平台过程中的经验教训。
先看数据
边缘函数运行在站点前端,处理每个匹配的请求。这里消耗的每一毫秒,都是客户等待时间的一部分,其权重远高于其他场景。
一次热调用——包括路由到计算节点、进入 MicroVM、执行函数、生成响应头——现在的成本如下:
- 中位数(p50)约 5–6ms,相比旧基础设施的 25–40ms 大幅下降
- p99 调用速度提升 47.4%
- 99.998% 的可用性
- 边缘函数日志投递速度提升 5 倍

冷启动情况也值得一提。当请求到达某个此前没有任何计算节点访问过的区域时,系统需要拉取相关镜像才能开始运行。这类情况大约发生在 1.2% 的调用中,平均耗时约 9ms。
请求处理期间发生了什么
下面是单个请求的完整路径,按顺序排列:请求到达边缘节点,被转换成规格说明,路由到某个计算节点,然后交给一个 MicroVM 执行——这个 MicroVM 是否已存在,取决于本次是冷启动还是热调用。
请求到达边缘节点
每个请求都会落在离客户端最近的 Netlify 边缘节点上。该节点终结 TLS 连接,并将请求路径与该部署的 Edge Functions 路由进行匹配。
如果没有任何路由匹配,请求会照常流向缓存和源站。如果有路由匹配,这里就是请求曾经离开我们网络的地方。在旧基础设施下,请求会经由互联网出去执行 edge function,然后再回来转发。而在新的计算平台上,请求会被转发到我们网络内部的某个计算节点。

创建 Edge Function 服务
计算节点收到带有机器规格和服务 ID 的请求后,会先检查该 ID 对应的服务是否已存在。如果存在,就把请求转发给该服务,由它送入 MicroVM。通过服务,我们可以让同一站点的 Edge Functions 关联多个 MicroVM,还能配置 MicroVM 扩缩容的时机。例如,我们为每个服务设定了一个固定请求数上限,达到上限就关闭该 MicroVM,避免其无限期运行;同时也用同样的参数判断何时提前启动新的 MicroVM,为旧实例关闭做好准备。
如果计算节点上还没有该站点 Edge Functions 对应的服务,就创建一个,然后检查机器规格中列出的所有镜像是否已在磁盘上。缺哪个就从边缘节点拉取并写入磁盘。这样我们只需要拉取该区域实际有流量的那些 Edge Function 镜像。
边缘节点生成 spec
在请求发出之前,边缘节点会为将要运行函数的机器生成一份 spec。其中指定了三个镜像:runtime、我们的平台镜像和 Edge Function 镜像,同时还设定了 CPU、内存和连接数上限。
spec 会随每个请求一起传递。系统会对 spec 计算哈希,并结合站点信息生成服务 ID。这样就实现了隔离:两次部署只要代码或环境变量不同,就是不同的服务,永远不会共享同一个 MicroVM。
这种隔离最重要的意义在于杜绝我们不希望发生的问题。一个可能被入侵的部署运行在独立的 MicroVM 中,即使它突破了 runtime 的限制,也无法污染其他客户的请求或计算层本身。而 V8 isolates 无论名字听起来多像沙箱,都达不到这种隔离级别。
选择运行 Edge Function 的计算节点
每个区域有一组计算节点。边缘节点通过 rendezvous hashing 为服务选定其中一个节点:同一个服务每次都会落到同一个节点上,MicroVM 因此能保持热状态,代码也早已读到磁盘和缓存里。这种粘性为我们提供了缓存策略——如果请求在集群里平均分发,冷启动的比例会显著上升。
需要记住,虽然将同一函数的所有请求都路由到同一个计算节点是最快的路径,但这也正是热点形成的原因——某个繁忙的函数会与其他所有服务争夺该节点上的资源。如果某个服务占据了区域内很大一部分流量,却绑定在单个节点上,该节点就会饱和,从而拖累其他服务。
我们通过放宽粘性(stickiness)来平衡这一点。当流量超过某个阈值时,我们会将该服务分散到一组节点上。这样,即使某个客户突然产生流量尖峰,我们也能将其吸收,而不会影响被哈希到同一节点的其他服务。
最后,一旦选定节点,它就开始拉取函数代码。之前处理过该函数的计算节点已经持有代码。首次接触该函数的节点会拉取一次并缓存起来,因此只有第一个请求需要承担这部分成本。
启动 MicroVM
每个函数都在自己的 Firecracker MicroVM 中运行。这些 MicroVM 在不到一毫秒内创建,p99 延迟下约 2ms 即可启动,因为 VM 启动的是一个精简版 Linux 环境,而非完整的操作系统。边缘函数的文件以未压缩的 EROFS 镜像形式挂载,然后进行内存映射(memory-mapped),这样 VM 只读取代码包中实际使用的部分,而不是加载整个包。
当 MicroVM 启动且 JavaScript 服务器开始在端口上监听时,我们会对 MicroVM 进行快照。当边缘函数没有被调用时,运行该函数的 MicroVM 会缩容到零,而不是空闲挂起。下次调用时,我们从该快照启动一个新的 MicroVM。快照是内存映射的,因此 VM 可以立即开始执行,无需等待整个快照读回内存。
VM 的生命周期——启动、快照、恢复和缩容到零——是 Unikraft 产品的核心工作。在迁移过程中,我们与 Unikraft 密切合作,以确保其能承载我们的请求量和流量模式。
运行与响应处理
在运营该项目几年后,我们已经积累了一些经验,并将其融入系统中以最大化性能和可调试性。在大规模部署中,我们遇到了各种问题,从虚拟交换机端口耗尽到 DNS 问题(这一点让我们有些意外,但问题并不总是 DNS 引起的)。
在这一轮迭代中,我们确保计算节点运行本地 DNS 解析器。我们还扩展了指标采集范围,记录启动时间、首次端口开放时间和用户代码启动时间。系统内置了多个熔断器,以便快速重新路由和下线计算节点。
这就是完整的请求路径,在温实例上仅增加约 6ms 延迟。整个流程不离开我们的网络,且我们完全掌控整个请求周期。上述所有步骤均发生在请求到达与响应返回之间。
设计具备韧性的计算基础设施
构建上述系统时,我们需同时优化两个目标:终端用户体验和发布韧性。我们需要快速发布变更,同时具备同样快速的回滚能力。
计算节点基于 Unikraft 发布的基础镜像构建,并安装一组软件包。出于几个原因,这些节点与边缘节点分开构建:保持边缘节点轻量快速、允许使用不同的实例类型、支持独立扩展。
控制平面负责追踪现有的计算节点及其健康状态,边缘节点定期轮询以获取该列表。控制平面还驱动部署流程:新实例组在现有实例组旁启动,扩容至匹配规模,待确认健康后再接管流量。
构建计算基础设施需要与 Unikraft 团队密切合作。在整个迁移过程中,我们与对方协作测试正确性、处理大流量请求,并构建特定于我们平台的能力。
已上线
重构边缘计算架构的意义远不止速度提升。这是一个更快的基础架构,允许我们持续构建并获得更大控制权。最佳的是,它已在当前以相同价格服务于您的生产流量,无需迁移步骤,也无需在任何项目中更改配置。
自行运行计算节点意味着边缘函数的性能上限由我们决定。这使以下三件以前难以处理的事情变得可行:
- 支持 npm 包,结束 beta 阶段。 npm 包目前已在 Edge Functions 中可用(beta),但对原生二进制文件和运行时导入文件有一些限制。真正的虚拟机加真正的文件系统,让这些限制大多失去了存在的理由。
- 有望重新审视运行限制。 文档中的限制——每请求 50ms CPU 时间、512MB 内存、20MB 压缩代码——都源自基于 isolate 的执行模型。
- 在我们自己的网络内进行计算。凡是依赖控制网络路径、而非通过公网访问第三方的能力,现在我们都可以构建了。
我们不会止步于此。过去无法触及的限制和粗糙之处,正是我们目前正在改进的方向,敬请期待。