1.3. 最终一致性

在上一篇文档 为什么选择 CouchDB? 中,我们了解到 CouchDB 的灵活性使我们能够在应用程序不断发展和变化时不断改进我们的数据。在本主题中,我们将探讨如何顺应 CouchDB 的“发展规律”来简化我们的应用程序,并帮助我们自然地构建可扩展的分布式系统。

1.3.1. 顺势而为

分布式系统是能够在广域网上稳定运行的系统。网络计算的一个特殊功能是网络链接可能会消失,并且有大量策略可用于管理这种类型的网络分段。CouchDB 与其他系统不同,因为它接受最终一致性,而不是像 RDBMSPaxos 那样将绝对一致性置于原始可用性之上。这些系统共同之处在于,它们意识到当许多人同时访问数据时,数据会以不同的方式起作用。当涉及到优先考虑一致性可用性分区容错的哪些方面时,它们的方法会有所不同。

设计分布式系统很棘手。随着时间的推移,您将面临许多陷阱和“意外”,而这些陷阱和“意外”并不总是显而易见的。我们没有所有的解决方案,而 CouchDB 也不是万能药,但当您顺应 CouchDB 的特性而不是违背它时,阻力最小的路径会将您引向自然可扩展的应用程序。

当然,构建分布式系统只是开始。一个网站,其数据库只有一半时间可用,几乎毫无价值。不幸的是,传统关系数据库对一致性的处理方式使得应用程序程序员很容易依赖全局状态、全局时钟和其他高可用性禁忌,甚至没有意识到他们正在这样做。在研究 CouchDB 如何促进可扩展性之前,我们将研究分布式系统面临的约束。在我们了解到当应用程序的各个部分无法依赖于彼此保持持续联系时出现的问题之后,我们将看到 CouchDB 提供了一种直观且有用的方式,围绕高可用性对应用程序进行建模。

1.3.2. CAP 定理

CAP 定理描述了几种不同的策略,用于在网络上分发应用程序逻辑。CouchDB 的解决方案使用复制在参与节点之间传播应用程序更改。这与在一致性、可用性和分区容错的不同交叉点上运行的共识算法和关系数据库有着根本不同的方法。

CAP 定理,如图 图 1. CAP 定理 所示,确定了三个不同的关注点

  • 一致性:即使在并发更新的情况下,所有数据库客户端也会看到相同的数据。

  • 可用性:所有数据库客户端都能够访问某些版本的数据。

  • 分区容错:数据库可以在多台服务器上拆分。

选择两个。

The CAP theorem

图 1. CAP 定理

当一个系统增长到足够大以至于单个数据库节点无法处理其上的负载时,一个明智的解决方案是添加更多服务器。当我们添加节点时,我们必须开始考虑如何在它们之间分区数据。我们是否有一些数据库完全共享相同的数据?我们将不同的数据集放在不同的数据库服务器上吗?我们只允许某些数据库服务器写入数据,而让其他服务器处理读取吗?

无论我们采取哪种方法,我们都会不断遇到的一个问题是使所有这些数据库服务器保持同步。如果您将一些信息写入一个节点,您将如何确保对另一个数据库服务器的读取请求反映此最新信息?这些事件可能相隔几毫秒。即使数据库服务器的数量不多,这个问题也可能变得极其复杂。

当所有客户端绝对必须看到数据库的一致视图时,一个节点的用户必须等待任何其他节点达成一致,然后才能读取或写入数据库。在这种情况下,我们看到可用性屈居于一致性之后。但是,在某些情况下,可用性胜过一致性

系统中的每个节点都应该能够完全基于本地状态做出决策。如果您需要在发生故障的高负载下执行某些操作,并且您需要达成一致,那么您就迷失了方向。如果您担心可扩展性,那么任何强迫您运行一致性的算法最终都会成为您的瓶颈。将此视为理所当然。

——亚马逊首席技术官兼副总裁维尔纳·沃格斯

