编程 celld 深度实战:把 Cloudflare Durable Objects 跑在你自己的服务器上——从零共识架构到 v0.2.0 内存革命全链路拆解

2026-08-16 14:47:22 +0800 CST views 9

一、背景:为什么你需要这个

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 ──┘

工作流程:

  1. Durable Object 初始化时,它的 SQLite 数据库文件被写入 S3 Bucket
  2. 节点通过 S3 读取和写入这个文件
  3. 节点之间不直接通信,只通过 S3 间接协调
  4. 没有主节点,没有选举,没有共识

这看起来不可思议——怎么可能不用共识就保证一致性?答案在于 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 块格式)

压缩过程:

  1. 当 L0 文件数量达到阈值,触发压缩
  2. 将所有 L0 对象合并为一个 L1 块
  3. 源 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.0v0.2.0
单节点最大驻留对象~4,700~34,000
单对象内存占用~3.4MB~471KB
内存减少比例87%
S3 写入延迟基准L0 合并后减少 60%
I/O 等待时 Isolate 占用阻塞释放

实际性能取决于:

  • S3 Bucket 的地理延迟(与节点的物理距离)
  • 网络带宽(大量并发对象激活时)
  • Isolate 池大小配置

九、与 Cloudflare Durable Objects 的对比

维度Cloudflare Durable Objectscelld
部署位置Cloudflare 全球边缘网络你自己的服务器
数据主权数据在 Cloudflare 服务器数据完全自主控制
可用性 SLACloudflare 保障自维护
地理分布全球 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/

推荐文章

实现微信回调多域名的方法
2024-11-18 09:45:18 +0800 CST
前端如何优化资源加载
2024-11-18 13:35:45 +0800 CST
15 个 JavaScript 性能优化技巧
2024-11-19 07:52:10 +0800 CST
CSS 奇技淫巧
2024-11-19 08:34:21 +0800 CST
Vue3中的JSX有什么不同?
2024-11-18 16:18:49 +0800 CST
程序员茄子在线接单