跳至正文
Cloudflare — Durable Object Facets:概念、使用场景与 Durable Objects 对比

Durable Object Facets:概念、使用场景与 Durable Objects 对比

AI 参与说明(Agent:Codex):本文由 Codex 根据作者提供的主题与 Cloudflare、Turso 官方文档调研、整理和校验。本文按 2026-08-23 可访问的资料编写;Dynamic Workers、Durable Objects 的 Beta 状态、限额与计费均可能变化,部署前请复核文末一手资料。

结论:Durable Object Facets 不是 Durable Objects 的替代品,也不是另一种独立数据库产品。它是一个让父 Durable Object(supervisor)托管子 Durable Object class 的组合能力:子对象可以是运行时通过 Dynamic Workers 加载的代码,并拥有与父对象彼此隔离的 SQLite 数据库。它解决的核心问题是:既要让 AI 或用户生成的代码保留持久状态,又不把一个可自由创建的 Durable Object namespace、宿主数据或平台权限直接交给这段代码。

如果代码由自己部署、业务实体需要独立寻址和分片,例如聊天室、库存项、限流桶或协作房间,通常继续选 普通 Durable Objects。只有在“动态/可扩展代码 + 隔离状态 + 父级治理”同时成立时,Facets 才是更贴切的模型。

Cloudflare 在 2026-04-13 的发布公告中把 Facets 称为 open beta;当前 Dynamic Workers 价格页说明 Dynamic Workers 仅向 Workers Paid 提供。因此不应把它当作已经承诺长期稳定 API 的 GA 基础设施;上线前需再次核对状态与价格。发布公告 当前价格页

先建立正确的心智模型

Facets 的典型架构有三层:

  1. Supervisor:由平台开发者编写、部署并拥有 namespace 的普通 SQLite-backed Durable Object。它保存授权、代码版本、配额或审计元数据。
  2. Dynamic Worker code:通过 Worker Loader API 在运行时加载,导出一个继承 DurableObject 的 class。
  3. Facet:由 supervisor 内的 this.ctx.facets.get() 创建或恢复的子对象。每个 facet name 都对应一个独立 SQLite 数据库。
flowchart TD
  C["客户端 / 入口 Worker"] --> S["Supervisor Durable Object<br/>认证、配额、代码版本与审计"]
  S --> PS["Supervisor SQLite<br/>元数据、ACL、账单计数"]
  S --> F["ctx.facets.get(name, factory)"]
  F --> DW["Dynamic Worker 中导出的<br/>DurableObject class"]
  DW --> FS["Facet SQLite<br/>业务状态"]
  S --> R["facet.fetch() 或 RPC 转发"]
  R --> F

父数据库与每个 facet 数据库属于同一个 overall Durable Object,但它们是相互隔离的:动态代码不能读取或修改 supervisor 数据库,supervisor 也不能通过 storage API 直接读取 facet 数据库。两侧若需要交换信息,应经过显式的 fetch()、RPC 或宿主授予的 capability,而不是共用一张表。官方概念与 API

官方文档把 Dynamic Workers 作为主要入口,但 ctx.facets 是 Durable Object 的 child/composition API,而不是仅限“AI 代码”的营销术语。例如 Cloudflare Agents SDK 的 Sub-agents 也使用 facets:只有顶层 parent 需要 namespace,child 可以作为 facet-only class 并拥有独立 SQLite。Cloudflare Agents SDK:Sub-agents

与普通 Durable Objects 的区别

维度普通 Durable ObjectDurable Object Facet工程含义
产品定位部署的 class + top-level Durable Object namespace。由父 DO 托管的 child class;官方主场景是 Dynamic Workers。Facet 是组合层,不是 DO v2。
代码来源随 Worker 部署,通常由平台开发者完全信任。可在运行时从 Dynamic Worker 取得 class。特别适合 AI 生成、用户上传或按租户变化的代码。
寻址入口调用方通过 namespace、ID 与 stub 找到一个业务对象。由父对象调用 ctx.facets.get(name, factory);没有单独的 top-level namespace。外部请求先经过 supervisor,便于认证、审计和限额。
存储边界每个顶层 DO 有自己的私有 storage。每个 facet name 有独立 SQLite;与父数据库共同存放在一个 overall DO 内。状态隔离很强,但不要把 facet 当成独立地域放置或独立扩缩容单元。后半句是由“同一 overall DO”推导出的工程约束,应压测验证。
身份自己的 ctx.id 来自 namespace ID。未提供 id 时,facet 继承 parent 的 ctx.id;可在 startup options 中显式提供 ID。依赖对象 name/ID 的框架需显式设计子身份,不能默认它天然独立。
安全模型class 通常与 Worker 的 bindings、代码信任边界一致。SQLite 隔离;但网络与 bindings 是否可用仍取决于 Dynamic Worker 的 capability 配置。数据库隔离不等于执行权限隔离。对不可信代码应默认 globalOutbound: null。
生命周期按普通 DO 的部署、唤醒与 storage 生命周期管理。get() 创建/恢复;abort() 停止运行但保留数据库;delete() 停止并永久删除数据库。可以热切换 child class,但 schema/data 兼容性仍由应用负责。
Alarm每个顶层 DO 有一个原生 Alarm。Facet 没有独立 physical Alarm slot;Cloudflare Agents SDK 由 root parent 持有 Alarm 并转发子任务。需要独立原生 Alarm 的业务实体,应优先建成顶层 DO 或让父对象统一调度。
成本与成熟度按 Durable Objects 的用量模型评估。采用 Dynamic Workers 的 Facet 模式还要纳入唯一代码、请求与 CPU 维度。不要假定一个 facet 等于一份独立 DO 配额或固定成本;需要用真实流量压测和监控。