如果可用性是优先考虑的事项,我们可以让客户端将数据写入数据库的一个节点,而无需等待其他节点达成一致。如果数据库知道如何处理节点之间的这些操作协调,我们就可以用高可用性换取一种“最终一致性”。对于许多应用程序来说,这是一个令人惊讶的适用权衡。

与传统关系数据库不同,传统关系数据库中执行的每个操作必然会受到数据库范围一致性检查的约束,CouchDB 使得构建应用程序变得非常简单,这些应用程序牺牲了即时一致性,以换取简单分布带来的巨大性能提升。

1.3.3. 本地一致性

在我们尝试了解 CouchDB 在集群中如何运行之前,重要的是我们了解单个 CouchDB 节点的内部工作原理。CouchDB API 旨在提供一个方便但精简的数据库核心包装器。通过仔细查看数据库核心的结构,我们将更好地了解围绕它的 API。

1.3.3.1. 数据的关键

CouchDB 的核心是一个功能强大的B 树存储引擎。B 树是一种已排序的数据结构,允许以对数时间进行搜索、插入和删除。正如图 2. 视图请求的剖析所示,CouchDB 对所有内部数据、文档和视图使用此 B 树存储引擎。如果我们理解其中一个,我们将理解所有这些。

Anatomy of a view request

图 2. 视图请求的剖析

CouchDB 使用 MapReduce 来计算视图的结果。MapReduce 使用两个函数,“map”和“reduce”,它们分别应用于每个孤立的文档。能够隔离这些操作意味着视图计算适用于并行和增量计算。更重要的是,由于这些函数生成键/值对,因此 CouchDB 能够将它们按键排序后插入到 B 树存储引擎中。按键或键范围查找是 B 树中非常高效的操作,在大 O表示法中分别表示为O(log N)O(log N + K)

在 CouchDB 中,我们通过键或键范围访问文档和视图结果。这是对 CouchDB 的 B 树存储引擎上执行的基础操作的直接映射。除了文档插入和更新之外,这种直接映射是我们描述 CouchDB 的 API 是数据库核心的一个精简包装器的原因。

仅通过键访问结果非常重要,因为它使我们能够获得巨大的性能提升。除了大幅提高速度之外,我们还可以将数据分区到多个节点,而不会影响我们孤立查询每个节点的能力。BigTableHadoopSimpleDBmemcached正是出于这些原因才限制按键查找对象。

1.3.3.2. 无锁定

关系数据库中的表是单一的数据结构。如果您想要修改表(例如,更新行),数据库系统必须确保没有其他人尝试更新该行,并且在更新该行时没有人可以读取该行。处理此问题的常见方法是使用所谓的锁。如果多个客户端想要访问表,第一个客户端将获取锁,让其他所有人等待。当第一个客户端的请求得到处理时,下一个客户端将获得访问权限,而其他所有人都在等待,依此类推。即使请求是并行到达的,这种串行执行请求也会浪费大量的服务器处理能力。在高负载下,关系数据库可能花费更多时间来弄清楚谁被允许做什么以及按什么顺序执行,而不是执行任何实际工作。

注意

现代关系数据库通过在底层实现 MVCC 来避免锁,但对最终用户隐藏了这一点,要求他们协调单行或字段的并发更改。

CouchDB 不使用锁,而是使用多版本并发控制(MVCC)来管理对数据库的并发访问。图 3. MVCC 意味着无锁定说明了 MVCC 和传统锁定机制之间的差异。MVCC 意味着 CouchDB 可以始终全速运行,即使在高负载下也是如此。请求并行运行,充分利用服务器提供的每一滴处理能力。

MVCC means no locking

图 3. MVCC 意味着无锁定

CouchDB 中的文档已进行版本控制,就像在常规版本控制系统(例如Subversion)中一样。如果您想要更改文档中的值,则可以创建该文档的全新版本并将其保存在旧版本之上。执行此操作后,您将获得同一文档的两个版本,一个旧版本和一个新版本。

这如何比锁提供更好的改进?考虑一组想要访问文档的请求。第一个请求读取文档。在处理此请求时,第二个请求更改了文档。由于第二个请求包含文档的全新版本,因此 CouchDB 可以简单地将其附加到数据库,而无需等待读取请求完成。

