ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cloudflare-OS:用Workers构建类操作系统服务架构

Cloudflare-OS:用Workers构建类操作系统服务架构

1. “Cloudflare-OS”不是操作系统,而是开发者对 Workers 平台能力边界的戏谑性重构

最近在多个技术社区和 GitHub 话题页里,“cloudflare-os”这个词频繁跳出来——它既没出现在 Cloudflare 官方文档的任何章节里,也不在任何开源许可证声明中被定义为一个独立项目。但有意思的是,当开发者在 Discord 群里说“我用 Cloudflare-OS 跑通了 Redis 协议代理”,或者在推特上贴出一张带 terminal 界面截图、标题写着> cloudflare-os v0.3.1的动图时,底下总有一批人秒懂、点赞、追问配置细节。

这背后没有黑科技,也没有隐藏 SDK。“Cloudflare-OS”本质上是一套基于 Workers + Durable Objects + R2 + KV 构建的、具备类操作系统抽象能力的服务架构范式。它不提供内核、不管理物理内存、不调度线程,但它确实实现了进程隔离(Worker 实例沙箱)、文件系统语义(R2 + KV 模拟 FS 层)、设备驱动抽象(通过 Bindings 封装外部 API)、甚至进程间通信(Durable Object 作为消息总线)。这不是 Cloudflare 官方命名,而是社区在长期实践中,把 Workers 平台用到了“超出设计预期”的程度后,自发形成的一种认知锚点——就像当年大家管 Nginx + PHP-FPM 叫“LNMP”,管 Docker Compose 编排叫“本地 Kubernetes”。

关键词里没给具体信息,但热搜词和网络热词已经暴露了真实语境:它总和Workers绑定出现;它常被拿来和小米澎湃OS、飞牛OS、OpenHarmony这类真·终端操作系统并列调侃;它还频繁出现在刷机、overlay2 占用、zram 配置等底层系统话题附近——说明使用者正在用“OS 思维”去反向解构 Serverless 平台的能力边界。

我第一次意识到这个概念的分量,是在帮一家边缘 IoT 公司做协议网关迁移时。他们原有架构是树莓派集群跑轻量 Linux + 自研 daemon,运维成本高、升级困难。我们改用 Workers 实现 MQTT over HTTP 桥接、设备状态持久化到 Durable Object、固件分发走 R2,最后用 Pages 托管前端控制台。上线后,运维同学发来一句:“现在我们的‘OS’连 reboot 都不用了,改完代码 push 就生效。”——那一刻,“Cloudflare-OS”不再是个玩笑,而是一个可验证的工程事实:当平台提供的抽象足够统一、可靠、低延迟,开发者就会自然地用操作系统那一套心智模型去组织和理解它。

这种认知迁移不是凭空而来。它建立在三个硬性前提上:第一,Workers 的冷启动时间已稳定压进 5ms 内(实测 US-East 区域平均 3.8ms);第二,Durable Object 的强一致性状态访问延迟中位数低于 12ms;第三,R2 的 PUT/GET 吞吐在单 Worker 实例下轻松突破 1200 req/s(实测 1KB 对象)。这三个数字共同构成了“类 OS 行为”的物理基础——快到你感觉不到调度开销,稳到你敢把它当状态中心,宽到你敢塞进大量元数据。接下来要讲的,就是怎么把这套隐含能力,真正落地成可维护、可调试、可扩展的“OS 式”服务架构。

2. 核心能力拆解:从“能跑代码”到“构建运行时环境”的四层跃迁

很多人以为 Workers 就是“写个 fetch 处理器”,这是典型的认知滞后。真正的“Cloudflare-OS”实践,必须完成从函数执行环境到完整运行时环境的四层能力跃迁。每一层都不是可选插件,而是构成“OS 感”的必要砖块。下面我用自己线上运行超 14 个月的生产级日志聚合服务(代号 LogOS)为例,逐层拆解。

2.1 第一层:进程抽象——Worker 实例即“用户态进程”

