1.3 终一致性
在上一个文档“为什么选择CouchDB?”中,我们看到CouchDB的灵活性使我们能够随着应用程序的增长和变化而发展数据。在本主题中,我们将探讨CouchDB的“细化”工作如何提高应用程序的简单性,并帮助我们自然地构建可扩展的分布式系统。
1.3.1 与Grain合作
分布式系统是可以在广泛的网络上稳定运行的系统。网络计算的一个特殊功能是网络链接可能会消失,并且有许多策略可以管理这种类型的网络分段。CouchDB与其他数据库的不同之处在于,它接受终的一致性,而不是像RDBMS或Paxos这样在原始可用性之前放置一致性。这些系统的共同点是认识到,当许多人同时访问数据时,数据的行为会有所不同。在优先考虑一致性,可用性或分区容忍的哪些方面时,他们的方法有所不同。
工程分布式系统是棘手的。随着时间的推移,您将要面对的许多警告和“陷阱”并不是立即显而易见的。我们还没有所有解决方案,而且CouchDB并非药,但是当您使用CouchDB的精髓而不是反对时,阻力小的途径将使您自然地扩展应用程序。
当然,构建分布式系统仅仅是开始。一个仅拥有一半时间可访问数据库的网站几乎一文不值。不幸的是,传统的关系数据库一致性方法使应用程序程序员很容易依赖全局状态,全局时钟和其他高可用性,甚至没有意识到自己正在这样做。在研究CouchDB如何提高可伸缩性之前,我们将研究分布式系统面临的约束。当我们看到了当您的应用程序的各个部分无法相互依赖时会出现的问题之后,我们将看到CouchDB提供了一种直观且有用的方式来围绕高可用性对应用程序进行建模。
1.3.2 CAP定理
CAP定理描述了用于在网络之间分布应用程序逻辑的几种不同策略。CouchDB的解决方案使用复制在参与的节点之间传播应用程序更改。这是与共识算法和关系数据库根本不同的方法,共识算法和关系数据库在一致性,可用性和分区容忍度的不同交集处运行。
CAP定理,如图1所示。CAP定理确定了三个不同的问题:
一致性:即使并发更新,所有数据库客户端也可以看到相同的数据。
可用性:所有数据库客户端都可以访问某些版本的数据。
分区容限:数据库可以拆分到多个服务器上。
选择两个。
当系统增长到足以使单个数据库节点无法处理施加在其上的负载时,明智的解决方案是添加更多服务器。添加节点时,我们必须开始考虑如何在它们之间分区数据。我们有几个共享完全相同数据的数据库吗?我们是否将不同的数据集放在不同的数据库服务器上?我们是否只允许某些数据库服务器写入数据,而让其他服务器处理读取?
无论采用哪种方法,我们都会遇到的一个问题是使所有这些数据库服务器保持同步。如果您将某些信息写入一个节点,那么如何确保对另一台数据库服务器的读取请求反映了此新信息?这些事件可能相隔毫秒。即使只有少量的数据库服务器,此问题也会变得非常复杂。
当至关重要的是,所有客户端都必须看到一致的数据库视图时,一个节点的用户将必须等待其他任何节点达成协议,才能读取或写入数据库。在这种情况下,我们看到可用性在一致性方面倒退了。但是,在某些情况下,可用性比一致性要好:
系统中的每个节点都应该能够纯粹基于本地状态做出决策。如果您需要在高负载下做某事且发生故障并且需要达成协议,那么您会迷失方向。如果您担心可扩展性,那么任何迫使您达成协议的算法终都会成为瓶颈。以此为前提。
—亚马逊首席技术官兼副总裁沃纳·沃格斯(Werner Vogels)
如果优先考虑可用性,我们可以让客户端将数据写入数据库的一个节点,而无需等待其他节点达成协议。如果数据库知道如何照顾节点之间的这些操作,那么我们将获得某种“终一致性”,以换取高可用性。对于许多应用来说,这是一个令人惊讶的适用折衷。
与传统的关系数据库不同,传统的关系数据库必须对每个执行的操作进行数据库范围的一致性检查,而CouchDB使得构建应用程序变得非常简单,而这些应用程序却牺牲了即时一致性,以简化简单分发带来的巨大性能提升。
1.3.3 本地一致性
在尝试了解CouchDB如何在群集中运行之前,重要的是我们了解单个CouchDB节点的内部工作原理。CouchDB API旨在提供围绕数据库核心的便捷但精简的包装。通过仔细研究数据库核心的结构,我们将更好地了解围绕它的API。
1.3.3.1 数据的Key
CouchDB的核心是功能强大的B树存储引擎。B树是一种排序的数据结构,允许以对数时间进行搜索,插入和删除。如图2所示。对视图请求的剖析表明,CouchDB使用此B树存储引擎存储所有内部数据,文档和视图。如果我们理解一个,我们将全部理解。
CouchDB使用MapReduce来计算视图的结果。MapReduce利用了两个函数,即“ map”和“ reduce”,它们分别应用于每个文档。能够隔离这些操作意味着视图计算可以进行并行和增量计算。更重要的是,由于这些函数产生键/值对,因此CouchDB能够将它们按键排序插入B树存储引擎。通过键或键范围进行的查找是使用B树的极其有效的操作,用大O表示法分别表示为O(log N)和O(log N + K)。
在CouchDB中,我们按键或键范围访问文档并查看结果。这是对CouchDB的B树存储引擎上执行的基础操作的直接映射。与文档插入和更新一起,这种直接映射是我们将CouchDB的API描述为围绕数据库核心的薄包装的原因。
只能通过键访问结果是一个非常重要的限制,因为它使我们获得了巨大的性能提升。除了大幅提高速度外,我们还可以在多个节点上划分数据,而不会影响我们独立查询每个节点的能力。正是由于这些原因,BigTable,Hadoop,SimpleDB和memcached通过键限制了对象查找。
1.3.3.2 无锁
关系数据库中的表是单个数据结构。如果要修改表(例如,更新行),数据库系统必须确保没有其他人试图更新该行,并且在更新该行时没有人可以从该行中读取数据。解决此问题的常用方法是使用锁。如果多个客户端要访问一个表,则个客户端将获得锁,从而使其他所有人都在等待。当个客户的请求得到处理时,下一个客户将获得访问权限,而其他人都将等待,依此类推。即使是并行到达请求,这种串行执行请求也会浪费大量服务器的处理能力。在高负载下,关系数据库比进行任何实际工作要花费更多的时间来确定允许谁执行什么工作以及按照什么顺序执行。
注意
现代的关系数据库通过在幕后实施MVCC来避免锁定,但对终用户隐藏了MVCC,要求它们协调单个行或字段的并发更改。
CouchDB使用多版本并发控制(MVCC)代替锁,来管理对数据库的并发访问。图3. MVCC表示没有锁定说明了MVCC和传统锁定机制之间的差异。MVCC意味着CouchDB即使在高负载下也可以一直全速运行。请求是并行运行的,从而充分利用了服务器必须提供的每后一滴处理能力。
图3. MVCC意味着没有锁定
CouchDB中的文档已经过版本控制,就像在常规版本控制系统(例如Subversion)中一样。如果要更改文档中的值,请创建该文档的全新版本并将其保存在旧版本上。完成此操作后,您将获得同一文档的两个版本,一个旧版本,一个新版本。
这如何提供对锁的改进?考虑一组想要访问文档的请求。个请求读取文档。在处理过程中,第二个请求更改了文档。由于第二个请求包含文档的全新版本,因此CouchDB可以简单地将其附加到数据库,而不必等待读取请求完成。
当第三个请求要读取相同的文档时,CouchDB将其指向刚刚编写的新版本。在整个过程中,个请求可能仍在读取原始版本。
读取请求在请求开始时始终会看到您数据库的新快照。
1.3.4 验证方式
作为应用程序开发人员,我们必须考虑应该接受什么样的输入以及应该拒绝什么输入。在传统的关系数据库中对复杂数据进行这种类型的验证的表达能力尚有许多不足之处。幸运的是,CouchDB提供了一种从数据库内部执行按文档验证的强大方法。
CouchDB可以使用类似于MapReduce的JavaScript函数来验证文档。每次您尝试修改文档时,CouchDB都会通过验证功能以传递现有文档的副本,新文档的副本以及其他信息的集合,例如用户身份验证详细信息。验证功能现在可以批准或拒绝更新。
通过使用Grain并让CouchDB为我们做到这一点,我们为自己节省了大量的CPU周期,否则这些CPU周期将被用于从SQL序列化对象图,将它们转换为域对象并使用这些对象进行应用程序级验证。
1.3.5 分布式一致性
对于大多数数据库而言,在单个数据库节点内维护一致性相对容易。当您尝试维护多个数据库服务器之间的一致性时,真正的问题开始浮出水面。如果客户端在服务器A上执行写操作,我们如何确保它与服务器B或C或D一致?对于关系数据库而言,这是一个非常复杂的问题,整本书都专门针对其解决方案。您可以使用多主机,单主机,分区,分片,直写式高速缓存以及各种其他复杂技术。
1.3.6 增量复制
CouchDB的操作在单个文档的上下文中进行。由于CouchDB通过使用增量复制实现了多个数据库之间终的一致性,因此您不必担心数据库服务器能够保持持续的通信。增量复制是在服务器之间定期复制文档更改的过程。我们能够构建所谓的无共享数据库集群,其中每个节点都是独立且自给自足的,在整个系统中不存在任何争用点。
需要扩展您的CouchDB数据库集群吗?只需投入另一台服务器即可。
如图4所示。在CouchDB节点之间进行增量复制,并使用CouchDB进行增量复制,您可以在任意两个数据库之间随时随地同步数据。复制后,每个数据库都可以独立工作。
您可以使用此功能通过cron之类的作业调度程序在群集内或数据中心之间同步数据库服务器,也可以使用它在便携式计算机上同步数据与笔记本电脑以进行离线工作。可以按常规方式使用每个数据库,并且以后可以在两个方向上同步数据库之间的更改。
当您在两个不同的数据库中更改同一文档并希望彼此同步时会发生什么?CouchDB的复制系统带有自动冲突检测和解决方案。当CouchDB在两个数据库中都检测到文档已被更改时,它将标记该文档为冲突文档,就像它们在常规版本控制系统中一样。
这并不像次听起来那样麻烦。如果在复制过程中两个版本的文档发生冲突,则胜出版本将另存为文档历史记录中的新版本。CouchDB不会像您期望的那样丢掉丢失的版本,而是将其保存为文档历史记录中的先前版本,以便您可以在需要时访问它。这是自动且一致地发生的,因此两个数据库都将做出完全相同的选择。
由您决定以对您的应用程序有意义的方式来处理冲突。您可以将选定的文档版本保留在原位,还原为较旧的版本,或尝试合并两个版本并保存结果。
1.3.7 案例分析
朋友和同事Greg Borenstein建立了一个小型库,用于将Songbird播放列表转换为JSON对象,并决定将它们存储在CouchDB中作为备份应用程序的一部分。完整的软件使用CouchDB的MVCC和文档修订版,以确保在节点之间可靠地备份Songbird播放列表。
注意
Songbird是基于Mozilla XULRunner平台的具有集成Web浏览器的免费软件媒体播放器。Songbird适用于Microsoft Windows,Apple Mac OS X,Solaris和Linux。
让我们检查Songbird备份应用程序的工作流程,首先是作为用户从单台计算机备份,然后使用Songbird在多台计算机之间同步播放列表。我们将看到文档修订如何将本来很棘手的问题变成可以解决的问题。
次使用此备份应用程序时,我们会将播放列表反馈入该应用程序并启动备份。每个播放列表都将转换为JSON对象,并传递到CouchDB数据库。如图5所示。备份到单个数据库时,CouchDB会将每个播放列表的文档ID和修订版本保存到数据库中。
几天后,我们发现我们的播放列表已更新,我们希望备份所做的更改。将播放列表反馈入备份应用程序后,它会从CouchDB获取新版本以及相应的文档修订版。当应用程序移交新的播放列表文档时,CouchDB要求文档修订包含在请求中。
然后,CouchDB确保请求中传递给它的文档修订与数据库中保存的当前修订匹配。因为CouchDB每次修改都会更新修订,所以如果这两个修改不同步,则表明在我们从数据库请求文档到发送更新之间,有人对文档进行了更改。在其他人没有先检查那些更改的情况下对其进行更改通常是一个坏主意。
强迫客户交出正确的文档修订版是CouchDB乐观并发的核心。
我们有一台笔记本电脑,希望与台式机保持同步。在台式机上播放所有播放列表后,步是“从备份还原”到笔记本电脑上。这是我们次这样做,因此之后我们的笔记本电脑应保留桌面播放列表集合的副本。
在笔记本电脑上编辑我们的阿根廷探戈播放列表以添加一些我们购买的新歌曲后,我们要保存更改。备份应用程序替换了我们笔记本电脑CouchDB数据库中的播放列表文档,并生成了新的文档修订版。几天后,我们记住了我们的新歌曲,并希望将播放列表复制到我们的台式计算机上。如图6所示,备份应用程序在两个数据库之间进行同步,将新文档和新修订版本复制到桌面CouchDB数据库中。现在,两个CouchDB数据库都具有相同的文档修订版。
因为CouchDB跟踪文档修订,所以它确保仅当这些更新基于当前信息时这些更新才有效。如果我们在同步之间对播放列表备份进行了修改,那么事情就不会那么顺利。
我们在笔记本电脑上备份了一些更改,却忘记了同步。几天后,我们正在台式计算机上编辑播放列表,进行备份,并希望将其同步到笔记本电脑。如图7所示。两个数据库之间的同步冲突,当我们的备份应用程序尝试在两个数据库之间复制时,CouchDB看到从台式机发送的更改是对过时文档的修改,并有帮助地通知我们 一直是一个冲突。
从应用程序的角度来看,从此错误中恢复很容易完成。只需下载CouchDB的播放列表版本,即可提供合并更改或将本地修改保存到新播放列表中的机会。
1.3.8 总结
CouchDB的设计大量借鉴了Web架构,并汲取了在该架构上部署大规模分布式系统的经验教训。 通过了解这种体系结构为何能以这种方式工作,并通过学习发现可以轻松分发应用程序的哪些部分而不能轻松分发哪些部分,可以增强使用CouchDB或不使用CouchDB来设计分布式和可伸缩应用程序的能力。
我们已经介绍了有关CouchDB一致性模型的主要问题,并暗示了在使用CouchDB而不是反对使用CouchDB时将获得的一些好处。但是有足够的理论–让我们开始并运行,看看大惊小怪的是什么!