FIELD NOTE
[翻译]celld:可自行托管的分布式 Durable Objects
celld 可以在你自己的机器上运行 Workers 和 Durable Objects,每个 cell 使用一个 SQLite 数据库,并通过 S3 兼容存储实现持久化。
原文:celld: self-hosted, distributed Durable Objects
Ryan Dahl 刚刚发布了 celld。
这是 Cloudflare Workers 和 Durable Objects 的一个开源实现,可以运行在你自己的机器上。
你编写一个 Worker。你绑定一个 Durable Object。你使用相同的 JavaScript API 和一个熟悉的 wrangler.jsonc 文件。
但你不需要把应用部署到 Cloudflare。
你在自己的 VM 上运行 celld。每个 Durable Object 都有自己的 SQLite 数据库。这些数据库会复制到一个由你控制的 S3 兼容存储桶中。
这个存储桶同时负责协调各节点。
不需要单独运维数据库集群、放置服务、成员系统或共识服务。
这是一个非常有趣的架构。
我在自己的项目里使用 Cloudflare Workers、D1、Durable Objects、Queues、R2 以及许多其他 Cloudflare 服务。我喜欢这个平台。
但 celld 把它最好的一个想法变成了我们可以在任何地方运行的基础设施。
该项目于 2026 年 8 月 5 日发布公告。0.1.0 版本以 Apache 2.0 许可证发布在 denoland/celld 仓库中。
它也是一个 alpha 版本。
我今天不会把重要的生产系统迁移到它上面。但我绝对会用它做一个实验。
下面说说为什么。
首先,什么是 Durable Object?
普通的无服务器函数是无状态的。
它接收一个请求,做一些工作,然后返回一个响应。下一个请求可能运行在另一台机器上。
在请求需要协调之前,这一切都没问题。
想象一个有 500 人在线的聊天室。消息必须有一个明确的顺序。连接需要共享状态。两个用户可能同时更新同一个聊天室。
你可以用数据库、锁、消息代理和 WebSocket 服务来解决这个问题。
我的免费实时 Web 应用课程涵盖了这些长连接背后的基础知识。
或者,你可以把对该聊天室的每个请求都发送到一个 Durable Object。
Durable Object 是一个小型的、有名字的服务器,带有私有的持久化存储。
你可以为每个对象创建一个实例:
- 聊天室
- 用户
- 文档
- 游戏
- 项目
- 设备
- AI 代理
针对一个对象的所有请求都会到达同一个逻辑位置。
该对象一次处理一个事件。它可以在内存中保存热状态,在 SQLite 中存储数据,持有 WebSocket 连接,并调度 alarm。
重要的不是类或 API。
重要的想法是:
给每一块独立的状态一个所有者。
这消除了应用中大量的协调工作。
Cloudflare 为其 Durable Objects 产品提供机器、路由、存储、故障转移、全局放置和运维。
celld 保留了编程模型。它用 V8、SQLite、LTX、一个 S3 兼容存储桶和一小组你自己的机器取代了托管平台。
什么是 cell?
在 celld 中,cell 就是一个 Durable Object。
每个 cell 拥有:
- 一个稳定的名称
- 一个当前所有者节点
- 一个 JavaScript isolate
- 一个私有的 SQLite 数据库
- 一个所有权纪元(epoch)
- 一份复制到对象存储中的持久状态副本
名称就是地址。
如果我为一个名为 flaviocopes.com 的 Events Logger 项目创建一个 cell,那么该项目的每个事件都会到达同一个 cell。
另一个项目 waitinglists.dev 会得到另一个 cell 和另一个数据库。
两个项目不共享表。它们不会竞争同一个数据库锁。破坏一个数据库的 bug 不会破坏另一个。
应用从一开始就是分片的。
这与常见的多租户设计不同——后者让每个客户共享一个大型数据库,每个查询都带上 tenant_id。
使用 celld,边界是物理的。
一个 cell,一个 SQLite 文件。
完整的架构
对 celld 最短的描述是:
V8 + SQLite + LTX + S3 + Tokio
下面展开说明。
V8 运行 Worker 代码
每个 celld 节点都嵌入了 V8,即 Chrome、Node.js、Deno 和 Cloudflare Workers 所使用的 JavaScript 引擎。
celld deploy 会读取受支持的 Wrangler 项目,并使用 esbuild 创建 Worker bundle。
同一个节点既运行无状态的 Worker 请求,也运行有状态的 cell。
该运行时实现了 Cloudflare Workers API 中相当大的一部分,包括 fetch、service bindings、JavaScript RPC、Durable Objects、alarms、静态资源、streams、WebSocket,以及部分 Node.js 兼容层。
这并不意味着每个 Cloudflare 应用都能不加修改地运行。我稍后会回到这个边界。
SQLite 存储每个 cell
每个 cell 都有一个独立的 SQLite 数据库。
如果你对 SQLite 不熟悉,我的免费 SQLite 课程介绍了这个数据库的工作原理以及如何使用它的工具。
这为对象提供了事务、索引、SQL 查询和熟悉的文件格式。
更重要的是,它让状态单元保持很小。
一个聊天室或一个项目的数据库,比整个应用的数据库更容易移动。
一个 cell 同时只有一个写入者。celld 使用所有权纪元来隔离旧的写入者。
如果一个节点失去了所有权,它就无法继续写入当前状态。它的旧写入会进入一个旧的纪元路径,该路径不再具有权威性。
LTX 复制 SQLite 变更
LTX 是 Litestream 使用的事务文件格式。
celld 包含一个 Rust 实现。它把 SQLite 的变更转换为可以通过对象存储保存和恢复的段。
本地 SQLite 文件让热读和执行保持快速。存储桶中的 LTX 副本让状态变得持久且可移动。
celld 在数据到达存储桶之前不会确认持久化写入。
这就是项目所说的 RPO=0 的含义:杀死一个节点不应该丢失调用方已经收到成功响应的写入。
我在免费的 Backup and Restore 课程中解释了 RPO、RTO 和恢复规划。
未确认的请求仍然可能失败。RPO=0 并不意味着零停机。它意味着持久性边界是明确的。
S3 是唯一事实来源
一个集群中的所有节点都指向同一个 S3 兼容存储桶。
该存储桶存储:
- 部署
- SQLite 和 LTX 状态
- cell 所有权记录
- 节点租约
- 对等节点认证材料
存储桶既是存储,也是协调者。
对象存储的 compare-and-swap 让一个节点获得 cell 的所有权。当另一个节点接管时,所有权纪元会改变。
注意这里发生了什么。
celld 并没有让分布式协调消失。它把这一责任交给了对象存储的条件写入和一致性保证。
celld 内部没有 Raft 集群或独立的共识服务。对象存储提供商底层仍然运行着一个分布式系统。
当 S3 已经是你基础设施中最持久的部分时,这是一个不错的权衡。
Tokio 运行节点
celld 用 Rust 编写,使用 Tokio 处理异步工作。
节点处理 HTTP、对等请求、WebSocket、SQLite 工作、复制、租约、恢复和生命周期管理。
第一个版本以一个小型静态可执行文件和 Docker 镜像的形式发布。
请求期间会发生什么?
让我们跟随一个请求到达 cell。
flowchart LR
A["客户端请求"] --> B["任意 celld 节点"]
B --> C["无状态 Worker"]
C --> D["解析 cell 名称"]
D --> E{"谁拥有这个 cell?"}
E -->|"本节点"| F["在 V8 中运行 cell"]
E -->|"另一节点"| G["签名对等请求"]
G --> F
E -->|"无人拥有"| H["用 compare-and-swap 声明所有权"]
H --> I["从 LTX 恢复 SQLite"]
I --> F
F --> J["写入本地 SQLite"]
J --> K["将 LTX 复制到存储桶"]
K --> L["返回已确认的响应"]
流量可以到达任何节点。
Worker 按名称选择 cell。celld 检查当前所有者。
如果 cell 已经存在于本节点上,请求就留在本地。
如果另一个节点拥有它,celld 会通过其经过认证的对等协议转发请求。
如果没有节点拥有它,某个节点会用条件存储桶写入来声明所有权,恢复其 SQLite 状态,然后启动 isolate。
代码在 V8 中运行。
如果请求改变了持久状态,celld 会在本地写入 SQLite,并在释放响应之前把已提交的变更复制到存储桶。
结果是一个有用的分工:
- 热工作在本地进行
- 持久化写入付出一次对象存储往返的代价
- 非活动状态存放在廉价的对象存储中
- 机器可以替换
为什么 celld 不需要控制平面
大多数分布式系统从一列机器开始。
机器需要知道谁加入了、谁离开了、谁健康、分片在哪里,以及失败后应该由谁移动它。
这通常会造出一个控制平面。
celld 改用存储桶。
一个节点启动时持有存储桶凭据和一个对等节点可以访问的地址。它写入一个租约。其他节点通过存储桶发现它。
没有 join 命令,也没有固定的成员文件。
cell 所有权也是存储桶中的一条记录。
如果一个节点消失,它的租约过期。另一个节点可以声明该 cell,恢复数据库,然后继续运行。
这让节点变得可替换。
但这个设计有一个后果:存储桶不仅仅是备份。
它是整个集群的权威根源。
任何拥有这些凭据的人都可以控制部署、所有权、状态和对等认证。安全文档建议将凭据限定在单个集群存储桶的范围内。
休眠改变了成本模型
一个 cell 不需要永远留在内存中。
当 cell 空闲时,celld 可以让它休眠。它的持久状态留在存储桶中,而它的 JavaScript isolate 和本地工作状态从内存中消失。
下一个请求会再次唤醒它。
这对许多实体大多处于空闲状态的应用很适用。
想象每个客户项目一个 cell。
也许有 100,000 个项目,但只有 300 个当前有流量。活跃的 cell 使用 RAM。其余 99,700 个以对象的形式存在于存储桶中。
这与 Durable Objects 吸引人的经济逻辑相同。
你为活跃的工作集付费,而不是为全部对象数量付费。
celld 在主页上发布了一些早期的测量数据:
- 每个常驻的简单 cell 约占 4 MB RAM
- 一个 8 GB 节点上约有 1,000 个常驻 cell
- 在本地基准机器上唤醒一个休眠 cell 约需 4 ms
- 区域内的持久化写入约需 90 ms
- 杀死节点后的故障转移约需 20 秒
这些数字来自不同的测试条件。该网站称其速度和密度测量使用的是 Apple M 系列笔记本电脑,而持久性测量使用的是区域 VM 集群和一个附近的存储桶。
测试文档提供了更多有用的背景。
一个测试运行了十个节点,每个节点 4 个 vCPU 和 8 GB 内存。该集群承载了 10,000 个常驻 cell 和 20,000 个 WebSocket 连接。在两个节点停止后,所有 cell 数据在大约 11 秒内(尾部延迟)恢复可用,其余节点仍有剩余容量。
这很有前景。
但这不保证我的应用会产生同样的数字。
成本主张需要背景
celld 主页将其自托管成本与长期存活的 Cloudflare Durable Objects 进行了比较。
它的模型使用一台每月 48 美元的 8 GB VM 承载 1,000 个常驻 cell。它估计 1,000 个常驻 cell 每月约需 49 美元,而 Workers Paid 计划中持续活跃的 128 MB Durable Objects 约为 4,150 美元。
这就是“便宜几个数量级”说法的来源。
但这个比较描述的是一个特定的工作负载:许多对象整月常驻。
这并不意味着 celld 总是更便宜。
对于小型应用,Cloudflare 可能成本更低。免费计划可以覆盖很小的负载,而托管平台省去了大量工作。
自托管的账单也不仅仅是 VM:
- 负载均衡器和公共入口
- 对象存储请求和数据传输
- 节点故障的备用容量
- 监控和日志
- 备份和恢复测试
- 安全更新
- 运维人员的时间
celld 的成本图表说明,应用流量和写入会增加更多的计算和存储桶用量。它还要求按整节点增加容量,所以第一个 cell 仍然需要一台机器。
我不会因为一张图表就选择 celld。
我会在架构合适、并且我想要控制故障域、存储、放置和成本曲线时选择它。
一个微型 celld 应用
如果你用过 Durable Objects,这个编程模型是熟悉的。
这个 Worker 把每个请求发送到一个名为 room-42 的计数器:
export class Counter {
constructor(state) {
this.state = state
}
async fetch() {
let count = (await this.state.storage.get('count')) ?? 0
count++
await this.state.storage.put('count', count)
return new Response(JSON.stringify({ count }))
}
}
export default {
async fetch(request, env) {
const id = env.COUNTER.idFromName('room-42')
return env.COUNTER.get(id).fetch(request)
},
}
绑定和迁移写在 wrangler.jsonc 中:
{
"name": "counter",
"main": "index.js",
"compatibility_date": "2026-08-05",
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
}
]
}
这是正常的 Durable Objects 代码。
你把它部署到一个存储桶,而不是 Cloudflare 账户:
celld deploy . \
--bucket s3://flavio-celld-lab
然后针对同一个存储桶启动一个节点:
celld \
--bucket s3://flavio-celld-lab \
--listen 0.0.0.0:8080 \
--advertise cell-a.internal:8080
标准的 AWS 凭据链提供对 S3 的访问。你也可以为 R2 或其他兼容的对象存储传入 endpoint 和 region。
Worker 项目需要 PATH 中有 esbuild。纯静态资源部署不需要。
什么可以不加修改地运行?
发布公告说 Workers 和 Durable Objects 代码可以不加修改地运行。
关键词是受支持的 API。
根据当前的 Cloudflare 兼容性页面,celld 支持:
- module Workers 和
fetch - 带 SQLite 存储的 Durable Objects
- alarms
- 可休眠的入站 WebSocket(hibernatable inbound WebSockets)
- 出站 WebSocket 客户端
- service bindings
- 大部分 JavaScript RPC 行为
- 静态资源、
_headers和_redirects - 常见的 Web Platform API
- 部分 Web Crypto
- 部分 Node.js API
该项目计划增加 D1、Workflows,也许还有 Queues。
它不打算复刻整个 Cloudflare 平台。
KV、R2 绑定、Cache API、Workers AI、Vectorize、Hyperdrive、Browser Rendering、Email、自定义域名和 TLS 终止都不在当前范围内。
Cron handlers 不受支持。cell 可以使用 durable alarms 代替。
celld 接受 wrangler.json 和 wrangler.jsonc,但不接受 wrangler.toml。不支持的配置键会中止部署。
这种响亮失败是好的。
一个悄悄忽略绑定的兼容层才是危险的。
在迁移任何 Worker 之前,我仍然会运行一套真实的测试。一个项目可能依赖某个 Node.js 模块或某个小的运行时行为,而这种依赖在配置中并不明显。
这不是你自己服务器上的 Cloudflare
celld 实现了一个聚焦的运行时和状态模型。
它不会给你 Cloudflare 的网络。
没有托管的全局入口、anycast 路由、DDoS 防护、TLS、账户系统、多租户调度器或全球放置层。
一个 celld 集群运行一个应用部署。
你在前面放自己的反向代理或负载均衡器。你在那里终止 TLS。你决定存在哪些区域以及流量如何到达它们。
节点之间通过带签名的对等协议通信。请求使用 HMAC 认证、body 签名、时钟限制和重放保护。
但对等流量是明文 HTTP。
限制页面说要把对等地址放在私有网络上,或放在 WireGuard、Tailscale 之类的加密覆盖网络中。我的免费 Tailscale 课程解释了这种私有网络的工作原理。
不要把对等端口暴露到公共互联网。
该项目目前也不适合恶意的多租户工作负载。
这是一个 alpha 边界,不是一条小注脚。
我觉得最有趣的架构
最有趣的部分不是自托管 Cloudflare API。
而是把对象存储作为有状态系统的基础。
我们通常把数据库放在中心:
application nodes -> shared database
celld 把它变成了:
requests -> named cells -> private SQLite databases -> object storage
移动的单位不是一行或一张表。
而是一个附加在某个逻辑参与者身上的小型数据库。
这种架构给了我们三个有用的特性。
争用保持在本地
一个繁忙的项目不会锁住另一个项目的数据库。
只有指向同一个 cell 的请求才需要串行化。
失败有更小的爆炸半径
一个受损的 cell 影响一个对象数据库,而不是一个被所有客户共享的数据库。
机器和存储桶仍然是共享的故障域。cell 隔离并不能消除这些。
状态随计算一起移动
拥有某个 cell 的节点同时也运行它的代码并打开它的 SQLite 数据库。
热请求不会跨越数据库网络边界。
当所有权移动时,数据库也跟着移动。
对于聊天、协作、游戏、设备、工作流和代理来说,这是一个干净利落的模型。
Cell 是 AI 代理的好归宿
我列的 cell 示例里包括一个 AI 代理。这一点值得多说几句。
我每天都在运行编码代理,所以我近距离看过这种工作负载长什么样。
一个代理是一个长生命周期的有状态的东西。它有对话历史、工作记忆、工具结果和一连串进度事件。它可能会派生出同样需要这一切的子代理。
把它映射到一个 cell 上,契合点显而易见。
每个代理都有一个名字,所以关于它的每个请求都会到达同一个地方。它的历史和记忆存放在自己的 SQLite 数据库中。它一次处理一个事件,所以两个工具结果不会破坏它的状态。它可以设置 alarm 稍后恢复工作。在等待慢速模型响应或等待我的时候,它可以休眠。
每个代理一个数据库这一点,比看起来更重要。
代理会不停地写入。每一步、每一次工具调用都会产生值得存储的事件。把一千个代理放在一个共享数据库上,这个日志流就成了扩展问题。给每个代理一个自己的数据库,写入永远不会相遇。
隔离是另一半。运行多个代理通常意味着给每个代理一个容器或 VM,这样它们就读不到彼此的状态。一个 cell 在几 MB 内存的尺度上画出类似的边界,而且它能在几毫秒内唤醒,而不是容器需要的几秒钟。
观看代理的仪表盘可以持有一个指向其 cell 的可休眠 WebSocket。进度实时流入,而且在代理于步骤之间休眠时连接依然存活。
在 celld 之前,用这种方式构建一个代理平台意味着把整个产品押注在单一提供商上。现在同样的架构可以运行在你选择的机器上。
Cell 模型也适合我的一些项目。
我可以用 cell 重建 Events Logger
Events Logger 是我第一个想尝试的项目。
今天它是一个自托管的 Astro、HTMX 和 Alpine.js 应用。每个应用把事件发送到一个 HTTP API。事件进入一个本地 SQLite 数据库。HTMX 在仪表盘中轮询新事件。
当前的架构很小,运行得很好。
一个 celld 版本会改变存储边界。
我会为每个 Events Logger 项目创建一个 cell:
events:flaviocopes.com
events:waitinglists.dev
events:prototyped.dev
每个 cell 会在 SQLite 中保存自己的事件、分类、收藏和洞察卡片。
公共 API 保持同样的形态。顶层 Worker 会认证 API 密钥、读取项目 ID,并用 idFromName(projectId) 路由请求。
cell 会在一个事务中插入事件并更新本地聚合。
我还可以用可休眠 WebSocket 取代 HTMX 轮询。仪表盘会连接到项目 cell,并立即收到新事件。
当没有人在看或写那个项目时,cell 可以休眠。
这把 Events Logger 从一个进程一个数据库,变成了每个项目一个数据库的分布式服务。
困难的部分是全局仪表盘。
Cell 是有意隔离的。我无法跨所有项目数据库运行一条 SQL 查询。
我需要一个显式的索引 cell 来存储每个项目的小摘要,或者查询多个 cell 然后合并结果。
这不是缺陷。这是隔离模型的代价。
它也逼出一个有用的问题:哪些数据真正需要是全局的?
Events Logger 是一个好的实验,因为我可以保留现有的 API 契约,在旁边构建 celld 实现。我可以把相同的事件推送给两个版本,杀死节点,比较结果,在不迁移生产数据的情况下学习。
Factory Log 可以获得可选私有同步
Factory Log 是本地优先的。
编码代理把事件追加到我 Mac 上的一个 JSONL 文件中。一个原生 SwiftUI 应用监视该文件,把事件变成实时仪表盘和每日编年史。
写入者使用进程间锁。什么都不上传。
我喜欢这个隐私边界,所以我不会用强制性的云服务取代本地文件。
但 celld 可以为一种可选的私有同步模式提供支持。
我会每个项目使用一个 cell,而不是每个任务一个 cell。
一个项目的所有事件都会保持有序地留在一个 SQLite 数据库中。CLI 可以追加它的本地 JSONL 事件,然后把同一事件发送到项目 cell。
macOS 应用可以打开一个 WebSocket,在另一台机器或远程代理报告进度时更新。
这会解决一个实际的限制:Factory Log 目前只显示一台 Mac 上的工作。一个 celld 集群可以把来自笔记本电脑、台式机、远程服务器和托管编码代理的报告汇聚起来,而不需要共享一个文件系统。
本地 JSONL 文件应该继续作为每台机器上的直接事实来源。上传需要一个 outbox 和稳定的事件 ID,这样重试同步就不会产生重复。
cell 会成为一个收敛的共享视图,而不是网络断开时让本地 CLI 停摆的理由。
这比更换存储调用要做更多的工作。
celld 给我的是串行化的服务器状态。它不会自动给我离线优先的同步。
尽管如此,一个项目一个 cell 是一个自然的匹配。
Waiting Lists 可以隔离每个列表
Waiting Lists 目前作为一个 Cloudflare Worker 运行,带一个 D1 数据库。
Cloudflare Email Service 发送确认邮件。一个 Queue 接收投递事件。Rate-limit 绑定保护端点。一个 Cron Trigger 删除过期的待处理订阅。Turnstile 保护管理员登录。
一个 celld 版本可以为每个等待列表使用一个 cell。
cell 会存储:
- 订阅者
- 确认状态
- 同意版本
- token 哈希
- 投递事件
- 列表设置
一个列表的所有注册都会由该 cell 串行化。
重复提交和确认请求可以在同一个事务中运行。cell 可以设置 alarm 删除过期的待处理记录。
像 Resend 这样的邮件提供商可以通过 fetch 调用。投递 webhook 会路由回同一个列表 cell。
这会给每个列表一个独立的 SQLite 数据库和一个小的故障边界。
但现有的 Worker 无法不加修改地运行。
celld 不提供 Cloudflare Email Service、Queues、Turnstile、托管限速或 Cron Triggers。我需要替换这些部分,或者把它们留在 celld 集群之外。
我还需要一个知道存在哪些列表的 owner cell。否则管理仪表盘无法在不知道每个数据库的情况下显示跨列表摘要。
这个设计是可行且有趣的。
当前的 Cloudflare 版本仍然是更安全的生产选择。
Sitebase 几乎是为这个模型设计的
Sitebase 是最强的概念匹配。
Sitebase 已经给每个 workspace 一个自己的 D1 数据库。我选择这个设计是为了在数据库层面隔离客户数据,而不是给每张表加 workspace_id。
配置这些数据库需要真正的机制。
控制数据库跟踪账户和路由。新的租户数据库通过 Cloudflare API 创建。它们作为 Worker 绑定被附加。部署必须保留动态绑定列表。本地开发使用一组预先创建的数据库池,因为它无法以同样的方式配置它们。
celld 让每个租户一个数据库成为默认。
我可以把每个 workspace 或网站映射到一个 cell。创建租户意味着选择一个名称。第一个请求会创建它的 SQLite 数据库。
没有 D1 配置 API。没有绑定生成。没有开发池。
租户的 widget、提交、订阅者、分析和设置会一起存放在一个私有数据库中。
Cell alarms 可以处理每个租户的清理和监控计划。休眠很适合这种负载,因为大多数小网站在大部分时间里都是安静的。
但 Sitebase 也比任何玩具演示都更好地展示了 celld 当前的限制。
Sitebase 使用 KV、R2、Queues、Analytics Engine、Cron Triggers、Turnstile、电子邮件和托管的 Cloudflare 路由。celld 不复刻这些服务。
我可以替换其中一些:
- 用 cell alarms 代替 cron 处理租户特定的工作
- 直接访问对象存储处理上传
- 通过
fetch调用外部邮件 API - 用 cell 支撑的队列处理聚焦的工作流
- 用反向代理处理域名和 TLS
但我将围绕这个运行时构建一个平台。
这可以移除 Sitebase 最别扭的部分,即租户数据库配置。它也会让我对入口、队列、存储适配器、监控、安全和故障转移负责。
架构非常契合。
运维上的权衡要大得多。
我不会用 celld 重建一切
新基础设施令人兴奋。但这不意味着它处处有用。
我不会把 flaviocopes.com 迁移到 celld。这个网站大多是静态的,Cloudflare Pages 已经很好地服务了它。
我不会用 cell 重建 HostingPicker。它的大部分价值是精选数据和客户端比较。
我不会迁移 inferencecost.dev 的核心计算器。它的状态已经存在于 URL 中,这比任何数据库都更便宜、更简单。
StackPlan 可以为每个保存的应用或报告使用一个 cell,但它也依赖全局提供商数据、AI Gateway、认证和计费。Cell 模型可能帮助一个部分,却不能改善整个产品。
我的规则是:
当产品包含许多需要协调的独立有状态事物时,使用 celld。
最强烈的信号是:
- 每个状态单元有一个自然的名称
- 单元很多,但只有一小部分活跃
- 对同一单元的并发写入
- 长生命周期的 WebSocket
- 每个租户的数据隔离
- alarms 或持久化工作流
- 需要在可替换的机器之间移动状态
如果应用是静态的、读密集的,或者围绕全局查询构建的,那么普通数据库可能更简单。
我会如何测试 celld
我会从 Events Logger 开始。
第一个版本会运行一个 celld 节点和一个 R2 存储桶。它只实现项目创建、事件接收、最近事件和一个实时 WebSocket 源。
然后我会在私有 Tailscale 网络上添加第二个节点。
我会在前面放一个小型反向代理,把请求发送到两个节点。
测试会是实际的:
- 创建 1,000 个项目 cell
- 向每个项目推送编号事件
- 为较小的活跃集合保持 WebSocket 源开启
- 在写入期间杀死一个节点
- 在恢复后验证每一个已确认的事件
- 重启节点并观察它重新加入
- 与当前应用比较延迟和存储桶操作
我还会测试丑恶的情况。
R2 返回 429 时会发生什么?私有网络丢包时会发生什么?集群没有剩余内存时会发生什么?逐个检查一个失败的 cell 并手工恢复它有多容易?
celld 自己的测试策略很好。它对 workerd 做差分测试,对协调协议做确定性模拟,还运行注入故障的实时集群。
我仍然会测试我自己的工作负载。
应用本身决定了这个架构是否有用。
我喜欢这个项目的什么
我喜欢 celld 作为一个系统足够小,可以理解。
这个架构有几个强有力的部分:
- 用于计算的 JavaScript isolate
- 每个有状态对象一个 SQLite 数据库
- 用于持久复制的 LTX
- 用于所有权的条件对象存储写入
- 用于执行的可替换 Rust 节点
边界是可见的。
热读取在本地。持久化写入等待对象存储。故障转移等待租约和恢复。全局查询需要显式设计。公共入口是运维者的工作。
它没有声称自托管能消除失败。
它改变的是谁拥有失败、谁能检查它。
我也喜欢可移植性的方向。
Durable Objects 是一个强大的模型,但到目前为止它绑定在一个托管平台上。celld 给这个模型提供了另一个实现。
即使我继续部署到 Cloudflare,第二个运行时也会让这个 API 变得更强大。兼容性变得可测试。应用有了退出路径。这个想法可以在单一提供商之外演化。
今天什么会阻止我使用它
alpha 标签是主要原因。
受支持的 API 表面仍在演化。安全文档说恶意的多租户使用是不安全的。对等流量需要私有加密网络。没有托管入口或全局放置。容量和负载释放行为仍在调整中。
这个项目进展也很快。
当前的 README、限制页面和 0.1.0 发布说明并没有以完全相同的方式描述每一个压力释放默认值。这在发布两天后是正常的,但它意味着我在运维一个集群之前会阅读代码和当前的发布说明。
对我的项目来说,最大的缺失不是运行时 API。
而是周边的平台。
Cloudflare 在 Workers 旁边给了我 DNS、TLS、DDoS 防护、日志、队列、邮件、对象存储、AI 服务、部署和 secrets。
celld 给我的是一个聚焦的计算和状态原语。
正是这种聚焦让它有趣。这也是为什么采用它意味着要自己拼装更多零件。
一个非常好的架构实验
celld 不是“免费的 Cloudflare”。
它是一种封装优秀有状态编程模型的不同方式。
一个有名字的对象拥有一个 SQLite 数据库。活跃的对象住在使用它们的代码旁边。空闲的对象坍缩进对象存储。一条条件存储桶记录决定所有权。一台新机器指向同一个存储桶即可加入。
这是一个紧凑而强大的想法。
我能看到 Events Logger 变成每个项目一个 cell。Factory Log 变成每个同步项目一个 cell。Waiting Lists 变成每个列表一个 cell。Sitebase 变成每个 workspace 一个 cell,而不再有数据库配置机制。
我也能清楚地看到为什么我不应该现在重写那些生产系统。
这正是那种正确的新基础设施项目。
它在要求我用它托管一切之前,先改变了我对架构的思考方式。
阅读 celld 文档,查看源代码,并密切关注限制和安全边界。
我会持续关注这个项目。
DISCUSSION
评论
正在加载评论…