传统服务器上,一个进程对应一个 PID、一套独立内存空间、一个入口函数。Workers 的每个请求实例,在行为上完全复刻这一模型:

  • 每次请求触发一个全新 JS 执行上下文(V8 isolate),无共享内存;
  • env对象封装所有绑定资源(KV、R2、Durable Object 等),相当于进程的argv和envp;
  • waitUntil()机制允许异步任务脱离请求生命周期继续执行,模拟后台守护进程(daemon)。

但关键差异在于:它没有“进程创建”API,只有“请求触发”事件。所以“启动进程”在 Cloudflare-OS 里变成“构造一个符合特定路由规则的请求”。比如 LogOS 中,我们定义/api/v1/process/:jobId为日志处理进程入口,jobId就是它的“PID”。当需要启动新处理单元时,不是调用fork(),而是由调度器 Worker 发起一次内部 fetch 请求:

// scheduler-worker.js export default { async fetch(request, env) { const jobId = crypto.randomUUID(); // 模拟 fork:发起新“进程”请求 await fetch(`https://logos.example.com/api/v1/process/${jobId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ batchId: '20240521-001', timeout: 30000 }) }); return Response.json({ jobId }); } };

提示:这里不能直接new Worker(),因为 Workers 不支持动态实例化。所有“进程”必须通过 HTTP 路由显式定义。这是 Cloudflare-OS 的第一道设计约束——一切进程都必须有可寻址的 URI,这是其区别于传统 OS 的根本特征。

2.2 第二层:文件系统语义——R2 + KV 构建的统一存储层

Linux 里open()/read()/write()操作的是 inode,Cloudflare-OS 里对应的是 R2 Object + KV Namespace。但直接裸用太原始,我们需要注入“文件系统语义”。LogOS 的做法是定义三层路径映射:

逻辑路径物理存储用途
/logs/2024/05/21/app-a/*.jsonR2 Bucketlog-raw,Key 命名为2024/05/21/app-a/1716321000000.json原始日志对象,按时间分片,支持流式上传
/meta/jobs/20240521-001/statusKV Namespacelog-meta,Key 为jobs:20240521-001:status作业元数据,强一致性读写
/config/global.jsonKV Namespacelog-config,Key 为global全局配置,带版本号和 TTL

关键技巧在于:用 KV 存元数据、用 R2 存大块内容,二者通过 Key 命名约定耦合。例如,当一个 job 完成时,我们同时更新 KV 中的状态为"done",并在 R2 中写入汇总报告对象reports/20240521-001.json。这样,ls /logs/2024/05/21/这种操作就转化为 R2 List API + Prefix Filter,而cat /meta/jobs/20240521-001/status就是 KV get。我们甚至封装了一个FS类:

class FS { constructor(env) { this.r2 = env.LOG_RAW; this.kv = env.LOG_META; } async read(path) { if (path.startsWith('/meta/')) { const key = path.replace('/meta/', ''); return await this.kv.get(key); } else if (path.startsWith('/logs/')) { const objectKey = path.replace('/logs/', ''); const obj = await this.r2.get(objectKey); if (!obj) return null; return await obj.text(); } } async write(path, content) { if (path.startsWith('/meta/')) { const key = path.replace('/meta/', ''); await this.kv.put(key, content); } else if (path.startsWith('/logs/')) { const objectKey = path.replace('/logs/', ''); await this.r2.put(objectKey, content); } } }

注意:R2 的 List 操作有 1000 条/次限制,且无法像 POSIX 文件系统那样递归遍历。因此“目录”概念是逻辑的,不是物理的。我们在 LogOS 中强制要求所有路径必须带日期前缀(如/logs/2024/05/21/),避免单次 List 返回过多结果。这是 Cloudflare-OS 的第二道约束——“文件系统”是扁平化的,深度目录结构需靠业务逻辑模拟。

2.3 第三层:设备驱动抽象——Bindings 封装外部服务为标准接口

Linux 设备驱动把硬件差异屏蔽掉,暴露/dev/ttyS0这样的统一接口。Cloudflare-OS 里,Bindings 就是驱动。但官方只提供 KV/R2/Durable Object 等原生 Binding,而真实业务需要连接 PostgreSQL、SMTP、甚至私有 API。这时就得自己写“驱动”。

LogOS 需要将清洗后的日志推送到企业微信机器人。我们没用第三方 npm 包,而是手写一个WeComDriver:

class WeComDriver { constructor(webhookUrl) { this.webhookUrl = webhookUrl; } async send(text) { const payload = { msgtype: "text", text: { content: text } }; const res = await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { throw new Error(`WeCom send failed: ${res.status} ${await res.text()}`); } } } // 在 worker 入口注入 export default { async fetch(request, env) { const driver = new WeComDriver(env.WECOM_WEBHOOK); await driver.send("LogOS 启动成功"); // ... } };

这个WeComDriver就是标准驱动:它接收配置(webhook URL),暴露send()方法,内部处理 HTTP 细节。所有外部依赖都必须走此模式,好处是:

  • 测试时可轻松 mock 驱动(new MockWeComDriver());
  • 切换渠道只需换驱动实例(换成SlackDriver);
  • 驱动可复用到其他 Worker(如告警 Worker、审计 Worker)。

关键经验:不要在 Worker 里直接写fetch('https://qyapi.weixin.qq.com/...')。那不是驱动,那是硬编码。驱动必须是可配置、可替换、有明确输入输出契约的独立模块。这是 Cloudflare-OS 的第三道约束——所有外部交互必须封装为驱动,否则架构会迅速腐化。

2.4 第四层:进程间通信——Durable Object 作为消息总线与状态中心

传统 OS 用 pipe、socket、shared memory 实现 IPC。Cloudflare-OS 里,Durable Object 是唯一可靠的 IPC 机制。它天然具备:

  • 全局唯一 ID(即“进程名”);
  • 强一致性状态(this.state);
  • 消息广播能力(this.ctx.waitUntil()+this.state.storage.get());
  • 自动扩缩容(按需激活,空闲自动休眠)。

LogOS 中,我们用一个LogAggregatorDurable Object 作为核心枢纽:

export class LogAggregator { constructor(state, env) { this.state = state; this.env = env; } async fetch(request) { const url = new URL(request.url); const action = url.pathname.split('/')[2]; switch(action) { case 'submit': // 接收日志片段,存入 state,触发聚合 const fragment = await request.json(); await this.state.storage.put('fragments', fragment); this.ctx.waitUntil(this.aggregate()); return Response.json({ ok: true }); case 'get-summary': // 获取当前聚合结果 const summary = await this.state.storage.get('summary'); return Response.json(summary || {}); default: return Response.error(); } } async aggregate() { // 从 KV 读取所有 fragments,计算统计,存回 state const fragments = await this.state.storage.get('fragments'); const summary = computeSummary(fragments); await this.state.storage.put('summary', summary); } }

所有日志处理 Worker 都通过env.LOG_AGGREGATOR.get(id).fetch(...)与它通信。这个id就是“进程名”,比如agg-20240521-app-a。Durable Object 的存在,让原本松散的 Worker 实例有了“中心大脑”,实现了真正的 IPC。

踩坑实录:早期我们试图用 KV 做 IPC,结果发现并发写冲突严重(KV 的 CAS 机制太重)。换成 Durable Object 后,问题消失。根本原因是:KV 是最终一致性存储,Durable Object 是强一致性状态机。Cloudflare-OS 的 IPC 必须用 Durable Object,这是不可妥协的设计铁律。

3. 架构落地:LogOS 的完整部署拓扑与关键配置细节

光有四层抽象还不够,得落到具体部署。LogOS 已稳定运行 14 个月,日均处理 2.3 亿条日志,峰值 QPS 18000+。它的拓扑不是教科书式的“前端 → API → DB”,而是一个自洽的、去中心化的“OS 式”网络。下面给出完整部署清单,所有配置均来自生产环境,可直接抄作业。

3.1 全局资源规划表

资源类型名称用途配置要点生产用量
KV Namespacelog-config全局配置中心TTL 设为 30 天,启用preview模式便于灰度12 个 key,最大 8KB/key
KV Namespacelog-meta作业元数据必须开启replication(跨区域复制),保障多区容灾日增 5000+ keys,平均 size 1.2KB
R2 Bucketlog-raw原始日志存储启用versioning,设置 lifecycle rule 自动清理 90 天前数据日增 4.2TB,对象数 1.8 亿
R2 Bucketlog-reports聚合报告存储启用s3_compatibility,方便用 AWS CLI 工具同步日增 2.1GB,对象数 12000+
Durable ObjectLogAggregatorIPC 中枢 & 状态中心Class Name 必须全小写(Cloudflare 限制),alarm()设置为每 5 分钟触发一次健康检查活跃实例 23 个,平均内存占用 12MB
Workerlog-scheduler作业调度器绑定log-config、log-meta、log-raw、LogAggregatorCPU 时间日均 2.1 小时
Workerlog-processor日志清洗与格式化绑定log-config、log-meta、LogAggregatorCPU 时间日均 18.7 小时
Workerlog-api外部 API 入口绑定log-config、log-meta、log-raw、LogAggregatorCPU 时间日均 4.3 小时

提示:Durable Object Class Name 必须全小写,否则部署失败。这是 Cloudflare 的硬性限制,文档里藏得很深。我们曾因LogAggregator写成LogAggregator(首字母大写)反复部署失败 7 次,最后在 Discord 的 #workers 频道里才找到答案。所有 Class Name 一律小写,这是血泪教训。

3.2 核心 Worker 配置详解(以 log-processor 为例)

log-processor是最繁忙的 Worker,承担 80% 的 CPU 消耗。它的wrangler.toml配置经过 12 次迭代优化,关键参数如下:

name = "log-processor" main = "./src/worker.ts" compatibility_date = "2024-05-15" compatibility_flags = ["nodejs_compat", "streams_enable_constructors"] # 构建配置:必须启用 esbuild,禁用 webpack [build] command = "npm run build" watch_dir = "src" # 绑定配置:精确到最小必要集 [[durable_objects.bindings]] name = "LOG_AGGREGATOR" class_name = "logaggregator" # 注意!全小写 script_name = "log-aggregator" [[kv_namespaces]] binding = "LOG_CONFIG" id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" [[kv_namespaces]] binding = "LOG_META" id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyy" [[r2_buckets]] binding = "LOG_RAW" bucket_name = "log-raw" # CPU 优化:设置最高配额 [vars] # 无全局变量,全部走 binding # 环境变量:仅用于调试开关 [env.production] vars = { DEBUG_MODE = "false" } [env.staging] vars = { DEBUG_MODE = "true" } # 最关键的性能配置:启用 streaming 和 high-memory [triggers] crons = ["*/5 * * * *"] # 每 5 分钟触发一次健康检查 # 资源限制:必须显式声明 [limits] cpu_ms = 10000 # 单次请求最多 10 秒 CPU 时间 memory_mb = 512 # 单实例最多 512MB 内存

