架构 (IT)系统架构师

如何为一个支持离线优先功能的全球分布式协作编辑平台构建无冲突的复制数据类型(CRDT)实现,该平台支持 1000 万个并发用户,同时确保强最终一致性,而无需集中协调,并在延长的离线场景中防止丢失更新问题?

用 Hintsage AI 助手通过面试

对问题的回答

要在如此大规模下架构一个基于 CRDT 的协作系统,必须放弃传统的 操作变换(OT)模型,这些模型需要中央权威来序列化操作。这些遗留方法从根本上阻止了真正的离线优先能力,因为它们要求与协调服务器保持持续连接以进行冲突解决。相反,实施 基于状态的 CRDTs(特别是 RGA - 可复制可增长数组用于序列数据),利用交换性、结合性和幂等的数学属性来保证在没有协调或共识协议的情况下的收敛。

部署 增量状态反熵 协议,客户机交换其本地状态之间的差异,而不是完整的状态快照。这种方法在同步期间相比于天真的基于状态的复制方案减少了数量级的带宽消耗。必须利用 混合逻辑时钟(HLC),将物理时间戳与逻辑计数器相结合,以建立因果关系,并在没有严格 NTP 依赖的情况下处理区域之间的时钟偏差。最后,实施 墓碑垃圾回收,使用基于时间段的修剪来防止删除标记造成无边界的内存增长,同时保持对延迟或分区副本的因果跟踪。

生活中的情况

我们的团队被委托重建一个类似 Figma 的设计工具的实时协作引擎,支持 50,000 个企业团队跨时区合作。遗留系统使用 Redis 发布/订阅与 WebSocket 连接通过中央 Node.js 服务器,在行业会议期间,当 10,000+ 用户尝试离线编辑并随后同时重新连接时,该系统崩溃。此激增导致不可逆的状态分歧和永久文档损坏,造成 48 小时的停机和大量客户流失。

我们首先评估了 集中式 OT 与租赁锁,这是一种用户必须在离线编辑之前获取文档部分的独占锁的方案。该解决方案承诺强一致性和类似于传统数据库的熟悉 ACID 语义。然而,它需要不断的连接以进行锁续约,完全违反了离线优先要求,并在锁服务器上创建了一个灾难性的单点故障,在网络分区期间将使整个产品无法使用。

第二个候选解决方案提出了 最后写入胜出(LWW)与向量时钟,利用 AWS DynamoDB 时间戳以确定性方式解决冲突。虽然这种方法支持真正的离线编辑,并且与现有的云基础设施实现起来非常简单,但在并发编辑期间遭遇了灾难性的数据显示丢失。当两个设计师同时离线移动相同组件时,只有最后一次同步的时间戳会存活,默默地破坏了协作的本质,完全丢弃了一个用户的工作而没有警告。

我们最终选择了 基于状态的 CRDTs,使用 Yjs 库,通过 QUIC 协议传输自定义增量状态同步。这一架构选择消除了编辑期间对中心协调的需求,允许在网络分区时保持收敛的数学保证,并支持在没有互联网连接的情况下在同一局域网内的 P2P 同步。我们实施了 Merkle 树 的增量编码,以较完全状态转移减少 94% 的同步负载,同时保持文档历史的密码完整性。

在六个月的生产流量后,系统成功应对了影响整个区域的 72 小时 Cloudflare 停机,用户继续离线编辑,并在重新连接时无缝合并,零数据丢失。文档加载时间从 4.2 秒改善到 180 毫秒,因为消除了用于冲突解决的服务器往返。基础设施成本下降了 60%,因为消除了协调开销并能够使用边缘缓存,而不是强大的集中计算实例。

候选人通常遗漏的内容

CRDTs 如何处理用户删除内容时墓碑的无限增长,以及什么触发它们的安全移除?

大多数候选人假设删除可以立即从内存中清除,但 CRDTs 需要墓碑以跟踪因果关系,并防止已删除的数据在与滞后副本合并时复活。解决方案实施了 因果稳定性 检测,使用向量时钟比较;当节点观察到所有其他副本已确认在特定时间戳之前的删除时,墓碑变得稳定并符合移除的条件。必须部署 基于时间段的垃圾回收,在可配置的生存时间后,墓碑被标记为待移除,只有当因果切口证明没有滞后副本需要它们进行收敛时才会被物理删除。没有这个机制,六个月前的某个离线设备可能会在重新连接时复活古老的已删除数据,违反用户的永久删除和隐私合规的期望。

基于状态的 CRDTs 与基于操作的 CRDTs 在网络需求方面的根本区别是什么,在带宽受限的移动环境中为什么会选择其中一个?

基于操作的 CRDTs 需要来自运输层的准确一次交付和因果广播保证,如 Apache KafkaRabbitMQ,使它们不适用于在消息可能丢失或重复的情况下不可靠的移动网络。基于状态的 CRDTs 允许消息重复和任意延迟,但传统上需要传输整个文档状态,这对蜂窝网络上的大设计文件来说是不可承受的昂贵。先进的解决方案使用 增量状态 CRDTs 仅传输自上次成功同步以来的变更,结合了基于状态的网络鲁棒性与基于操作的方法的效率。在移动环境中,您实施 指数退避增量同步布隆过滤器,避免重新发送已经看到的更新,与全状态同步相比,减少 99% 的移动数据使用,同时保持离线优先能力。

如何在两个用户同时在同一光标位置插入文本时防止序列 CRDTs 中的“交错异常”,确保他们的编辑作为连续块出现,而不是任意交错的字符?

标准的 LWW 或简单的基于计数器的 CRDTs 导致“helo”问题,其中在相同位置同时插入“hi”和“bye”的并发插入变成了难以理解的“hbyeio”。解决方案要求使用 可复制的可增长数组(RGA)Woot 算法,基于节点 ID 和逻辑时间戳为每个字符分配全球唯一标识符(GUID),并设定确定性的打破平局规则来建立总顺序。在插入时,您将新元素附加到特定前任 ID,而不是数字索引,创建一个链表结构,使得并发插入形成独立分支,确定性地合并而不交错。您还必须实施 游程编码 优化,以防止 GUID 的开销主导文档大小,通常实现文本文档少于 20% 的元数据开销,同时保持直观的合并语义,以保留并发编辑的意图。