CRDT(Conflict-free Replicated Data Type)经常被以一种过于美好的方式介绍:节点之间无需协调,并发修改自动收敛到一致状态。这个概括既准确又误导——它对了语义,错了代价。
CRDT 的真实代价
在 Nexus 早期版本里,我们用过 Yjs 风格的 CRDT。用了三个月之后,我们发现以下几件事都不像宣传里那样美好。
代价 1:元数据无限增长
原生 CRDT 为了保留并发修改的因果信息,会保存大量元数据(向量时钟、tombstone、操作历史)。在长寿命的 Agent 集群里,这些元数据会随时间无限增长。我们看到过单个状态对象的元数据膨胀到正文的 40 倍。
代价 2:合并复杂度
"自动收敛"这个承诺隐藏了 CPU 代价。在千 Agent 规模下,合并操作变成了显著的性能热点——某些节点 30% 的 CPU 都花在了合并上。
代价 3:调试灾难
CRDT 的合并语义不直观。当业务方看到状态收敛到一个"看起来不对"的值时,他们没办法用直觉解释为什么。我们需要花大量时间在工程师之间科普"这就是 CRDT 的合并规则"——这些时间本可以用来做更有价值的事。
Nexus 的非传统选择
我们最终在 2.0 版本里魔改了状态层,做了三个不太"教科书"的决定。
决定 1:因果 epoch + 增量快照,替代向量时钟
我们用一个粗粒度的因果 epoch(基于物理时钟 + 序列号)替代了细粒度向量时钟。代价是某些极端并发场景下需要更多的协商;收益是元数据从 O(n) 降到 O(1),集群运行 30 天后状态对象大小不再膨胀。
决定 2:不是所有数据都用 CRDT
原生 CRDT 思维倾向于把"所有共享状态"都做成 CRDT。但实际上很多数据(比如 Agent 的能力声明、协议版本、配置)变更频率极低、并发冲突几乎不可能发生。这些数据用经典的 leader-based 复制更合适。Nexus 现在是 70% leader-based + 30% CRDT 的混合状态层。
决定 3:合并语义对业务可见
我们让业务代码可以查询"这个值是怎么合并出来的"——不是泄漏 CRDT 内部细节,而是提供一个抽象的"合并轨迹"。这样当业务方对收敛结果有疑问时,他们能自己看到原因,不需要找协议组工程师。
▸工程信念:当一个学术抽象在生产中无法被业务方理解时,工程上的正确做法不是要求业务方学习这个抽象,而是给抽象增加可解释性。
收益
上面三个决定加起来,让 Nexus 的状态层在千 Agent 规模下:内存占用下降 71%、合并 CPU 占用下降 64%、跨团队的状态相关 issue 数量下降了 80%。
我们没有发明新的 CRDT,我们只是更诚实地承认它的代价,并在合适的地方拒绝用它。