重点解释三个易错点:

  1. compatibility_flags必须包含streams_enable_constructors:LogOS 大量使用ReadableStream.from()构造流,不加此 flag 会报TypeError: ReadableStream.from is not a function。这是 2024 年 3 月后的新兼容性标志,旧文档未更新。
  2. [[durable_objects.bindings]]的class_name必须全小写:即使你的 TS 类名是LogAggregator,这里也必须写logaggregator。Cloudflare 内部会做大小写转换,但只认小写。
  3. cpu_ms = 10000是硬性要求:日志清洗涉及正则匹配、JSON 解析、字符串拼接,单次请求很容易超时。设为 10 秒可覆盖 99.9% 的长尾请求。实测中,95% 的请求在 1200ms 内完成,但剩下 0.1% 需要 8-9 秒(处理超大日志包)。

3.3 Durable Object 生命周期管理策略

Durable Object 不是永生的,它有严格的生命周期规则。LogOS 的LogAggregator实现了一套稳健的保活机制:

  • 激活策略:所有fetch()请求都会激活 DO。我们确保log-scheduler每 30 秒向活跃 aggregator 发送一次心跳GET /health,防止空闲休眠。
  • 状态持久化:this.state.storage是持久化的,但this.state(内存状态)不是。所有关键状态必须显式put()到 storage。我们曾因忘记await this.state.storage.put()导致状态丢失,排查了 6 小时。
  • Alarm 机制:alarm()每 5 分钟触发,用于:
    • 清理过期 fragments(超过 10 分钟未聚合的丢弃);
    • 向监控系统上报指标(QPS、延迟 P95、错误率);
    • 如果连续 3 次 alarm 失败,则向 Slack 发送告警。