当第三个请求想要读取同一文档时,CouchDB 将将其指向刚刚写入的新版本。在此整个过程中,第一个请求仍然可以读取原始版本。

读取请求将始终看到在请求开始时数据库的最新快照。

1.3.4. 验证

作为应用程序开发人员,我们必须考虑应该接受什么样的输入,以及应该拒绝什么样的输入。在传统关系数据库中对复杂数据执行此类验证的表达能力还有很多不足之处。幸运的是,CouchDB 提供了一种强大的方式,可以在数据库中执行逐文档验证。

CouchDB 可以使用类似于 MapReduce 所用 JavaScript 函数验证文档。每次尝试修改文档时,CouchDB 都会将验证函数传递给现有文档的副本、新文档的副本以及其他信息的集合,例如用户身份验证详细信息。验证函数现在有机会批准或拒绝更新。

通过与 grain 合作并让 CouchDB 为我们完成此操作,我们可以节省大量的 CPU 周期,否则这些周期将用于从 SQL 中序列化对象图、将它们转换为域对象,并使用这些对象执行应用程序级验证。

1.3.5. 分布式一致性

对于大多数数据库而言,在单个数据库节点内保持一致性相对容易。当您尝试在多个数据库服务器之间保持一致性时,真正的难题才开始显现。如果客户端在服务器 A 上执行写操作,我们如何确保它与服务器 BCD 保持一致?对于关系数据库而言,这是一个非常复杂的问题,有整本书专门讨论其解决方案。您可以使用多主、单主、分区、分片、直写缓存以及各种其他复杂技术。

1.3.6. 增量复制

CouchDB 的操作在单个文档的上下文中进行。由于 CouchDB 通过使用增量复制在多个数据库之间实现最终一致性,因此您不必再担心数据库服务器是否能够保持持续通信。增量复制是一个在服务器之间定期复制文档更改的过程。我们能够构建一个称为无共享的数据库集群,其中每个节点都是独立且自给自足的,从而在整个系统中没有单一的争用点。

需要扩展 CouchDB 数据库集群吗?只需添加另一台服务器即可。

图 4. CouchDB 节点之间的增量复制 所示,通过 CouchDB 的增量复制,您可以根据需要在任意两个数据库之间同步数据。复制后,每个数据库都可以独立工作。

您可以使用此功能使用作业调度程序(例如 cron)在集群内或数据中心之间同步数据库服务器,也可以在旅行时将其用于与您的笔记本电脑同步数据以进行离线工作。每个数据库都可以按照通常的方式使用,并且数据库之间的更改可以在以后双向同步。

Incremental replication between CouchDB nodes

图 4. CouchDB 节点之间的增量复制

当您在两个不同的数据库中更改同一文档并希望相互同步这些文档时会发生什么?CouchDB 的复制系统带有自动冲突检测和解决功能。当 CouchDB 检测到文档在两个数据库中都已更改时,它会将此文档标记为冲突,就像它们在常规版本控制系统中一样。

这不像它听起来那么麻烦。当两个版本的文档在复制过程中发生冲突时,获胜版本将作为文档历史记录中的最新版本保存。与你可能期望的不同,CouchDB 不会丢弃失败版本,而是将其作为文档历史记录中的前一版本保存,以便你可以在需要时访问它。此操作自动且一致地发生,因此两个数据库都将做出完全相同的选择。

由你来以对你的应用程序有意义的方式处理冲突。你可以保留所选文档版本,恢复到较旧版本,或尝试合并两个版本并保存结果。

1.3.7. 案例研究

我的朋友兼同事 Greg Borenstein 构建了一个小型库,用于将 Songbird 播放列表转换为 JSON 对象,并决定将它们存储在 CouchDB 中作为备份应用程序的一部分。已完成的软件使用 CouchDB 的 MVCC 和文档修订版来确保 Songbird 播放列表在节点之间得到稳健备份。

注意

