celld从v0.1.0到v0.2.0为何不能滚动升级:两个破坏性变更详解
celld从v0.1.0到v0.2.0为何不能滚动升级:两个破坏性变更详解
celld 是一个自托管、分布式的 Durable Objects 开源守护进程,它在你自己的机器上运行 Cloudflare Workers,每个对象对应一个独立的 SQLite 数据库,并持续复制到你自己拥有的 S3 兼容存储桶中。如果你正准备把 v0.1.0 的集群(fleet)升级到 v0.2.0,这篇文章必须先读:这次升级不能滚动升级,必须采用"先停光、再启动"的一次性发布方式。下文详解背后两个破坏性变更的来龙去脉,以及正确的升级步骤。
先搞懂背景:celld 平时是怎么滚动升级的
滚动升级之所以在大多数场景可行,是因为 celld 的节点下线是优雅的:
- 收到
SIGTERM(即systemctl stop、docker stop、K8s Pod 删除都会发出的信号)后,节点的/__celld/health立即报告不健康,负载均衡停止向它路由; - 节点把在途请求做完,同时逐个释放驻留 cell 的所有权,默认并发上限 128(
CELLD_RELEASES),对端节点立即接管被释放的 cell; - 整个排空过程受
CELLD_SHUTDOWN_DRAIN_MS(默认 25000 毫秒)约束。
因此官方推荐的日常发布方式是:用编排器的滚动更新逐个停节点,等替补节点健康后再停下一个;滚动期间还要用 celld diagnose 观察,等所有节点报告 restoring=0 再重启下一个节点。官方原文见 docs/README.md。
但官方在同一页明确写了例外:
The upgrade from v0.1.0 to v0.2.0 must not be a rolling update. Stop every v0.1.0 node, then start the v0.2.0 nodes.
破坏性变更一:所有权记录里的地址换"门"了 🚪
celld 集群唯一的协调介质是存储桶:每个 cell 的所有权租约写在桶里,谁拿到了租约谁就是这个 cell 的唯一写入者,节点之间靠桶里的租约互相发现、互相通信。
问题出在"地址"上:
- v0.1.0:节点只有一个监听器,对外服务和节点间通信走同一个地址;
- v0.2.0:节点拆成两个监听器——公共监听器(
--listen)只服务部署的 Worker,内部监听器(--internal-listen)承载节点间协议和运维 API,并且--advertise必须指向内部监听器。
后果就是:v0.2.0 节点写入的所有权记录,指的是内部监听器地址;v0.1.0 的节点拿着旧格式的地址去连新节点,走的是旧的单监听器假设,无法正确跟随到对端。反过来,新格式集群里混着的旧节点写的记录,同样无法被可靠地追到。
所有权记录是全集群共享的唯一事实来源,一旦新旧版本共存,节点间的所有权交接、cell 接管、诊断探测都会失效。所以官方结论很干脆:"A fleet must not mix the two versions." 两个监听器的设计动机(内部监听器不带 Worker 流量、运维 API 无需暴露公网)见 docs/security.md。
破坏性变更二:L1 压缩的新格式,旧读者读不了 📦
第二个变更发生在数据复制层。
celld 把每个 cell 的 SQLite WAL 以 LTX 文件形式持续上传到桶中(对象布局为 cells/<cell>/ltx/e<epoch>/,见 crates/celld/ltx_repl.rs)。一个热点 cell 的 L0 层可能积累成千上万个 LTX 小对象——如果 cell 迁到别的节点,接管方得串行下载几千个对象,接管变成分钟级的失败。
v0.2.0 引入了 L1 压缩(crates/ltx/src/compactor.rs):把 L0 小对象合并成更少的 L1 块对象,接管时只需读几十个对象。而 L1 对象采用的是新编码(LTX v0.5.2:页头带 4 字节压缩长度字段 + 原始 LZ4 块),与旧编码(每页一个 LZ4 帧)字节布局不同。
关键在于兼容方向是单向的:
- ✅ v0.2.0 的读者两种编码都能读(见 crates/ltx/src/ltx.rs 的模块说明:"decodes a complete LTX file with either v0.5.2 LZ4 blocks or the older LZ4 frames");
- ❌ v0.1.0 的读者读不了 L1 块对象。
也就是说:只要某个 cell 完成了第一次 L1 发布,它再被 v0.1.0 节点接管时,恢复会直接失败。在滚动升级中这几乎必然发生——旧节点正在排空,它的 cell 会被接管,而这些 cell 的数据桶里已经可能被新节点写入了新格式对象。
如果确实需要过渡窗口,文档提供了一个开关:给每个节点都设置 CELLD_LTX_COMPACTION=0(暂停 L1 对象产生),但官方仍建议一次性整体升级,见环境变量表 docs/README.md。
正确的升级方式:先全停,再全启(Big Bang)🛠️
把两个变更放在一起看就明白了——一个破坏的是集群级共享的所有权记录,一个破坏的是集群级共享的复制数据,任何新旧节点共存的窗口都会让某一方追不上另一方。正确步骤:
- 规划维护窗口,提前用
celld diagnose确认集群健康、所有节点restoring=0; - 优雅停止全部 v0.1.0 节点(发
SIGTERM),等待排空完成。此时所有 cell 变成 inactive,状态完整留在桶里,不丢任何已确认写入(celld 保证 RPO=0); - 启动 v0.2.0 节点,指向同一个桶;新节点从桶恢复所有 cell,按新格式继续写入所有权记录;
- 升级后再次
celld diagnose,确认节点健康、协议版本一致。
由于"桶才是持久的事实来源,节点是可替换的",这个停机窗口只损失服务连续性,不损失数据。
关键文件速查清单
| 资料 | 说明 |
|---|---|
| docs/README.md | 官方原文:v0.1.0 → v0.2.0 禁止滚动升级 |
| docs/security.md | 公共/内部监听器分离与 --advertise 语义 |
| docs/README.md | CELLD_LTX_COMPACTION 等升级相关环境变量 |
| crates/celld/protocol.rs | 桶中部署对象类型与版本校验 |
| crates/ltx/src/ltx.rs | LTX 文件读写与新旧两种编码的兼容说明 |
| crates/ltx/src/compactor.rs | L1 压缩器实现 |
| crates/celld/ltx_repl.rs | 进程内复制后端与桶对象布局 |
一句话总结:celld 的滚动升级依赖"新旧节点能互相同步",而 v0.2.0 同时改掉了节点间寻址格式和复制数据格式,且都是单向兼容——所以升级 v0.2.0 时,先停光旧节点、再启动新节点,是唯一的官方姿势。
更多推荐
所有评论(0)