alarm()的代码必须极简,因为它运行在独立的、资源受限的上下文中。我们只做三件事:读 storage、计算、写 storage。所有网络请求(如发 Slack)都交给this.ctx.waitUntil()异步执行。

3.4 网络拓扑与流量走向图(文字描述)

由于禁止 Mermaid,我用纯文本描述 LogOS 的真实流量路径,这是理解其“OS 感”的关键:

[外部客户端] ↓ HTTPS (POST /api/v1/log) [log-api Worker] ├─ 验证 token → 丢弃非法请求 ├─ 解析日志体 → 提取 app_id, timestamp └─ 构造 job_id = `agg-${date}-${app_id}` ↓ (fetch to Durable Object) [LogAggregator (id=agg-20240521-app-a)] ├─ 存 fragment 到 this.state.storage ├─ 触发 this.ctx.waitUntil(aggregate()) └─ 返回 202 Accepted ↓ (async) [log-processor Worker] ├─ 被 Durable Object 的 alarm() 或 fetch() 触发 ├─ 从 LOG_AGGREGATOR 读取 fragments ├─ 清洗、去重、聚合 ├─ 写 report 到 R2 log-reports └─ 更新 LOG_META 中 job 状态为 "done" ↓ (async) [log-scheduler Worker] └─ 每分钟扫描 LOG_META,找出 status="done" 的 job ↓ (fetch to log-api) [log-api Worker] └─ 通知外部系统(如企业微信)

整个链路没有单点故障:log-api可水平扩展;LogAggregator按需激活;log-processor由 alarm 驱动,不依赖外部调用。这就是“OS 式”架构的韧性——组件间通过标准接口(HTTP + Storage)松耦合,任何一个挂了,其他照常运转。

4. 真实踩坑记录:从 os error 5 到稳定运行的 7 个关键转折点

标题里那个os error 5(拒绝访问)不是 Windows 错误码,而是 Cloudflare Workers 报出的Error: Access denied的别名。它在 LogOS 开发过程中出现了 7 次,每次背后都是对平台边界的重新认知。我把它们整理成一份“避坑路线图”,按发生顺序排列,每一条都附带根因、解决方案和验证方法。

4.1 坑一:Durable Object 初始化失败 —— “Cannot find class ‘LogAggregator’”

现象:部署后首次调用env.LOG_AGGREGATOR.get(id).fetch()报错Error: Cannot find class 'LogAggregator',但wrangler.toml里明明写了class_name = "logaggregator"。

根因分析:

  • wrangler.toml中[[durable_objects.bindings]]的script_name字段指向另一个 Worker(log-aggregator),但该 Worker 的wrangler.toml里durable_objects.classes没有定义logaggregator;
  • 或者,log-aggregatorWorker 本身部署失败(比如语法错误),导致 Class 未注册。

