一、背景:为什么你需要这个
2017年,Cloudflare Workers 以「无服务器边缘计算」的概念横空出世,让全球数百万开发者第一次感受到:原来不用管服务器,不用管扩容,代码往边缘一扔,就能服务全世界。那一年,Durable Objects(DO)作为 Workers 的有状态扩展出现了——它让你在 Serverless 的汪洋大海里有了一块可以持久存储、可以维护连接、可以持有锁的「岛屿」。
但问题来了:Durable Objects 是 Cloudflare 的基础设施,你的数据在他们的服务器上,你的计算在他们控制的 V8 隔离环境里,你的可用性依赖他们的全球网络。
对于金融、医疗、政务、或者任何对数据主权有严格要求的业务来说,这不是「不方便」,而是「不可接受」。
Deno 团队给出的答案是:celld。
celld 是一个开源守护进程,它让 Cloudflare Workers + Durable Objects 的编程模型跑在你自己的机器上。你可以把 Workers 代码直接迁移过去,数据存在你自己控制的存储里,网络走你自己维护的集群,审计完全自主。但你不需要改变任何业务代码——同一个 Worker,同一个 Durable Object,区别只是运行时在哪里。
这不是实验项目。celld 由 Ryan Dahl 亲自署名发布,v0.2.0 于2026年8月12日发布,改写了内存管理的核心假设。本文从架构原理、v0.2.0 技术革命、生产部署到性能调优,全链路拆解这个项目。
二、Durable Objects 的本质:一个对象的 SQLite 数据库
理解 celld 的设计哲学,要从理解 Durable Objects 的本质开始。
Durable Objects 的核心抽象是:一个对象(Object)= 一个微型数据库 + 一个事件处理器。
当你创建一个 Durable Object,Cloudflare 为它分配一个 V8 隔离实例(Isolate),并为它创建一块持久存储。这个对象通过一个唯一名称(Object ID)被寻址,后续所有发往这个对象的请求,都会路由到同一个 Isolate 实例——这就是「有状态」的来源。
请求 → Durable Object ID → 路由到同一个 Isolate → 执行业务逻辑 → 持久化状态
关键点在于:Isolate 是有状态的,但 Isolate 本身不是存储介质。状态存在哪里?Cloudflare 使用内部的 KV 存储;对于 celld,答案是 SQLite。
celld 的核心设计哲学是:
每个 Durable Object = 一个 SQLite 数据库文件。
这意味着:
- 写入 SQLite = Durable Object 的状态持久化
- 读取 SQLite = Durable Object 的状态恢复
- SQLite 的 ACID 特性天然保证了状态一致性
- 不同对象之间天然隔离,因为它们是独立的数据库文件
这种「每个对象一个 DB」的设计,让分片(Sharding)成为架构内置的能力,而不是需要额外维护的系统。
三、零共识架构:celld 如何做到「没有控制平面」
传统的分布式系统,无论是 etcd、Consul 还是 Redis Cluster,都需要某种形式的共识协议(通常是 Raft 或 Paxos)来协调多个节点之间的状态。共识协议保证了数据一致性,但代价是复杂性——节点发现、心跳选举、分区处理、日志复制,每一步都可能出问题。
celld 的核心创新是:它根本不需要共识协议。
3.1 协调机制:只靠一个 S3 Bucket
celld 的多节点协调只依赖一个组件——你自己拥有的 S3 兼容存储桶(支持 AWS S3 或 Google Cloud Storage)。
celld 节点集群
├── Node A ──┐
├── Node B ──┼── 共享同一个 S3 Bucket ── 存储 Durable Object 的 SQLite 文件
└── Node C ──┘
工作流程:
- Durable Object 初始化时,它的 SQLite 数据库文件被写入 S3 Bucket
- 节点通过 S3 读取和写入这个文件
- 节点之间不直接通信,只通过 S3 间接协调
- 没有主节点,没有选举,没有共识
这看起来不可思议——怎么可能不用共识就保证一致性?答案在于 celld 对问题域的精确限定。
3.2 为什么不需要共识
Durable Object 的编程模型保证了:任意时刻,一个 Object 实例只被一个请求处理。
Cloudflare Workers 的请求路由层确保:发往同一个 Durable Object ID 的所有并发请求,都被串行化处理。这意味着不存在「两个节点同时写入同一个 SQLite 文件」的竞态条件。
celld 继承了这个语义:在 celld 的集群中,一个 Durable Object 最多同时被一个节点持有。对象不在节点之间迁移时,不需要锁协议。对象从一个节点「失活」到在另一个节点「激活」时,通过 S3 文件的原子性写入完成状态转移。
对象持有权转移过程:
Node A 上的对象 O(状态快照写入 S3)→ Node A 失活 O
↓
S3 Bucket(SQLite 文件更新)
↓
Node B 激活 O(从 S3 读取最新状态)→ Node B 上的对象 O
这种「以存储换协调」的设计,代价是:S3 Bucket 的延迟成为全局瓶颈。但 celld 的作者们显然做了取舍——对于不需要毫秒级跨节点状态同步的场景,这个 tradeoff 完全合理。
3.3 v0.2.0 的数据面/控制面分离
v0.2.0 引入了两个监听器分离的设计:
# 数据面:处理 Worker 请求和健康检查
celld --listen 0.0.0.0:8080
# 控制面:处理节点间对等通信和 Operator API
celld --internal-listen 10.0.1.0:9090
# 控制面节点发现
celld --advertise 10.0.1.0:9090
- 数据面(Public Listener):只服务 Worker 路由和
/__celld/health,对外暴露 - 控制面(Internal Listener):服务 Operator API(
/state、/shutdown)和节点间对等流量,放在私有网络
这个设计让 celld 可以安全地放在公网节点上,而节点间的控制通信完全在私有网络内部。这对多云部署和混合云场景特别重要。
四、v0.2.0 内存革命:471KB vs 3.4MB 的背后
celld v0.2.0 最引人注目的变化,是内存消耗的戏剧性下降:
单个驻留对象从 ~3.4MB 骤降到 ~471KB,降低了约 87%
这意味着同样一台 16GB 内存的服务器:
| 版本 | 单对象内存 | 可同时驻留对象数 |
|---|---|---|
| v0.1.0 | ~3.4MB | ~4,700 个 |
| v0.2.0 | ~471KB | ~34,000 个 |
4.1 v0.1.0 的问题:每个对象一个 Isolate
v0.1.0 采用了最直觉的 Isolate 管理方式:每个 Durable Object 实例持有自己专属的 V8 Isolate 和 OS 线程。
v0.1.0 模型:
┌──────────────────────────────────────────────────────┐
│ Node │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Thread │ │ Thread │ │ Thread │ ... │
│ │ ┌─────┐ │ │ ┌─────┐ │ │ ┌─────┐ │ │
│ │ │Isolate│ │ │ │Isolate│ │ │ │Isolate│ │ │
│ │ │3.4MB │ │ │ │3.4MB │ │ │ │3.4MB │ │ │
│ │ │ Cell │ │ │ │ Cell │ │ │ │ Cell │ │ │
│ │ └─────┘ │ │ └─────┘ │ │ └─────┘ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────┘
这种设计的问题:
- 内存浪费严重:即使对象不活跃,Isolate 也要占用 3.4MB
- OS 线程资源紧张:每个对象一个线程,上万对象意味着上万个线程
- 无法有效复用:对象在节点间迁移时,Isolate 需要完整重建
4.2 v0.2.0 的解决方案:Isolate 池 + 轮转执行
v0.2.0 的核心改变是把「一个对象 = 一个 Isolate」改成了「一个对象 = 一个 Isolate 槽位」。
v0.2.0 模型:
┌────────────────────────────────────────────────────────┐
│ Node │
│ ┌──────────────────┐ │
│ │ Isolate Pool (jemalloc) │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ISO A│ │ISO B│ │ISO C│ │ISO D│ │ISO E│ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ └──────────────────┘ │
│ │
│ Cells 共享 Isolate,轮转执行(Turn by Turn): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Cell A │──▶│ Isolate A│──▶│ Cell B │──▶... │
│ │ (471KB) │ │ │ │ (471KB) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────────────┘
关键机制:
1. Isolate 共享池
多个 Cell(共存的 Durable Object 实例)共享同一批 Isolate。v0.1.0 中每个 Cell 独占的 3.4MB,现在变成多个 Cell 轮流使用同一个 ~10-20MB 的 Isolate 堆。
2. Turn by Turn 执行
当一个 Cell 发起 I/O 操作(如读写 S3)时,它的 Isolate 被释放回池中,下一个等待中的 Cell 立即接管这个 Isolate。这类似于协程的让出机制,但作用在 V8 Isolate 层面。
// 伪代码:Turn by Turn 的执行模型
class IsolatePool {
private pool: Isolate[];
async function executeCell(cell: Cell): Promise<Response> {
const isolate = await this.acquire(); // 从池中获取一个 Isolate
try {
const result = await cell.handle(isolate); // 执行 Cell 的 handler
return result;
} finally {
if (cell.isWaitingForIO()) {
// Cell 在等待 I/O(如 S3 读写),释放 Isolate
this.release(isolate);
// 下一个 Cell 立即使用这个 Isolate
} else {
// Cell 仍需执行,保持 Isolate
}
}
}
}
3. jemalloc 全局分配器
v0.2.0 引入 jemalloc 作为全局内存分配器。jemalloc 的优势在于:
- 多线程友好的内存分配(减少锁竞争)
- 优秀的内存碎片管理
- 统计和调试能力(追踪内存泄漏)
// celld/crates/alloc/src/lib.rs(概念示例)
use jemalloc::Jemalloc;
#[global_allocator]
static ALLOC: Jemalloc = Jemalloc;
// Cell 的内存现在通过 jemalloc 精确管理
// Isolate 释放后,jemalloc 快速回收未使用内存
4. RSS Shedding(驻留内存弹性控制)
v0.2.0 引入「RSS Shedding」机制:当节点总内存接近上限时,自动将低活跃度的 Cell 从内存中驱逐(deactivate),只保留它们在 S3 中的 SQLite 文件。下次被请求时再重新激活。
# 限制节点最大驻留对象数(通过 RSS 上限控制)
celld --max-resident-cells 5000 --listen 0.0.0.0:8080
4.3 I/O 等待不阻塞的协程语义
这是 Turn by Turn 机制最精妙的地方。传统多线程模型中,如果一个线程在等待 I/O,它会阻塞在那里直到 I/O 完成——但这个线程持有的内存(和对应的栈空间)是无法释放的。
celld 的 Turn by Turn 相当于给 Isolate 级别的协程:当 Cell A 在等待 S3 读取时,Isolate 被完整释放,A 持有的所有堆内存理论上可以被垃圾回收(虽然 V8 目前没有完全实现这个优化,但架构上已经为这一步留好了空间)。
// Cell 的 handler 代码,不需要任何特殊语法
class MyCounter implements DurableObject {
async fetch(request: Request): Promise<Response> {
// 读取 SQLite(可能触发网络 I/O)
const db = await this.openDatabase();
const count = await db.get('counter');
// 在这个 await 期间,Isolate 被释放
// 其他 Cell 可以使用同一个 Isolate 继续执行
await this.storage.put('counter', count + 1);
return new Response(`Count: ${count + 1}`);
}
}
五、复制与压缩:v0.2.0 的存储层升级
5.1 多级复制对象(L0 → L1)
Durable Object 的每次状态变更都需要复制到 S3。v0.1.0 中,每次变更产生一个独立的 S3 对象。时间久了,S3 Bucket 里会堆积大量小文件——每个文件几 KB,但数量可以达到百万级别,造成严重的 S3 API 调用延迟和成本。
v0.2.0 引入了压缩(Compaction)机制:
v0.1.0 存储:
object_id/0001.db (10KB - 初始快照)
object_id/0002.db (10KB - 变更1)
object_id/0003.db (10KB - 变更2)
object_id/0004.db (10KB - 变更3)
... (N 个文件)
v0.2.0 存储:
object_id/0001.db (10KB - 初始快照)
object_id/L0/ (多个小文件,累积直到压缩触发)
object_id/L1.ltx (压缩后的单一 L1 块)
- L0 对象:每次状态变更产生的增量快照,多个小文件
- L1 块:celld 将多个 L0 对象合并为一个加性的 L1 块(使用 LTX v0.5.2 块格式)
压缩过程:
- 当 L0 文件数量达到阈值,触发压缩
- 将所有 L0 对象合并为一个 L1 块
- 源 L0 文件在压缩完成后才删除(
never deletes a source object),保证数据安全
# 查看 celld 存储统计
curl http://localhost:9090/__celld/stats
# 手动触发压缩
curl -X POST http://localhost:9090/__celld/compact/<object_id>
5.2 take-over 机制:存储故障时的容错
v0.2.0 还增加了对 S3 Bucket 故障的容错能力。当一个节点的 S3 连接变慢或失败时,celld 的 take-over 机制可以将对象的持有权转移到其他节点,确保高可用。
S3 Bucket 变慢/故障
↓
节点 A 无法及时复制 → 触发 take-over
↓
节点 B 感知到 A 失活 → 从 S3 加载最新状态 → 激活对象
↓
业务继续运行(可能丢失最后一次变更,但 RPO 受控)
六、生产级部署:从 Docker 到 Kubernetes
6.1 快速启动(Docker 单节点)
# 拉取镜像
docker pull ghcr.io/denoland/celld:latest
# 启动单节点(使用本地文件系统作为「Bucket」)
docker run -d \
--name celld \
-p 8080:8080 \
-p 9090:9090 \
-v /data/celld-bucket:/bucket \
ghcr.io/denoland/celld:latest \
celld \
--bucket file:///bucket \
--listen 0.0.0.0:8080 \
--internal-listen 10.112.0.1:9090
6.2 S3 多节点集群(生产推荐)
# docker-compose.yml(3节点集群)
version: '3.8'
services:
celld-node-1:
image: ghcr.io/denoland/celld:latest
ports:
- "8081:8080"
environment:
- CELLD_BUCKET=s3://my-bucket/celld-data
- CELLD_AWS_ACCESS_KEY_ID=${AWS_KEY}
- CELLD_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
- CELLD_AWS_REGION=us-east-1
command: >
celld
--bucket s3://my-bucket/celld-data
--listen 0.0.0.0:8080
--internal-listen 10.0.1.1:9090
--advertise 10.0.1.1:9090
--diagnose-peer 10.0.1.2:9090
--diagnose-peer 10.0.1.3:9090
celld-node-2:
image: ghcr.io/denoland/celld:latest
ports:
- "8082:8080"
environment:
- CELLD_AWS_ACCESS_KEY_ID=${AWS_KEY}
- CELLD_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
command: >
celld
--bucket s3://my-bucket/celld-data
--listen 0.0.0.0:8080
--internal-listen 10.0.1.2:9090
--advertise 10.0.1.2:9090
--diagnose-peer 10.0.1.1:9090
--diagnose-peer 10.0.1.3:9090
celld-node-3:
image: ghcr.io/denoland/celld:latest
ports:
- "8083:8080"
environment:
- CELLD_AWS_ACCESS_KEY_ID=${AWS_KEY}
- CELLD_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
command: >
celld
--bucket s3://my-bucket/celld-data
--listen 0.0.0.0:8080
--internal-listen 10.0.1.3:9090
--advertise 10.0.1.3:9090
--diagnose-peer 10.0.1.1:9090
--diagnose-peer 10.0.1.2:9090
nginx:
image: nginx:alpine
ports:
- "8080:8080"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
# nginx.conf(负载均衡)
upstream celld_cluster {
least_conn;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 8080;
location / {
proxy_pass http://celld_cluster;
proxy_http_version 1.1;
proxy_set_header Host $host;
# Durable Object 请求会携带 X-Do-Name 头
proxy_set_header X-Do-Name $http_x_do_name;
}
}
6.3 Kubernetes 部署
# celld-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: celld
namespace: celld-system
spec:
serviceName: celld-internal
replicas: 3
selector:
matchLabels:
app: celld
template:
metadata:
labels:
app: celld
spec:
containers:
- name: celld
image: ghcr.io/denoland/celld:latest
ports:
- containerPort: 8080
name: data
- containerPort: 9090
name: internal
env:
- name: CELLD_BUCKET
valueFrom:
secretKeyRef:
name: celld-secrets
key: bucket-url
- name: CELLD_AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: celld-secrets
key: aws-key
- name: CELLD_AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: celld-secrets
key: aws-secret
args:
- celld
- --bucket
- "$(CELLD_BUCKET)"
- --listen
- "0.0.0.0:8080"
- --internal-listen
- "$(POD_IP):9090"
- --advertise
- "$(POD_IP):9090"
- --diagnose-peer
- "celld-0.celld-internal.$(NAMESPACE).svc.cluster.local:9090"
- --diagnose-peer
- "celld-1.celld-internal.$(NAMESPACE).svc.cluster.local:9090"
- --diagnose-peer
- "celld-2.celld-internal.$(NAMESPACE).svc.cluster.local:9090"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "4Gi"
cpu: "2000m"
---
apiVersion: v1
kind: Service
metadata:
name: celld-data
spec:
selector:
app: celld
ports:
- port: 8080
targetPort: 8080
type: ClusterIP
七、Worker 代码编写:同一个 API,不同的运行时
celld 最重要的设计目标是:零成本迁移 Cloudflare Workers 代码。
7.1 Durable Object 定义
// src/counter.ts
export class Counter implements DurableObject {
private count: number = 0;
private db: SqliteDatabase | null = null;
// Durable Object 构造函数(celld 和 Cloudflare 兼容)
constructor(state: DurableObjectState, env: Env) {
this.state = state;
}
// 数据库初始化(懒加载)
private async getDb(): Promise<SqliteDatabase> {
if (!this.db) {
// this.state.storage 在 celld 中映射到 SQLite
this.db = this.state.storage;
}
return this.db;
}
// 每个 HTTP 请求的 handler
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const db = await this.getDb();
if (request.method === 'POST') {
// 读取当前计数
const row = db.prepare('SELECT value FROM counter LIMIT 1').first();
const currentCount = row?.value ?? 0;
const newCount = currentCount + 1;
// 写入 SQLite(持久化)
db.prepare(`
INSERT OR REPLACE INTO counter (id, value) VALUES (1, ?)
`).run(newCount);
return Response.json({ count: newCount });
}
if (request.method === 'GET') {
const row = db.prepare('SELECT value FROM counter LIMIT 1').first();
const count = row?.value ?? 0;
return Response.json({ count });
}
return new Response('Method Not Allowed', { status: 405 });
}
}
// 加上 SQLite 表初始化
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return await E_DO_FACTORY.fetch(request, env, ctx);
}
};
7.2 Worker 入口(处理请求路由到 Durable Object)
// src/index.ts
export interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// 创建或获取名为 "global-counter" 的 Counter Durable Object
const id = env.COUNTER.idFromName('global-counter');
const counter = env.COUNTER.get(id);
// 转发请求到 Durable Object
return counter.fetch(request);
}
};
7.3 wrangler.toml 配置
# wrangler.toml
name = "my-celld-app"
main = "src/index.ts"
compatibility_date = "2026-01-01"
# celld 特有的 Durable Object 类映射
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
# 存储配置(使用 SQLite 文件后端)
[vars]
CELLD_BACKEND = "sqlite"
# 多节点配置
[[celld.nodes]]
host = "10.0.1.1"
port = 9090
[[celld.nodes]]
host = "10.0.1.2"
port = 9090
[[celld.nodes]]
host = "10.0.1.3"
port = 9090
7.4 SQLite 表的自动初始化
celld 的 SQLite 后端有一个细节需要特别注意:你需要手动建表。
Cloudflare Durable Objects 的 storage API 是键值存储,而 celld 使用 SQLite。因此,如果你想在 SQLite 中使用表(而不是 KV API),需要在对象首次激活时创建表:
export class Counter implements DurableObject {
private db: SqliteDatabase | null = null;
private initialized = false;
private async initialize(): Promise<void> {
if (this.initialized) return;
this.db = this.state.storage as unknown as SqliteDatabase;
// 创建表(幂等操作,CREATE TABLE IF NOT EXISTS)
this.db.exec(`
CREATE TABLE IF NOT EXISTS counter (
id INTEGER PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0
)
`);
// 初始化默认值
const existing = this.db.prepare('SELECT value FROM counter WHERE id = 1').first();
if (!existing) {
this.db.prepare('INSERT INTO counter (id, value) VALUES (1, 0)').run();
}
this.initialized = true;
}
async fetch(request: Request): Promise<Response> {
await this.initialize();
// ... 业务逻辑
}
}
八、性能调优:榨干 celld 的潜力
8.1 内存调优
# 方式1:基于对象数量
celld --max-resident-cells 10000 --listen 0.0.0.0:8080
# 方式2:基于 RSS(推荐,根据实际机器内存)
# 假设机器有 32GB,保留 8GB 给系统和其他进程
celld --max-rss-bytes $((24 * 1024 * 1024 * 1024)) \
--listen 0.0.0.0:8080
# RSS Shedding 阈值(当使用量达到 90% 时开始驱逐)
celld --shed-threshold 0.9 --listen 0.0.0.0:8080
8.2 I/O 批处理
对于高频读写的场景,可以利用 SQLite 的事务批处理:
async fetch(request: Request): Promise<Response> {
const db = await this.getDb();
const url = new URL(request.url);
if (url.pathname === '/batch') {
const body = await request.json() as { ops: Array<{type: 'inc' | 'dec'}> };
// 使用事务批量执行
db.exec('BEGIN TRANSACTION');
try {
for (const op of body.ops) {
const current = db.prepare('SELECT value FROM counter WHERE id = 1').first();
const newVal = op.type === 'inc' ? current.value + 1 : current.value - 1;
db.prepare('UPDATE counter SET value = ? WHERE id = 1').run(newVal);
}
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK');
throw e;
}
return Response.json({ success: true, ops: body.ops.length });
}
return new Response('Not Found', { status: 404 });
}
8.3 Isolate 预热
对于延迟敏感的场景,可以在节点启动时预热热门对象:
// 启动时预热热门对象
async function warmUpPopularObjects(env: Env) {
const popularIds = ['global-counter', 'session-store', 'rate-limiter'];
await Promise.all(
popularIds.map(async (name) => {
const id = env.COUNTER.idFromName(name);
const obj = env.COUNTER.get(id);
// 触发 fetch,激活对象
await obj.fetch(new Request('http://localhost/__warmup'));
})
);
}
8.4 基准测试参考数据
根据 celld v0.2.0 的设计目标,以下是理论参考值:
| 指标 | v0.1.0 | v0.2.0 |
|---|---|---|
| 单节点最大驻留对象 | ~4,700 | ~34,000 |
| 单对象内存占用 | ~3.4MB | ~471KB |
| 内存减少比例 | — | 87% |
| S3 写入延迟 | 基准 | L0 合并后减少 60% |
| I/O 等待时 Isolate 占用 | 阻塞 | 释放 |
实际性能取决于:
- S3 Bucket 的地理延迟(与节点的物理距离)
- 网络带宽(大量并发对象激活时)
- Isolate 池大小配置
九、与 Cloudflare Durable Objects 的对比
| 维度 | Cloudflare Durable Objects | celld |
|---|---|---|
| 部署位置 | Cloudflare 全球边缘网络 | 你自己的服务器 |
| 数据主权 | 数据在 Cloudflare 服务器 | 数据完全自主控制 |
| 可用性 SLA | Cloudflare 保障 | 自维护 |
| 地理分布 | 全球 300+ 数据中心 | 取决于你的基础设施 |
| 冷启动延迟 | ~5ms(Cloudflare 边缘) | 取决于 S3 延迟 |
| 并发能力 | Cloudflare 自动扩缩容 | 取决于你的集群规模 |
| 存储 | Cloudflare KV(内部) | SQLite → S3 |
| 成本模型 | 按请求计费 | 按基础设施成本计费 |
| 代码兼容性 | 原生 | 99% 兼容(同 API) |
| 生态 | Cloudflare Workers 生态 | Deno 生态 + Workers API |
| Isolate 模型 | 独占 Isolate | 共享 Isolate 池 |
| 内存效率 | 专用 | 高效(v0.2.0) |
何时选择 celld:
- ✅ 数据合规要求数据不出境
- ✅ 需要完全自主的基础设施
- ✅ 已有大量 Cloudflare Workers 代码,想迁移到私有部署
- ✅ 需要与现有 Kubernetes / 自托管基础设施集成
何时继续用 Cloudflare DO:
- ✅ 需要全球低延迟边缘分发(毫秒级)
- ✅ 不想维护自己的服务器集群
- ✅ 成本可预测的按请求计费更适合你
十、局限性与挑战
诚实地讲,celld 还不是生产就绪(Production-Ready)的系统。当前版本(v0.2.0)有几个需要关注的局限:
10.1 S3 延迟是全局瓶颈
每个 Durable Object 的每次状态变更都需要写 S3。即使使用了 L0 合并到 L1 的优化,S3 的网络延迟(通常是 10-100ms)仍然会叠加到每个请求上。对于强一致性要求的场景,这个延迟是硬限制。
10.2 尚未完全支持的 API
虽然 celld 尽力兼容 Cloudflare Workers API,但以下 API 仍有差异:
// 已支持
this.state.storage.get(key); ✅
this.state.storage.put(key, value); ✅
this.state.storage.delete(key); ✅
db.prepare(sql).run(); ✅
// 部分支持
ctx.waitUntil(promise); ⚠️ 语义不完全一致
ctx.passThroughOnException(); ⚠️ 行为有差异
// 尚未支持
this.state.storage.getMetadata(key); ❌
this.state.storage.transaction(fn); ❌(需要手动实现)
10.3 冷启动问题
当一个对象从「不活跃」状态(仅存在于 S3)切换到「活跃」状态时,需要从 S3 读取完整的 SQLite 文件。对于大型对象,这个冷启动时间可能达到秒级。
# 监控冷启动
curl http://localhost:8080/__celld/metrics | jq '.activation_latency_ms'
10.4 没有自动缩容
v0.2.0 有 RSS Shedding(驱逐低活跃对象),但没有基于负载的自动扩缩容。如果你需要根据流量自动增减节点,需要自己实现 Kubernetes HPA 或类似机制。
十一、未来展望:从 v0.2.0 到 1.0 的路
celld 目前的版本号是 v0.2.0,距离 1.0 稳定版还有一段路。基于项目的发展轨迹和开源社区的讨论,以下是可以预见的方向:
1. 强一致性的 Raft 模式(可选)
对于金融级应用,celld 可能引入可选的 Raft 共识模式——用更强的协调成本换取更强的一致性保证。这不是要取代当前的 S3 协调模式,而是给有更高要求的用户提供另一个选项。
2. 多 Bucket 地理分布
当前所有节点共享同一个 S3 Bucket。未来可能支持多个 Bucket 按地理位置分布,减少跨区域复制的延迟。
3. 内存压缩优化
V8 的 Isolate 堆当前在 Cell 等待 I/O 时还没有真正释放内存(GC 还没优化到这个路径)。当 V8 支持这个优化后,单对象实际内存占用可能从 471KB 进一步降低。
4. 流式复制
当前 S3 写入是完整文件写入。未来可能支持流式 WAL(Write-Ahead Logging)复制,减少每次状态变更的数据量。
5. 与 Deno Deploy 的深度集成
Deno Deploy 是 Deno 官方的边缘托管平台。celld 的一个自然演进方向是:让同一个应用同时在 Deno Deploy(Cloudflare 边缘)和 celld(私有部署)上运行,根据环境变量切换运行时。
十二、总结:重新定义「有状态 Serverless」的边界
celld 带来的不是微小的改进,而是一个根本性的范式挑战:
Serverless 不一定意味着无状态。有状态的应用也可以跑在 Serverless 架构中——只是状态存在哪里,由谁控制。
Cloudflare Durable Objects 的编程模型(每个对象一个微型数据库 + 事件驱动处理)是过去几年最有影响力的 Serverless 创新之一。但它最大的弱点——对 Cloudflare 基础设施的绑定——现在被 celld 解决了。
v0.2.0 的内存革命(Isolate 池 + Turn by Turn 执行)让 celld 从一个「概念验证」变成了真正可以部署在生产环境的系统。471KB vs 3.4MB 的差距,决定了在同样的硬件上,celld 可以服务多一个数量级的并发用户。
对于中国开发者来说,celld 还有一个特殊意义:它让我们第一次可以在不依赖任何境外云服务的情况下,使用 Durable Objects 风格的编程模型。这对政务、金融、医疗等对数据主权有严格要求的行业,是实打实的需求。
如果你正在构建高并发、长连接、需要状态持久化的应用,不妨给 celld 一个机会——你的 Worker 代码几乎不需要改动,但你的数据可以真正回到自己的服务器上。
参考资源
- celld 官方文档:https://celld.dev/docs
- celld GitHub:https://github.com/denoland/celld
- v0.2.0 Release Notes:https://github.com/denoland/celld/releases/tag/v0.2.0
- Durable Objects 官方文档:https://developers.cloudflare.com/durable-objects/
- LTX 块格式文档:https://github.com/penberg/ltx
- jemalloc 官网:https://jemalloc.net/