前七行均来自 Facets 官方文档;Alarm 的实现说明来自 Agents SDK Sub-agents 文档。Dynamic Worker 的出站与 capability 安全边界见 Bindings 和 Egress control。

与 Turso 的区别、组合方式与选型

Facet 与 Turso 不是两个可以直接互换的 SQLite 方案。Facet 把“可运行的 child Durable Object class”和它的私有状态放进一个受 supervisor 治理的 overall Durable Object;Turso 则把数据库作为可独立访问的服务。前者的重点是动态代码的最小权限、生命周期和局部状态隔离,后者的重点是可由多个可信运行时访问的 SQL 数据边界。

这里的 Turso 指托管的远程 Turso database,而不是把旧的 libSQL client path 一概等同于它。Turso 当前文档区分新版 Turso Database 的 @tursodatabase/serverless 和仍受支持的 @libsql/client:前者用于远程 Turso database,后者主要用于远程 libSQL database 或 ORM 集成。这个产品/驱动差异不改变下面的架构结论:两者都是 Facet 之外、经网络访问的数据库,而不是 Facet 的独立 namespace 或 child runtime。Turso TypeScript SDK reference

维度Durable Object FacetTurso
核心抽象由 parent DO 管理的 child class:它既执行代码,也拥有独立 SQLite。由应用通过 SQL 客户端访问的外部数据库;自身不运行应用的 Durable Object class。
所属边界每个 facet name 有隔离 SQLite,但与 parent 同属一个 overall DO,且没有独立 top-level namespace。数据库是独立的数据服务边界;持有相应凭据的可信服务可以访问同一 schema。
调用路径parent 通过 ctx.facets.get() 获得 stub,再用 fetch() 或 RPC 调用 child。在 Workers 中,远程 Turso database 的官方 driver @tursodatabase/serverless 只依赖 fetch;现有 libSQL 应用使用 @libsql/client/web。
数据共享与查询parent/facet 之间不能通过 storage API 直接读取对方的数据库;跨 facet 的共享须经 parent/RPC 明确设计。多个服务可在同一个数据库 schema 内执行 SQL、事务和查询;适合作为跨 parent DO、Worker 或其他可信后端共享的业务数据源。
动态代码与权限可让 supervisor 保存 ACL、代码版本、限额和审计,并只向 child 授予窄 capability。远程访问需要网络和数据库凭据;把宽权限 token 直接给不可信动态代码,就会绕开 supervisor 的治理边界。
生命周期与运维abort()/delete() 面向 child 的代码与隔离存储生命周期。应分别管理数据库、schema migration、访问凭据和数据生命周期;它们不由 Facet API 接管。
本地副本使用 Cloudflare 托管的 Durable Object SQLite,不是应用可持有的数据库文件。Turso 的 Embedded Replicas 依赖本地文件系统;官方明确指出没有文件系统的 serverless 环境不能使用它,不能把它当作 Workers/Facet 内的本地副本优化。

Turso 的 database group 有 primary region;从 Cloudflare Workers 调用它仍是远程数据库 I/O。因此 Facet 调用与 Turso 查询的延迟、故障边界和成本不能凭“都用 SQLite”类比,更不能宣传为“全球零延迟”;应按目标 primary region、读写比例、连接方式和 p95/p99 实测判断。Turso 的 edge/serverless driver Turso database group 的 primary region

两者组合时:让 supervisor 成为可信的 Turso gateway

Facet 和 Turso 可以同时使用,但应把状态职责拆开:Facet 保存只属于动态 child 的会话、草稿、临时工具状态或局部协调状态;Turso 保存需要被多个 parent DO、Worker、后端服务或设备访问的长期业务数据。不要因为两侧都与 SQLite 有关,就把同一份权威状态无规则地重复写入两边。