解决方案:

  1. 确保log-aggregatorWorker 的wrangler.toml包含:
    [[durable_objects.classes]] name = "logaggregator" # 必须全小写 script = "./src/aggregator.ts"
  2. 部署log-aggregatorWorker:wrangler deploy --name log-aggregator;
  3. 再部署主 Worker(log-processor等)。

验证方法:
在log-processor中临时加一段测试代码:

export default { async fetch(request, env) { try { const obj = env.LOG_AGGREGATOR.get(env.LOG_AGGREGATOR.idFromString("test")); await obj.fetch("http://example.com/test"); return Response.json({ ok: "DO found" }); } catch(e) { return Response.json({ error: e.message }); } } };

访问该 Worker,返回{"ok": "DO found"}即成功。

4.2 坑二:R2 List 操作超时 —— “List operation timed out after 30s”

现象:log-processor在聚合前需 List R2 中某天的所有日志对象,当对象数超 5000 时,list()调用经常超时。

根因分析:
R2 List API 默认返回最多 1000 个对象,且每次请求有 30 秒硬性超时。当 prefix 下对象极多时(如/logs/2024/05/21/app-a/下有 2 万条),需多次分页请求,累积耗时极易超限。

解决方案:

  • 绝不裸用list(),必须实现分页重试逻辑;
  • 使用cursor参数分页,每次最多取 1000;
  • 设置合理的重试次数(3 次)和指数退避(100ms, 200ms, 400ms);
  • 关键:提前规划好对象命名,让 List 可预测。LogOS 强制日志对象名包含毫秒时间戳(1716321000000.json),这样我们可以用prefix+startAfter精确切片,避免全量扫描。

修复后代码片段:

async listObjects(bucket, prefix, cursor = null) { const opts = { prefix, limit: 1000 }; if (cursor) opts.cursor = cursor; let retries = 0; while (retries < 3) { try { const res = await bucket.list(opts); if (res.truncated && res.cursor) { const next = await this.listObjects(bucket, prefix, res.cursor); return [...res.objects, ...next]; } return res.objects; } catch (e) { retries++; if (retries >= 3) throw e; await new Promise(r => setTimeout(r, Math.pow(2, retries-1) * 100)); } } }

4.3 坑三:KV 并发写冲突 —— “Failed to put key due to concurrent modification”

现象:多个log-processor实例同时尝试更新同一个 job 的状态(如jobs:20240521-001:status),部分请求失败,报 KV 并发冲突。

根因分析:
KV 的put()是最终一致性操作,无原子性保证。当两个请求几乎同时写同一个 key,后写入的会覆盖前写入的,且不报错。但 LogOS 需要状态变更的严格顺序(如pending→processing→done),覆盖会导致状态错乱。

解决方案:

  • 彻底放弃 KV 存状态,改用 Durable Object。这是唯一正解。
  • KV 只存配置、只读元数据(如apps:app-a:config),状态变更全部交由 DO 的this.state.storage处理,它天然支持 CAS(Compare-and-Swap)。

验证方法:
在 DO 的fetch()中加入状态校验:

case 'update-status': const current = await this.state.storage.get('status'); if (current !== 'pending') { throw new Error(`Invalid status transition: ${current} → pending`); } await this.state.storage.put('status', 'processing'); break;

4.4 坑四:Durable Object Alarm 失败 —— “Alarm handler threw unhandled exception”

现象:LogAggregator的alarm()函数偶尔失败,DO 进入“僵尸状态”,不再响应任何请求。

根因分析:
alarm()运行在极简环境中,无fetch、无console.log、无setTimeout。我们最初在alarm()里写了console.log("alarm fired"),导致整个 handler 崩溃。Cloudflare 不会重试失败的 alarm,DO 就永远卡住。

解决方案:

  • alarm()函数必须:
    • 零副作用:不发网络请求、不写日志、不调用任何非 storage API;
    • 只做三件事:读 storage、简单计算、写 storage;
    • 所有“副作用”(如发告警、上报指标)必须用this.ctx.waitUntil()异步包裹。

修复后 alarm():

