1 简介
谷歌文档是一种协作文档编辑服务。
协作文档编辑服务可以通过两种方式设计:
- 设计为C/S架构的集中式设施,为所有用户提供文档编辑服务
- 使用点对点技术设计,以便在单个文档上协作
大多数商业解决方案侧重于客户端服务体系结构,以实现更精细的控制。因此,我们将关注使用客户端服务体系结构设计服务。让我们看看在这一章节中我们将如何进展。
2 需求
2.1 功能性
文档协作
多用户应该能够同时编辑文档。此外,大量用户应该能够查看文档。
冲突解决
系统应该将一个用户做的编辑推送给所有其他协作者。如果他们正在编辑文档的同一部分,系统还应解析用户之间的冲突。
建议
用户应该能够获得有关在文档中完成常用单词、短语和关键词的建议,以及有关修复语法错误的建议。
查看计数
文档的编辑应能够看到该文档的查看计数。
历史
用户应该能够查看文档上的协作历史。
2.2 非功能性
延迟
不同的用户可以连接起来协作同一份文档。为来自不同区域的用户维护低延迟是具有挑战性的。
一致性
系统应能够解析用户并发编辑文档时之间的冲突,从而实现文档的一致视图。与此同时,不同区域的用户应看到文档的更新状态。对于连接到同一区域和不同区域的用户来说,保持一致性都是重要的。
可用性
该服务应该一直可用,并展示出对故障的鲁棒性。
可扩展性
大量用户应该能够同时使用该服务。他们可以查看相同的文档,也可以创建新文档。
3 组件
3.1 数据存储
- 关系数据库 —— 用于保存用户信息和文档相关信息以施加特权限制
- NOSQL —— 用于存储用户评论以获得更快的访问速度
- 时间序列 —— 用于保存文档的编辑历史记录
- Blob 存储 —— 用于存储文档中的视频和图像
- 缓存 —— 分布式缓存,如 Redis 和 CDN,为最终用户提供良好的性能。使用 Redis存储不同的数据结构,包括用户会话、类型预期服务的功能、频繁访问的文档。CDN存储频繁访问的文档和重量级对象如图像和视频。
处理队列
针对每次微小字符更改使用 HTTP 调用是低效的。因此使用 WebSockets 减少开销,并通过不同用户实时观察文档的更改。
其他组件
其他组件包括会话服务器,维护用户的会话信息。通过会话服务器管理文档访问权限。本质上,还将有配置、监控、发布-订阅和日志记录服务来处理监控任务,如在服务器失败时监控和选举领导者,排队用户通知等任务,以及记录调试信息。
图 1.0: 协作文档编辑服务的详细设计:
4 工作流程
4.1 协作编辑和冲突解决
每个请求都会转发到操作队列。这是解析同一文档的不同协作者之间冲突的地方。如果没有冲突,则通过会话服务器将数据批量存储在时间序列数据库中。像视频和图像这样的数据会被压缩以优化存储,而字符会被立即处理。 历史:借助时间序列数据库,可以恢复文档的不同版本。可以使用 DIFF 操作来比较版本并标识差异以恢复同一文档的旧版本。
4.2 异步操作
通知、电子邮件、查看次数和评论都是可以通过像 Kafka 这样的发布-订阅组件排队的异步操作。API 网关生成这些请求并将它们转发到发布-订阅模块。
4.3 建议
建议以类型提前服务(typeahead service)的形式出现,该服务提供通常使用的单词和短语的自动完成功能。类型提前服务还可以从文档中提取属性和关键词并向用户提供建议。由于单词数量可能很高,我们将为此目的使用 NoSQL 数据库。此外,最常用的单词和短语将存储在像 Redis 这样的缓存系统中。
导入和导出文档
应用程序服务器执行许多重要任务,包括导入和导出文档。应用程序服务器还将文档从一种格式转换为另一种格式。例如,.doc 或 .docx 文档可以转换为 .pdf 或反之亦然。应用程序服务器也负责为类型预期服务提取特征。
5 详细设计
5.1 文档编辑器
文档由以特定顺序排列的字符组成。每个字符都有一个值和一个位置索引。字符可以是字母、数字、回车()或空格。索引表示字符在有序字符列表中的位置。
文本或文档编辑器的作用是在文档中的字符上执行插入()、删除()、编辑()等操作。下面是文档的描绘以及编辑器将如何执行这些操作。
文档编辑器如何执行各种操作
5.2 并发性
不同用户对同一文档的协作可能导致并发问题。若多个用户编辑文档的同一部分,可能出现冲突。由于用户在本地有文档的副本,服务器上的最终文档状态可能与用户在他们端看到的不同。在服务器推送更新版本后,用户会发现意外结果。
① 在同一位置索引处添加字符
两个用户修改同一字符可能导致并发问题:
② 删除同一字符
删除同一字符,可能导致意外更改:
第二个例子表明,不同用户应用相同的操作不会是幂等的。因此,在多个协作者同时编辑文档同一部分时,需冲突解决。
协作编辑中并发问题的解决方案应遵循规则:
- 交换律:应用操作的顺序不应影响最终结果
- 幂等性:重复的相似操作只应用一次
6 冲突解决技术
6.1 操作转换(OT)
广泛用于协作编辑中的冲突解决的技术,一种【无锁】、【非阻塞】的冲突解决方法。若协作者之间的操作冲突,OT会解析冲突并将正确的汇聚状态推给最终用户。因此,OT为用户提供一致性。
OT 使用位置索引方法执行操作来解析上面讨论的那些冲突。通过保持交换律、幂等性来解决上述问题。
OT示例:
基于 OT 的协作编辑器在满足以下两个属性时一致:
- 因果关系保持:如果操作 a 发生在操作 b 前,那先执行操作 a,然后执行操作 b
- 收敛:不同客户端上的所有文档副本最终相同
上述属性是 CC 一致性模型的一部分,CC 一致性模型是协作编辑中一致性维护的模型。
OT的缺点
对字符的每个操作都可能需要更改位置索引。这意味着操作之间存在顺序依赖关系。它的开发和实现具有挑战性。
OT是一组复杂算法,其正确实现在实际应用中已被证明有挑战性。Google Wave 团队花两年时间实现OT。
6.2 无冲突复制数据类型 (CRDT)
是为了改进 OT。CRDT 具有:
- 复杂的数据结构
- 但简化的算法
CRDT 通过为每个字符分配两个关键属性来满足交换律和幂等性:
- 为每个字符赋予全局唯一标识
- 全局订购每个字符
每个操作现在都有一个更新后的数据结构:CRDT 简化的数据结构
SiteID 唯一标识请求操作的用户站点,带有一个值和一个 PositionalIndex。PositionalIndex值可以是分数:
- 某些操作不会改变其他字符的 PositionalIndex
- 避免不同用户操作之间的顺序依赖性
示例描绘来自站点 ID 123e4567-e89b-12d3 的用户在 PositionalIndex 为 1.5 的位置插入一个值为 A 的字符。尽管添加了新字符,但使用小数索引保留了现有字符的位置索引。因此,避免了操作之间的顺序依赖性。如下所示,在 O 和 T 之间插入()并没有影响 T 的位置。
防止操作之间的顺序依赖性:
CRDT 确保用户之间的强一致性。即使一些用户处于离线状态,当他们重新联机时,最终用户处的本地副本也将汇聚。
尽管众所周知的在线编辑平台如 Google 文档、Etherpad 和 Firepad 使用 OT,但 CRDT 使协作文档编辑中的并发和一致性变得容易。事实上,使用 CRDT,可以实现无服务器点对点协作文档编辑服务。
注意
OT 和 CRDT 是协作编辑中冲突解决的良好解决方案,但我们使用 WebSockets 可以突出显示协作者的光标。其他用户将预期协作者的下一个操作的位置,并自然避免冲突。
7 评估
一致性
操作转换(OT)和冲突不定决议数据类型(CRDT)在文档中实现冲突解决的强一致性。
时间序列数据库能保留事件的顺序。一旦 OT 或 CRDT 解析了任何冲突,最终结果就保存在数据库。这有助我们在单个操作方面实现一致性。
在IDC内的不同服务器之间保持文档状态的一致性。要在同一IDC内同时复制更新后的文档状态,可使用 Gossip 协议这样的点对点协议。这不仅提高一致性,还会提高可用性。
可用性
我们的设计通过使用副本以及使用监控服务监控主副本服务器来确保可用性。操作队列和数据存储等关键组件在内部管理自己的复制。
由于使用 WebSockets,WebSocket 服务器可将用户连接到会话维护服务器,这些服务器将确定用户是否正在主动查看或协作文档。
因此,保留多个 WebSocket 服务器将增加设计的可用性。最后,采用缓存服务和 CDN 改进可用性。
可扩展性
由于使用微服务,若操作队列的请求数量超过其容量,可轻松单独扩展每个组件。可使用多个操作队列。此时,每个操作队列将负责单个文档。可将不同用户请求的与单个文档相关的操作转发到特定队列。生成的队列数量将等于活动文档的数量。因此可实现水平扩展性。
参考:
- https://medium.com/@sureshpodeti/system-design-google-docs-93e12133a979 本文由博客一文多发平台 OpenWrite 发布!