flowchart TD
  C["客户端 / 入口 Worker"] --> S["Supervisor Durable Object<br/>认证、配额、审计与租户边界"]
  S --> F["Facet<br/>动态代码与隔离 SQLite"]
  F -. "受控 RPC:请求已授权操作" .-> S
  S --> G["可信 Turso gateway<br/>参数校验、租户过滤"]
  G --> T["Turso database<br/>跨运行时共享数据"]

最关键的安全规则是:不要把 Turso Platform API token、长期 database token 或任意 SQL 能力直接放进不可信 facet。即使把 token 缩小权限,持有它的动态代码仍能执行其获准范围内的 SQL。保留 globalOutbound: null,由 facet 通过 parent 暴露的窄 RPC 请求诸如“读取当前 tenant 的公开配置”或“写入已校验事件”;再由可信 supervisor/gateway 持有凭据、绑定 tenant 与操作范围。这是把 Dynamic Workers 的 capability 设计延续到外部数据库的一种工程实践。Dynamic Workers bindings Turso SQL over HTTP 与 database token

Facet SQLite 与 Turso 之间也没有跨产品的原子事务。一次业务动作若同时写入两边,应以幂等 command、outbox、重试和对账来设计,而不是假设“先 await Facet 写入、再 await Turso 写入”可以一起提交或一起回滚;这是由两个独立持久化边界得出的工程约束。

快速判断

  • 选普通 Durable Object:代码可信且静态,核心是按 room、document、tenant 等对象进行状态化协调、路由或独立分片。
  • 选 Facet:核心是 AI/用户生成代码、插件或 sub-agent 的隔离状态与受控执行;平台必须保留授权、代码版本、配额和审计。
  • 选 Turso:核心是独立的共享数据库,需要被多个可信服务/运行时访问,或需要数据库级的 schema、凭据和数据运维边界。
  • 两者组合:既需要不可信 child 的局部持久状态,又需要跨应用/跨服务共享的业务数据。保留 parent DO 作为租户、权限和一致性边界,Turso 作为受控的外部数据面。

最小工作路径

以下示例使用 JavaScript,结构与 Cloudflare 当前 Facets 指南一致,但没有在本仓库部署到 Cloudflare account。它展示的是 API 关系;生产代码还必须完成鉴权、代码来源校验、限额、日志和 schema migration。

先配置一个 SQLite-backed supervisor 与 Worker Loader。这个配置沿用 Facets 指南中的 migrations 写法;新项目也可以按 Durable Objects class lifecycle 文档 使用 exports,但两种生命周期配置不能混用。

jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "compatibility_date": "2026-08-23",
  "main": "src/index.js",
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["AppRunner"]
    }
  ],
  "worker_loaders": [{ "binding": "LOADER" }]
}

随后由 AppRunner 加载 dynamic class,并将请求转发给名为 app 的 facet:

js
import { DurableObject } from "cloudflare:workers";

const APP_SOURCE = [
  'import { DurableObject } from "cloudflare:workers";',
  "export class App extends DurableObject {",
  "  fetch() {",
  '    const count = (this.ctx.storage.kv.get("count") ?? 0) + 1;',
  '    this.ctx.storage.kv.put("count", count);',
  "    return Response.json({ count });",
  "  }",
  "}",
].join("\n");

export class AppRunner extends DurableObject {
  async fetch(request) {
    const facet = this.ctx.facets.get("app", async () => {
      const version = this.ctx.storage.kv.get("codeVersion") ?? "v1";
      const worker = this.env.LOADER.get(
        "app:" + this.ctx.id.toString() + ":" + version,
        async () => ({
          compatibilityDate: "2026-08-23",
          mainModule: "app.js",
          modules: { "app.js": APP_SOURCE },
          globalOutbound: null,
        }),
      );

      return { class: worker.getDurableObjectClass("App") };
    });

    return facet.fetch(request);
  }
}

export default {
  async fetch(request, _env, ctx) {
    return ctx.exports.AppRunner.getByName("demo").fetch(request);
  },
};

这里有四个容易遗漏的事实:

  • get(“app”, factory) 首次启动或 facet 从 hibernation 恢复时才调用 factory;如果 facet 仍在运行,会复用现有实例而不再次运行 factory。
  • 返回值像普通 DO stub 一样,可调用 fetch(),也可调用 RPC 方法。
  • “app” 是 facet 的存储名称;同一父对象里不同 name 会得到不同的 SQLite 数据库。
  • globalOutbound: null 只切断默认全局出站网络,不会替代对自定义 binding、参数、存储配额和业务授权的审查。

更新、停止与删除不是同一件事

Facet API 将三种操作分得很清楚:

js
// 停止旧代码;旧 stub 会失效,但 SQLite 数据仍保留。
this.ctx.facets.abort("app", new Error("switching to code v2"));