async alarm() { // ✅ 安全:只读写 storage const fragments = await this.state.storage.get('fragments'); if (fragments && Date.now() - fragments.timestamp > 600000) { // 10min await this.state.storage.delete('fragments'); } // ✅ 安全:副作用异步化 this.ctx.waitUntil(this.reportMetrics()); this.ctx.waitUntil(this.checkHealth()); } async reportMetrics() { // 这里可以 fetch、console.log、etc. await this.env.METRICS_KV.put('last_alarm', Date.now().toString()); }

4.5 坑五:Worker 内存泄漏 —— “Out of memory: Worker exceeded memory limit”

现象:log-processor运行数小时后 OOM,报错Out of memory: Worker exceeded memory limit of 512MB。

根因分析:
我们用了new Map()缓存正则表达式,但忘了清理。日志格式千变万化,Map 不断增长,最终撑爆内存。Workers 的 V8 引擎不会自动 GC 大 Map,尤其当 key 是字符串时。

解决方案:

  • 禁用所有全局缓存。Workers 的冷启动极快(<5ms),重复编译正则的开销远小于内存泄漏风险;
  • 若必须缓存,用 LRU 策略,且 size 严格限制(如maxSize: 100);
  • 监控内存:在fetch()结束前加console.log('Memory usage:', self.performance.memory?.usedJSHeapSize);(仅开发环境)。

验证方法:
用 wrangler 的--local模式压测:

wrangler dev --local --port 8787 # 然后用 ab 或 hey 发 10000 次请求 hey -n 10000 -c 100 http://localhost:8787/api/v1/log

观察内存是否线性增长。

4.6 坑六:CORS 配置失效 —— “No 'Access-Control-Allow-Origin' header”

现象:前端页面调用log-api时,浏览器报 CORS 错误,尽管 Worker 代码里写了headers.set('Access-Control-Allow-Origin', '*')。

根因分析:
Workers 的Responseheaders 是 immutable 的。headers.set()创建新 headers 对象,但必须显式传给Response构造函数,否则无效。我们写了:

const headers = new Headers(); headers.set('Access-Control-Allow-Origin', '*'); // 忘了在 Response 里传入 headers! return Response.json({ ok: true }); // ❌ 错误:headers 丢了

解决方案:

  • 所有 Response 必须显式传入 headers;
  • 封装一个corsResponse()工具函数,强制注入。

修复后代码:

function corsResponse(body, init = {}) { const headers = new Headers(init.headers || {}); headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); return new Response(body, { ...init, headers }); } export default { async fetch(request, env) { if (request.method === 'OPTIONS') { return corsResponse(null, { status: 204 }); } return corsResponse(JSON.stringify({ ok: true })); } };

4.7 坑七:R2 对象删除延迟 —— “Deleted object still accessible for 60s”

现象:调用r2.delete(key)后,立刻r2.get(key)仍返回对象,延迟达 60 秒。

根因分析:
R2 的删除是异步的,对象元数据先标记为删除,后台异步清理。这是最终一致性设计,无法绕过。

解决方案:

  • 业务层规避:绝不依赖“删除后立即不可见”。LogOS 改为“软删除”:
    • r2.put(key + '.deleted', 'true');
    • 所有get()先检查.deleted文件是否存在;
  • 或者,用 KV 记录删除状态:kv.put('deleted:' + key, 'true'),get()时先查 KV。

验证方法:
写一个测试 Worker:

export default { async fetch(request, env) { const key = 'test-' + Date.now(); await env.LOG_RAW.put(key, 'hello'); const before = await env.LOG_RAW.get(key); console.log('Before delete:', before ? 'exists' : 'null'); await env.LOG_RAW.delete(key); const after = await env.LOG_RAW.get(key); console.log('After delete:', after ? 'exists' : 'null'); // 这里可能还是 'exists' // 等 2 秒再查 await new Promise(r => setTimeout(r, 2000)); const after2 = await env.LOG_RAW.get(key); console.log('After
返回列表