免费 Workers 计划中 10 毫秒的 CPU 限制让初次阅读的读者感到害怕。对于任何有用的事情来说,十毫秒似乎都短得离谱。结果是,许多团队立即升级到为 30 秒 CPU 付费的计划 - 没有意识到测量模型与传统服务器根本不同,并且大多数 Worker 即使在免费的 10 毫秒内也有剩余的 CPU。另一方面,128MB 内存限制被系统性地低估,并导致生产中出现一系列无声故障。
CPU 定时器的实际工作原理
Workers 运行时测量“CPU 时间”——JavaScript 线程主动执行代码的时间。定时器在任何异步 I/O 操作期间停止:011、23。在这些等待期间,isolate 处于空闲状态,并且计时器不会提前。
实际效果是很大的。一个 Worker 对外部 API 进行 5 次连续调用(每次调用需要 100 毫秒的网络延迟),其挂钟时间为 500 毫秒,但可能使用 6 毫秒的 CPU — 仅用于标头的序列化、响应 JSON 的解析以及调用之间的业务逻辑。对于大多数本质上是 I/O 协调器的 Worker 来说,10ms 的限制是很慷慨的。
真正使用 CPU 的是同步密集型操作:应用于大型字符串、具有深层层次结构或大型数组的正则表达式、即使 API 异步时的加密操作(哈希工作在执行期间发生在 CPU 上),以及大型二进制文件上的 Base64 编码/解码。接收 500KB JSON 负载并对其执行 6 操作的 Worker 将在此操作上花费可测量的 CPU 时间 — 解析是同步的。
在付费计划中,CPU 时间的限制高达 30 秒,这足以满足计算密集型用例:压缩、图像生成、小模型推理。但即使是付费的,CPU 操作时间超过几秒也是边缘设计不佳的症状——工作人员针对低延迟进行了优化,而不是针对繁重的处理进行了优化。
CPU 计时器并不是杀死生产中 Worker 的原因
在没有明确警告的情况下,真正让生产中的工人崩溃的是内存。 128MB 的限制似乎是合理的,直到您了解什么才是重要的:V8 堆上的未压缩脚本、自调用开始以来全局范围内存在的所有模块闭包,以及在正在进行的请求处理期间分配的所有内容。
脚本本身可能比您意识到的更耗时。一个压缩后的500KB的Worker,经过V8解压解析后,可能会占用3-4MB。在模块范围内导入的依赖项(验证库、解析器、SDK)只要隔离存在,就会保留在内存中,即使它们没有在当前请求中使用。全局范围在同一 PoP 中的同一隔离按顺序处理的请求之间共享。
最常触发限制的分配是 7 或 8。对 40MB 响应调用此方法会立即分配 40MB 堆。如果 Worker 然后从此缓冲区创建更多数据结构(解析的对象、转换的副本),则在处理结束之前内存使用量可能会超过 128MB。运行时在此时杀死isolate,客户端收到错误1101,并且没有堆栈跟踪——只是在wrangler tail中将运行时错误报告为通用异常。
流式传输作为内存生存策略
解决大有效负载内存问题的方法是永远不要将整个内容具体化为缓冲区。使用 10 代替 9 ,它是 11 ,并使用 12 分块处理数据。
Workers Streams API 遵循 WHATWG Streams 规范,这与现代浏览器中可用的规范相同。 13有14和15:将上游响应的16连接到转换的17,并将转换的18连接到发送给客户端的响应。块流过管道,而内存中不会同时存在整个整数。
这种架构有一个重要的后果:在开始响应之前,您无法再读取整个内容来做出依赖于整个文件的决策。对于需要随机访问的情况(例如,处理需要全局排序的 CSV),Workers 不适合。对于逐块操作的转换(重新压缩、文本替换、行过滤),流管道可以在没有内存压力的情况下解决它。
对于前往 R2 的上传,绑定直接接受 19: 20 — 不缓冲任何东西。请求主体流直接以块的形式发送到 R2。
由于 CPU 使用率而令人惊讶的操作
Base64 编码和解码是 CPU 密集型操作,与数据大小成正比,结果占用了 33% 的内存。如果您正在传输二进制文件,则值得询问是否需要编码; Base64 的许多用途都是旧版 HTTP 限制,不再适用于 fetch 和本机二进制类型。
通过 Web Crypto API 进行的散列操作在签名中是异步的 — 21 — 但散列消耗的 CPU 与数据大小成正比。对每个请求执行 HMAC-SHA256 来验证 Webhook 的工作线程在其上花费了真正的 CPU,这与小负载无关,但显着高于几百 KB。
长字符串上的正则表达式可能会非常昂贵。具有灾难性回溯的模式(多个量词嵌套在同一字符集上)可能会使处理几千字节字符串的 CPU 时间增加三倍。使用实际字符串进行测量,而不是测试中有效的 10 字节输入。
检测资源压力需要注意什么
22 对于每个事件返回 23 — 该调用的 CPU 时间(以毫秒为单位)。结构化地记录该值使我们能够检测回归:将 CPU p99 从 3ms 增加到 12ms 的部署意味着引入了一些新的同步操作。
内存使用情况不会通过 tail 中的调用直接公开。在超过限制之前检测内存压力的方法是观察隔离的行为:如果运行时为同一 PoP 开始比正常情况更频繁地创建新隔离(表现为冷启动的增加),则可能表明隔离已被垃圾收集器提前丢弃或由于内存压力。
在同一 PoP 中的请求之间重用隔离的能力是一项重要的优化:冷启动的成本(解压缩脚本、初始化 V8、执行更高级别的模块代码)发生一次,后续请求将重用已经预热的隔离。请求之间位于内存中的模块闭包是 Workers 中最快的缓存机制——比 KV 更快,比任何子请求都快。
另请阅读
- [生产中的 Cloudflare Workers:hello world 之后发生了什么变化24
- [生产中的 D1:性能、限制以及无法单独扩展的内容25
- [Cloudflare Workers:无服务器边缘计算实用指南26
- [生产中的耐用物品:法案将是什么样子以及令人惊讶的限制27
- [WebAssembly 处于边缘:为什么快速启动和隔离很重要28
- [Workers + D1 + KV + R2:在同一服务中组合绑定29