// 下次 get() 会按 factory 重新启动,可返回新的 class。
const next = this.ctx.facets.get("app", factoryForV2);

// 停止并永久清空该 facet 的 SQLite 数据库。
this.ctx.facets.delete("app");

因此,安全的更新流程通常是:在 supervisor 数据库保存不可变的 code version 与 source/bundle → 让新的 Dynamic Worker ID 同时包含版本、内容 hash 和权限版本 → 做好数据库兼容迁移 → abort() 旧 facet → 由下一次 get() 启动新版本。abort() 不会做数据迁移,delete() 则不可逆;这两点应成为运维 API 的明确语义。

Dynamic Workers 的价格页明确指出:同一个 ID 配上不同代码版本会按不同 Dynamic Worker 计数,而 load() 每次调用都会创建一个新的 Dynamic Worker。对可复用版本使用稳定且不可变的 get(id),既使恢复语义可预测,也避免不必要的创建计数。Dynamic Workers pricing

Facets 真正适合哪些场景

1. AI 生成的小应用或个人工具

这是官方的典型案例。平台为每个应用创建一个 AppRunner,把代码版本、权限、用量、审计日志留在父数据库;AI 生成的 App 则只能读写自己的 facet SQLite。平台既能让应用有计数器、草稿、会话等持久状态,又不用把一整个 DO namespace 交给模型。

2. 多租户插件、自动化与低代码扩展

若租户能上传脚本或配置化业务逻辑,可以把一个 tenant/application 作为 parent DO 的路由粒度,把插件的状态放在对应 facet。supervisor 在进入 child 前执行租户鉴权、配额、审计、版本选择,并只传入最小 capability。这比让插件直接拿到 D1、R2、KV 或原始 DO binding 更容易实现最小权限。

3. 有私有状态的子 Agent

Cloudflare Agents SDK 用 facets 为 Sub-agents 提供独立 SQLite,让 parent agent 保留编排、路由与访问控制。这适用于一个顶层会话下有多个子任务/角色、且它们不应直接共享短期记忆或业务状态的设计。注意 child 没有独立 physical Alarm slot;需要定时唤醒时由根 parent 统一承接和路由。Sub-agents 的存储、路由与调度说明

4. 可持久化的代码预览与应用构建平台

预览系统通常需要快速加载某个不可变版本的用户代码,又要保留该预览的局部状态。Facet 让平台能把“代码版本 + 数据库 + 访问策略”绑定在同一个 supervisor 下。若预览只需要短暂运行、完全不需要可靠状态,单纯 Dynamic Worker 反而更简单。

何时不要选 Facets

  • 代码是静态、可信且由团队部署的。聊天室、协作房间、游戏状态、锁、库存扣减、限流等仍是普通 DO 的直接用例;额外引入 Loader、supervisor 和代码版本治理只会提高复杂度。
  • 子实体必须成为独立的路由、地域或吞吐单元。Facet 的数据库与父对象共处一个 overall DO,官方没有承诺每个 facet 具有独立 placement、吞吐或配额。需要横向分片时,应先按 tenant、document、room 等键创建多个顶层 DO,再在每个顶层对象内按需使用 facets。
  • 需要跨大量实体查询或分析。Facet SQLite 仍是局部对象存储;全局索引、报表或关系查询更适合 D1、R2 或 Analytics Engine。
  • 工作本质是可恢复的多步骤流程。使用 Workflows;Facet 解决的是子代码与局部状态隔离,不是通用流程编排。参见 Workflows 与 Dynamic Workflows。
  • 需要完整 Linux、原生依赖或 shell。选择 Containers 或 Sandbox SDK,而不是把 Dynamic Worker/Facet 当作通用容器。

上线前检查单

  • 顶层 supervisor 的分片键明确,例如 tenant、应用或 workspace;不能把所有用户塞进一个 parent DO。
  • 动态代码的 ID 同时包含代码内容、依赖、权限与 compatibility 配置的不可变版本。
  • 默认禁用全局出站网络,并通过受控 capability 提供真正需要的业务能力。
  • 父库只保存控制面数据,facet 库只保存 child 业务状态;不依赖跨库直接读取。
  • 已设计 abort() 后的 code/schema 兼容、回滚和 stale stub 错误处理;delete() 设为显式且可审计的破坏性操作。
  • 已在 hibernation、代码更新、多个 facet name、配额耗尽和异常退出下测试 factory 是否能稳定恢复正确版本。
  • 已同时观察 Durable Objects 与 Dynamic Workers 的用量、错误和延迟;不要按照“facet 数量”猜测容量或成本。

参考资料

本文共 4496 字,创建于 Aug 22, 2026
博客助手

正在打开博客助手…