隔离模型的硬限额与兼容性日期

隔离不是无限的:limits 表逐项读

上一章我们把 Workers 的隔离机制画成六层叠加——V8 isolate 提供内存边界,cordon 把不同信任等级的租户拆到不同进程,Layer-2 沙箱用 namespaces 加 seccomp 把所有文件系统与网络系统调用屏蔽掉,计时与多线程被刻意关闭,最后是动态进程隔离、周期内存重排、补丁 gap 收敛三道动态防御。但所有这些机制最终都要被一组数字约束。Cloudflare 的 limits 页面是这份数字清单的官方源,下面把它按"读得懂、记得住、用得上"的方式重排,让读者能带着具体数字而不是抽象概念走完这一章。

第一类数字描述单个请求的资源。CPU 时间是这一类里最重要的一项:HTTP 请求默认上限 10 毫秒(Free 计划)/ 30 秒(Paid 计划默认,可上调到 5 分钟也就是 300000 毫秒)。Cron Trigger 的上限则按间隔分档——10 毫秒(Free)/ 30 秒(间隔小于 1 小时)/ 15 分钟(间隔大于等于 1 小时)。这里有一个非显然的细节:等待 fetch、KV 读、数据库查询等 I/O 不计入 CPU 时间——只有 CPU 真正执行的指令才计。这意味着 CPU 时间衡量的不是 wall clock,而是"代码做实际计算消耗"。对读者写代码的直接含义是:你在 await 一个远程 API 时不会消耗 CPU 时间,但 await 醒来之后做 JSON 解析、做正则匹配、做排序——这些会消耗。

内存上限是另一个核心数字:每个 isolate 128 MB,包含 V8 JS 堆加上 WebAssembly 分配。文档原话:"Each isolate can consume up to 128 MB of memory, including the JavaScript heap and WebAssembly allocations. This limit is per-isolate, not per-invocation. A single isolate can handle many concurrent requests." 这里有两点要强调:第一是"per-isolate, not per-invocation"——一个 isolate 同时处理多个并发请求时它们共享这块内存预算;第二是超额处理——"When an isolate exceeds 128 MB, the Workers runtime lets in-flight requests complete and creates a new isolate for subsequent requests. During extremely high load, the runtime may cancel some incoming requests to maintain stability."——超额时 Workers 让在飞请求跑完,然后为后续请求开新 isolate;不会像进程 OOM 那样整台崩。

subrequests 这一档是隔离面里容易被低估的:每次 invocation 50 个(Free)/ 10000 个(Paid,可调);向 Cloudflare 内部服务(KV、R2、D1 等)发的子请求单独有 1000/次的上限。每次 invocation 同时打开的 outbound 连接最多 6 个,并发"等待响应头"的连接计入这一额度。fetch、KV 的 get/put/list/delete、Cache 的 put/match/delete、R2 的 list/get/put/delete/head、Queues 的 send/sendBatch、TCP connect、出站 WebSocket 全部都算。一旦响应头到达、连接进入正文收发阶段,就不再占这 6 个额度。env vars 加 secrets 那一档是每 Worker 64 个(Free)/ 128 个(Paid),单条 5 KB。

第二类数字描述 Worker 自身。压缩后大小 3 MB(Free)/ 10 MB(Paid),未压缩大小上限 64 MB。启动时间是 1 秒——top-level 代码必须在 1 秒 CPU 时间内解析并执行完,超时会触发错误码 10021 中的"Script startup exceeded CPU time limit",wrangler deploy 会自动生成 CPU profile 供排查。账户内 Workers 数上限 100(Free)/ 500(Paid)。每日请求方面,Free 计划 100000/天(UTC 午夜重置),Paid 无硬上限。

第三类数字描述请求与响应本身。URL 大小上限 16 KB;请求头总大小 128 KB;响应头 128 KB;响应体大小"no enforced limit"(受 Cloudflare CDN cache 限制:Free/Pro/Business 512 MB、Enterprise 5 GB)。请求体大小则按账户级别计:Free 100 MB、Pro 100 MB、Business 200 MB、Enterprise 500 MB 默认。

接下来是一个常被新手忽略的细节:limits 页面明确写道,"Each isolate has some built-in flexibility to allow for cases where your Worker infrequently runs over the configured limit. If your Worker starts hitting the limit consistently, its execution will be terminated according to the limit configured."——偶发超出不会立刻报错,但持续超出会被终止,并被记录为 exceededCpu 或 exceededMemory(在 dashboard 与 Logpush 中均可见)。换句话说:"偶发超限不报警"不等于"安全"——它只是给你一次"流量尖峰"的缓冲,让你有时间优化或扩容;但一旦持续超出,故障会以 1102 错误码直接打给客户端。

错误码是把这些数字翻译给客户端的官方语言。最常用的几个值得记牢:1101 表示 Worker 抛了 JS 异常;1102 表示超 CPU 或内存(dashboard 把它记为 Exceeded CPU Time Limits 或 Exceeded Memory,Logpush 的 invocation outcome 是 exceededCpu / exceededMemory);1019 表示循环上限——Worker 自调用或 Worker 链路过深,CF-EW-Via 头值归零即触发;1027 表示 Free 计划的 100k/天超限;10021 是 validation 期的失败码,包含 startup 超 1 秒或 memory 超 128 MB。其它 11xx 错误通常表示 Workers 运行时自身问题,遇到时看 status page。把"看到 5 位数错误码立即回查 limits 表"当成肌肉记忆,能省掉很多凌晨的事故复盘。

