celld 死节点对账与 GC:过期租约如何被自动清理与修复
【免费下载链接】celldself-hosted, distributed Durable Objects项目地址: https://gitcode.com/GitHub_Trending/ce/celld
celld 是一款自托管的分布式 Durable Objects 运行时,多个节点通过共享的对象存储桶协同工作。当某个节点宕机或失联后,它在桶中留下的"节点租约"记录会过期,如果一直堆积,就会污染集群的容量与放置决策。celld 内置了一套死节点对账与 GC 机制,由被选举出的 waker 节点周期性地把这些过期租约清理掉,让桶始终反映真实存活的集群。
🧩 先理解背景:celld 里的"租约"是什么
celld 的设计哲学是"桶是唯一的持久事实源,节点可随时替换"(见 README.md)。为了实现这一点:
- 每个节点定期在桶的
nodes/前缀下写入一份自己的租约记录(nodes/<节点名>.json),里面带有expires_ms(过期时间)——相当于"我承诺在这个时间点之前还会续租"。 - 节点之间互相发现、做放置决策时,会列出
nodes/下的记录,只看租约未过期的节点(过滤逻辑在 crates/celld/ownership_store.rs)。 - 如果节点崩溃后没有再续租,它留下的记录就会变成"死记录"——不删掉它,集群就永远以为这台机器还活着。
死节点 GC 要做的,就是把这些过期租约识别出来、并安全地修复桶中的状态。核心代码在 crates/celld/dead_node_gc.rs。
🏆 谁负责清理:全集群选举出的 waker 角色
清理不能每个节点都做,否则会互相打架。celld 的做法是:每个节点在每个 tick(默认 60 秒,由CELLD_WAKER_TICK_MS控制)都尝试去持有桶里wake/waker.json这把建议性的 waker 租约,抢到的那一个才执行本轮 GC。
- 抢租约用的是对象存储的 compare-and-swap(CAS)条件写,天然保证同一时刻只有一个持有者(实现见 crates/celld/wake.rs 中的
try_hold_waker)。 - 持有者每 TTL/3 的时间续租一次;续租失败就立刻取消本轮 GC,避免"两个清理者"长时间并存。
- 这个租约是建议性的:即使某个持有者中途崩溃,正确性也不会受损——最坏情况只是多花一轮 tick 由别人接手。
- 该循环与"到期唤醒扫描"共用同一个定时任务,入口在 crates/celld/main.rs。
🔍 对账三步走:如何判定一个节点"死了"
每一轮run_pass(crates/celld/dead_node_gc.rs)只做三件事,判定规则刻意写得极其保守:
- 列出
nodes/前缀下的全部租约记录; - 读回每条记录,校验"文件名里的节点名"与"记录体内的节点名"完全一致;
- 只有当
expires_ms <= 当前时间时才判定为死节点(纯函数判定见 crates/logic/dead_node_reconciliation.rs 的node_record_is_dead)。
注意两点设计:
- 不信时钟之外的任何信号:判定只看租约是否到期,不依赖网络探活,时钟偏差也不会造成误删——因为删除前还要再过一道 CAS(见下文)。
- 决策逻辑与 I/O 分离:判定函数放在无 I/O 的 crates/logic/ 逻辑层,方便单元测试穷举边界情况。
🧹 修复分两步:先清碎片,再删租约
判定一个节点死亡后,GC 会做两次清理,顺序不能颠倒:
第 1 步:清理node-cells/标记碎片
历史上 celld 曾在node-cells/<节点>/<代次>/<cell>路径下写"归属索引"标记。节点死掉后,它名下所有代次的标记都成了垃圾(按前缀匹配、丢弃代次,解析函数见 crates/logic/dead_node_reconciliation.rs 的parse_marker_key)。GC 会列出这些标记并以 64 并发删除(crates/celld/dead_node_gc.rs)。
第 2 步:用"墓碑 + CAS"删除租约记录
对象存储不支持"条件删除",于是 celld 采用一个精巧的两段式(retire_dead_node):
- 先以 CAS 把记录改写为一个墓碑(
expires_ms: 0,仍然是一份"死的"记录); - CAS 成功说明租约在读取后没被别人续过,再执行真正的删除;
- 如果 CAS 失败,说明节点恰好复活并续租了——直接放弃,什么都不删。
即使进程在两步之间崩溃也没关系:留下的墓碑记录读取时依然是"死"的,下一轮 GC 会接着删掉。整个流程是崩溃安全(crash-safe)的。
⏱️ 失败怎么办:指数退避的重试
如果标记删除有失败(比如存储短暂不可用),GC 不会立刻重扫,而是按指数退避推迟重试:延迟从 1 个 tick 起,逐次翻倍,封顶 64 个 tick(纯函数retry_delay_ms见 crates/logic/dead_node_reconciliation.rs)。
- 退避状态只保存在进程内存里(
retries/swept),重启后自动重建,不落盘、不引入新的协调对象; - 一个"扫完但没删干净"的节点不会重复触发全桶扫描,只有条件删除剩余时才重试;
- 每次清理完成都会打出一条
dead_node_reconciliation日志事件,包含清理的标记数、退休数与失败数,便于运维核对。
🔎 如何在生产集群中观察它
- 用
celld diagnose --bucket s3://your-bucket会列出每一条节点租约并探测存活对等节点,能直接区分"过期记录、地址异常、节点不可达"等状态(用法见 README.md 的 Operate a fleet 一节); - 相关的所有权与防写坏(fencing)机制的完整论证,推荐阅读官方文档 docs/fencing.md:为什么"epoch 前缀 + 条件写"能保证一个 cell 同时只有一个写者;
- 环境变量的完整清单在 docs/README.md,其中
CELLD_WAKER_TICK_MS控制 waker 与 GC 的 tick 间隔。
📌 小结
celld 的死节点对账与 GC,本质上是一套"桶内自治"的清理协议:建议性选举决定谁来扫,保守判定(名字一致 + 租约到期)决定扫谁,墓碑 CAS保证删除绝对不误伤复活节点,指数退避保证存储抖动时不会变成重试风暴。全程不依赖任何外部协调服务——这正是 celld "无控制面、无共识" 设计哲学的又一次体现。
【免费下载链接】celldself-hosted, distributed Durable Objects项目地址: https://gitcode.com/GitHub_Trending/ce/celld
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考