最近,一篇关于 Cloudflare Workers CPU 性能的基准测试报告在技术圈引起了轩然大波。独立开发者 Theo Browne 发布的一系列测试显示,在多种 CPU 密集型任务中,Cloudflare Workers 的性能竟然比基于 AWS Lambda 的 Vercel (Node.js) 慢了高达 3.5 倍。
对于一个以性能著称的全球网络巨头,这无疑是一次不大不小的“公关危机”。然而,Cloudflare 随后的反应,堪称科技公司危机处理的典范。他们不仅在短短一周内迅速定位并修复了问题,还发布了一篇极其详尽、坦诚的技术博客,深入剖析了问题的来龙去脉。
这篇文章不仅让我们看到了一个顶级技术团队如何解决问题,更像一部精彩的侦探小说,带领我们层层深入,从调度算法到 V8 引擎的垃圾回收,再到开源社区的协作。
接下来,让我们跟随 Cloudflare 的脚步,一起“破案”,看看他们是如何化解这次性能危机的,并从您的注解中汲取更深层次的技术洞见。
缘起:一份出乎意料的性能测试报告
故事的开端很简单:一份公开的基准测试报告显示 Cloudflare Workers 在 CPU 密集型任务上表现不佳。
这让 Cloudflare 的工程师们大为不解。因为从技术上讲,Cloudflare Workers 和 Vercel 上的 Node.js 都使用了相同的 JavaScript 引擎——Google 的 V8。理论上,执行纯粹的 CPU 计算任务,性能应该在同一水平线上,不应出现如此巨大的差异。
这背后一定另有隐情。
破案第一站:调度算法的“误判”
经过调查,Cloudflare 发现的第一个问题,并非 CPU 本身的性能,而是一个更宏观的调度问题。
Sharding and Warm Isolate Routing:一个关于“热启动”的智慧
为了减少冷启动,Cloudflare 在过去一年中部署了一套智能路由系统。其核心思想是将来自特定 Worker 的请求,尽可能地路由到已经“预热”(Warm)好的 Isolate(隔离实例)上。
注解解析:分片与一致性哈希
您的注解非常精彩地解释了这背后的技术——分片 (Sharding) 和 一致性哈希 (Consistent Hashing)。
传统模型:一个 Worker 的请求被随机分配到数据中心内的任何一台服务器上。结果就是,为了处理这个 Worker 的请求,可能需要在 100 台服务器上都加载一份 Worker 的代码,造成巨大的内存浪费和更高的冷启动概率。
分片模型:通过一致性哈希算法,将每个 Worker ID “固定”到环上的某一个“分片服务器”上。无论哪个边缘节点收到了请求,它都会计算出这个 Worker 的“家”在哪,然后将请求转发过去。
// Worker A 固定分配给服务器 23 请求 1 → 服务器 10 收到 → 转发到 23 ✅ 请求 2 → 服务器 45 收到 → 转发到 23 ✅这样做的好处是,绝大多数情况下,只需要在一台服务器上保持 Worker A 的实例活跃,极大地提高了缓存命中率,让 Worker 永远“热”着。
Cloudflare 甚至还设计了乐观路由 (Optimistic Routing) 和优雅降级方案。当请求被转发到分片服务器时,如果该服务器过载,它不会直接拒绝请求,而是会通过 Cap’n Proto RPC 返回一个指令,告诉接收请求的边缘节点:“你自己来执行吧”。这样就实现了负载的动态平衡。
然而,这套为 I/O 密集型任务优化的系统,在遇到这次的 CPU 密集型基准测试时,却出现了“误判”。测试程序会从单个客户端瞬间发出大量计算密集型请求,导致这些请求被堆积在同一个 Isolate 的任务队列中,互相等待。系统虽然有启发式算法来检测这种情况并启动更多的 Isolate,但显然,这个算法对于这种极端的流量模式不够敏感。
结果就是:测试记录到的延迟,大部分是“排队等待时间”,而不是真正的“CPU 执行时间”。而 Cloudflare 的计费模型只计算 CPU 执行时间,所以这个问题并不会影响用户的账单,但在测试数据上却显得性能很差。
解决方案:Cloudflare 迅速调整了算法,使其能更早地检测到持续的 CPU 密集型工作,并更快地启动新的 Isolate 来分担压力。这个改动已经全球部署,所有用户都因此受益。
破案第二站:V8 垃圾回收的“陈年旧账”
调度问题解释了大部分的延迟差异,但并非全部。Cloudflare 的工程师们继续深挖,发现了一个影响代码执行性能的微小问题,而这个问题,竟然要追溯到 2017 年。
V8 Garbage Collector Tuning:年轻代空间(Young Generation)的取舍
注解解析:分代垃圾回收与 Young Generation
您的注解为这部分提供了完美的背景知识。V8 的垃圾回收机制基于经典的分代假说:
- 大多数对象“朝生夕死”。
- 少数对象会长期存活。
因此,内存被划分为两大部分:
- 年轻代 (Young Generation):存放新创建的对象。这里的垃圾回收(称为 Minor GC 或 Scavenger)非常频繁,但速度极快,采用的是一种“空间换时间”的 Cheney 算法(半空间复制)。
- 老年代 (Old Generation):存放经过几轮回收后依然存活的对象。这里的回收(Major GC)不频繁,但更耗时。
Young Generation 的大小至关重要:
- 太小:会导致 Minor GC 过于频繁。在 CPU 密集型任务中(如 React SSR),会创建大量临时对象,迅速填满年轻代,导致 CPU 大量时间被 GC 占用,而不是执行业务逻辑。
- 太大:会增加单次 GC 的扫描时间,导致 STW (Stop-The-World) 暂停时间变长,影响应用的响应延迟。
问题就出在这里。早在 2017 年,Workers 项目刚起步时,当时的工程师(也就是这篇博客的作者 Kenton Varda 本人)根据 V8 当时的建议,为一个 128MB 内存限制的环境,手动设置了一个较小的 Young Generation 空间。
然而,从 2017 年到 2025 年,V8 的垃圾回收器已经发生了翻天覆地的变化。当年合理的配置,在今天看来已经过于保守,它限制了 V8 内部的启发式算法,导致 GC 比原本需要的更努力、更频繁地工作。
解决方案:Cloudflare 工程师们移除了这个手动配置,让 V8 可以根据其内部更现代、更智能的启发式算法自由地调整 Young Generation 的大小。这一改动,为基准测试带来了大约 25% 的性能提升,并且所有 Workers 应用都因此受益。
破案第三站:框架与生态的“坑”
平台层面的两个主要问题解决后,大部分基准测试项的性能已经与 Vercel 持平。但还有一个例外:Next.js。
Tuning OpenNext for Performance:当框架成为瓶颈
注解解析:OpenNext vs. Next.js on Vercel
您的注解指出了一个关键的背景:Next.js 与其母公司 Vercel 的平台深度绑定。为了在其他平台(如 Cloudflare)上也能获得良好的体验,社区创建了 OpenNext 项目。因此,这次测试实际上是 OpenNext on Cloudflare 与 Next.js on Vercel 的对比。
Cloudflare 在分析 OpenNext 的性能时,发现了大量可以优化的地方:
- 不必要的内存分配和拷贝:在渲染管线的“热路径”上,存在大量不必要的
Buffer拷贝和拼接操作。例如,仅仅为了获取一堆 Buffer 的总长度,代码执行了Buffer.concat(chunks).length,这会创建一个巨大的新 Buffer,然后立刻丢弃,造成了极大的浪费。 - 低效的流适配器:Next.js 大量使用 Node.js 的流 API,而 Workers 则使用 Web 标准的 Streams API。在两者之间转换的适配器实现得非常低效,造成了多层不必要的数据缓冲。
- JSON.parse() 的性能陷阱:React 和 Next.js 在服务端渲染时,会使用
JSON.parse()的reviver函数。这个reviver函数在一次请求中可能被调用超过 10 万次!而最近的一项标准变更,使得带reviver的JSON.parse()变得更慢了。
解决方案:Cloudflare 不仅向 OpenNext 社区提交了一系列的 PR 来修复这些性能问题,甚至还向上游的 V8 项目提交了一个 Patch,将带有 reviver 的 JSON.parse() 性能提升了约 33%。这个改进将惠及整个 V8 生态(包括 Node.js, Chrome, Deno 等)。
注解解析:V8 如何优化
JSON.parse您的注解深入解释了 V8 的优化原理。
reviver函数可以接收三个参数(key, value, holder)。但在实际使用中,开发者往往只关心key和value。Cloudflare 的工程师发现,通过一个简单的箭头函数包装,可以给 V8 一个明确的信号:// 优化后 JSON.parse(str, (key, val) => reviver(key, val));这个箭头函数包装禁用了
arguments对象和自己的this绑定,让 V8 引擎可以确定reviver内部不会访问第三个参数,从而可以应用更激进的优化策略。
一个意外的发现:Node.js 的三角函数问题
在调查过程中,Cloudflare 还顺手解决了另一个不相关、但很有趣的问题。在最初引发讨论的另一份基准测试中,Cloudflare Workers 在执行三角函数(如 Math.sin)时,比 Vercel 上的 Node.js 快了 3 倍。
Cloudflare 并没有心安理得地接受这个“胜利”,而是深入探究了原因。
注解解析:编译时标志与“最小公分母”
您的注解再次提供了完美的解释。V8 引擎对三角函数的实现有快速和慢速两个路径,具体走哪个路径,取决于编译时标志 (Compile-Time Flag)。
- Node.js:为了支持最广泛的硬件平台(从古老的服务器到各种 CPU 架构),它必须采用“最小公分母”原则,默认关闭了使用现代 CPU 指令(如 AVX2)的快速路径。
- Cloudflare Workers:由于运行在 Cloudflare 自己可控的、现代化的数据中心硬件上,它可以在编译 V8 时,放心地开启这些优化标志,从而“巧合地”使用了硬件加速路径。
解决方案:本着开源和回馈社区的精神,Cloudflare 向 Node.js 项目提交了一个 PR,建议在能够安全检测到平台支持的情况下(如现代的 64 位 Linux/macOS),自动开启这个快速路径。这样一来,整个 Node.js 生态都能从中受益。
结论:一次堪称典范的危机公关
回顾整个事件,Cloudflare 的应对策略堪称完美:
- 坦诚透明:不回避问题,不甩锅,而是用一篇极其详尽的技术博客,把所有细节公之于众。
- 深入彻底:从平台调度,到 V8 引擎,再到上游框架,层层递进,把问题挖得一清二楚。
- 快速行动:一周之内定位问题、修复上线、发布报告,行动力惊人。
- 回馈社区:不仅修复了自己的问题,还把改进方案(如 V8
JSON.parse优化、Node.js 三角函数优化)回馈给开源社区,展现了顶级公司的技术担当和格局。
从这次事件中,我们不仅学到了关于 Isolate 调度、V8 GC、框架性能优化的硬核知识,更看到了一个优秀技术团队应有的文化和态度。当你的产品被指出不足时,最好的回应不是辩解,而是用更优秀的产品和更开放的态度去赢得尊重。
Cloudflare 做到了。
3845

被折叠的 条评论
为什么被折叠?