把上面这些数字组合起来,可以用一张状态图描述"一个请求在 Workers 内部的限额检查时序":

stateDiagram-v2 [*] --> 启动期 启动期 --> 加载: top-level 代码执行 加载 --> 运行: isolate 加载完 加载 --> 失败_10021: startup CPU > 1s 或 memory > 128MB 运行 --> 等待_响应头: 发起 fetch / KV / R2 / Cache / TCP / WebSocket 等待_响应头 --> 运行: 响应头到达 (释放并发额度) 等待_响应头 --> 排队: 第 7 个并发连接被排队 排队 --> 等待_响应头: 有连接释放 运行 --> 失败_1102: CPU 持续超额 运行 --> 失败_1102m: memory 持续超额 运行 --> 失败_1101: 未捕获 JS 异常 运行 --> waitUntil: handler 返回但有挂起任务 waitUntil --> 结束: 最多 30 秒 waitUntil --> 强杀: 30 秒后或 runtime 重启 失败_1102 --> [*] 失败_1102m --> [*] 失败_1101 --> [*] 失败_10021 --> [*] 结束 --> [*] 强杀 --> [*]

解读这张图有两条主线值得记下。第一条主线是启动期的独立闸门——只要 top-level 代码 1 秒内跑不完,或者它声明的某个常量就把内存推过 128 MB,Worker 还没接流量就被拒收,错误码是 10021。这意味着读者在写项目时应当问自己一句:top-level 是不是在做"应该移到 build time"的事?比如把一个大的 JSON schema 在 top-level 解析、加载本地化资源、用 crypto 算一次性 hash——这些都该移到 build step。第二条主线是6 个并发 outbound 连接的独立闸门——超过 6 的第 7 个请求不会失败,而是被排队等额度。这条闸门解释了为什么 reader 写出"for 循环里同时发 50 个 fetch"会被悄悄限流,而不是报错。

兼容性日期:把运行时升级变成可选项

limits 数字管的是"一次能跑多少",但 Cloudflare 自身也在升级 Workers runtime。Compatibility date 是 Worker 项目告诉运行时"我承诺按这套语义运行"的最重要字段。

Workers 文档原话:"Cloudflare regularly updates the Workers runtime. These updates apply to all Workers globally and should never cause a Worker that is already deployed to stop functioning. Sometimes, though, some changes may be backwards-incompatible. In particular, there might be bugs in the runtime API that existing Workers may inadvertently depend upon. Cloudflare implements bug fixes that new Workers can opt into while existing Workers will continue to see the buggy behavior to prevent breaking deployed Workers."——runtime 一直在更新;对已部署 Worker,Cloudflare 永不主动破坏其行为(除非不得已,会主动联系开发者);破坏性变更的开关是 Worker 项目自己决定是否打开。

开关在 compatibility_date。文档:"There is no need to update your compatibility_date if you do not want to. The Workers runtime will support old compatibility dates forever. If, for some reason, Cloudflare finds it is necessary to make a change that will break live Workers, Cloudflare will actively contact affected developers."——compatibility_date 不强制更新;保留旧 date 的 Worker 永远可以跑。但反过来,文档同样警告:"Sometimes, new features can only be made available to Workers that have a current compatibility_date. To access the latest features, you need to stay up-to-date." 与 "Generally, other than the compatibility flags page, the Workers documentation may only describe the current compatibility_date, omitting information about historical behavior. If your Worker uses an old compatibility_date, you will need to continuously refer to the compatibility flags page in order to check if any of the APIs you are using have changed."

合起来,compatibility date 的工程意义是把"运行时版本"作为 Worker 项目自己的可审计字段。一个项目的 wrangler 配置文件里写着 compatibility_date 是某一天,CI 在每次部署时核对"今天不晚于 date 加 180 天则警告"——这条规则就能在不知不觉中让"运行时的潜在行为变化"变成有节奏的、可在大 PR 里处理的升级,而不是某天早上突然发现线上行为变了。

几个具体细节值得记。第一个细节是默认值:如果你通过 dashboard 创建 Worker,date 自动设为创建当天;如果你通过 API 上传 Worker 而没有指定 date,文档原话是 "it defaults to the oldest compatibility date, before any flags took effect (2021-11-02). When creating new Workers, it is highly recommended to set the compatibility date to the current date when uploading via the API."——永远显式设置这条规则在 API 路径上尤其重要。第二个细节是设了不等于升级:你随时可以更新 compatibility_date 并重新部署;新 date 生效于部署完成那一刻,已经在飞的请求仍按旧 date 跑。第三个细节是配套是 compatibility flags:某些 flag 会在某个特定 date 起默认开启,因此只设 compatibility_date 就能同时启用截止到那一天的所有默认 flag。Flags 页面记录了每条 flag 的默认开启日期——升级前先读它,是避开"语义变化"的标准动作。

一句话总结这一章:limits 表把抽象的隔离翻译成可对照检查的数字;compatibility date 把"运行时升级"翻译成可审计的工程字段。两者合起来,读者才真正知道自己的 Worker 能跑多满、能跑多久、什么时候会被迫升级。

到这里读者已经能在自己项目里逐项核对硬限额,并知道如何把"运行时升级"控制成有节奏的工程动作。但资源与版本只是算力时机——Worker 实际能做什么,还得看 env 这个统一表面背后站着什么。这一章结束前,读者应当会自然产生一个未解决的问题:这些 CPU、内存、连接数都拿到手了,但我的 Worker 究竟如何访问 D1、KV、R2、Durable Objects 这些平台资源?env 这个对象承诺什么、又禁止什么?下一章会把"能力面"打开。

References