Songbird 是一款免费软件媒体播放器,具有集成的网络浏览器,基于 Mozilla XULRunner 平台。Songbird 适用于 Microsoft Windows、Apple Mac OS X、Solaris 和 Linux。

让我们检查 Songbird 备份应用程序的工作流,首先作为从一台计算机进行备份的用户,然后使用 Songbird 在多台计算机之间同步播放列表。我们将看到文档修订版如何将原本可能是一个棘手的问题变成一个正常工作的问题。

我们第一次使用此备份应用程序时,我们将播放列表提供给应用程序并启动备份。每个播放列表都转换为 JSON 对象并传递给 CouchDB 数据库。如 图 5. 备份到单个数据库 中所示,CouchDB 在将文档 ID 和每个播放列表的修订版保存到数据库时将其返回。

Backing up to a single database

图 5. 备份到单个数据库

几天后,我们发现我们的播放列表已更新,并且我们希望备份我们的更改。在我们向备份应用程序提供我们的播放列表之后,它从 CouchDB 中获取最新版本,以及相应的文档修订版。当应用程序返回新的播放列表文档时,CouchDB 要求在请求中包含文档修订版。

然后,CouchDB 确保在请求中传递给它的文档修订版与数据库中保存的当前修订版匹配。由于 CouchDB 在每次修改时都会更新修订版,因此如果这两个修订版不同步,则表明在我们在数据库中请求它和我们发送更新之间,其他人对文档进行了更改。在其他人修改文档后对其进行更改而不首先检查这些更改通常是一个坏主意。

强制客户端返回正确的文档修订版是 CouchDB 乐观并发性的核心。

我们有一台笔记本电脑,我们希望使其与我们的台式计算机保持同步。由于所有播放列表都在我们的台式机上,因此第一步是在我们的笔记本电脑上“从备份中恢复”。这是我们第一次这样做,因此之后我们的笔记本电脑应该保存我们台式机播放列表集合的确切副本。

在我们的笔记本电脑上编辑阿根廷探戈播放列表以添加我们购买的一些新歌曲后,我们希望保存我们的更改。备份应用程序将替换我们笔记本电脑 CouchDB 数据库中的播放列表文档,并生成一个新的文档版本。几天后,我们记起了我们的新歌曲,并希望将播放列表复制到我们的台式电脑上。如图 图 6. 在两个数据库之间同步 所示,备份应用程序将新文档和新版本复制到台式电脑 CouchDB 数据库。现在两个 CouchDB 数据库都有相同的文档版本。

Synchronizing between two databases

图 6. 在两个数据库之间同步

由于 CouchDB 跟踪文档版本,因此它确保此类更新仅在基于当前信息时才会起作用。如果我们在同步之间对播放列表备份进行了修改,事情就不会那么顺利。

我们在笔记本电脑上备份了一些更改,并忘记同步。几天后,我们在台式电脑上编辑播放列表,进行备份,并希望将其同步到我们的笔记本电脑。如图 图 7. 两个数据库之间的同步冲突 所示,当我们的备份应用程序尝试在两个数据库之间复制时,CouchDB 会看到从我们的台式电脑发送的更改是对过时文档的修改,并善意地通知我们发生了冲突。

从应用程序的角度来看,很容易从这个错误中恢复。只需下载 CouchDB 版本的播放列表,并提供合并更改或将本地修改保存到新播放列表的机会即可。

Synchronization conflicts between two databases

图 7. 两个数据库之间的同步冲突

1.3.8. 总结

CouchDB 的设计大量借鉴了 Web 架构以及在该架构上部署大规模分布式系统的经验教训。通过理解此架构为何以这种方式工作,以及学会发现应用程序的哪些部分可以轻松分布,哪些部分不能分布,你将增强设计分布式和可扩展应用程序的能力,无论是否使用 CouchDB。

我们已经介绍了围绕 CouchDB 一致性模型的主要问题,并暗示了当你与 CouchDB 合作而不是反对它时会获得的一些好处。但理论已经足够了——让我们开始运行,看看这到底是怎么回事!