NoSQL数据库介绍(4)

时间:2022-03-02 08:56:24

4 键/值存储


     讨论了常用的概念、技术和模式后,第一类NoSQL数据存储会在本章进行研究。键/值存储通常有一个简单的数据模型:一个map/dictionary,允许客户按键来存放和请求数值。除了数据模型和API,现代键/值存储倾向于高扩展性而非一致性,因此它们中的大多数也省略了富ad-hoc查询和分析功能(尤其是联接和聚合操作被取消)。通常,可存储的键的长度被限制为一定的字节数,而在值上的限制较少([ Ipp09 ],[ Nor09 ])。      键/值存储已经存在了很长一段时间(如BerkeleyDB [ Ora10d ]),但过去几年中出现的大量的这类NoSQL存储已经受到了Amazon Dynamo的巨大影响,其将在本章进行彻底研究。在免费和开源键/值存储的巨大多样性中,Project Voldemort将被进一步检视。本章最后,一些其他著名的键/值存储将简要地评判。
4.1 Amazon的Dynamo      Amazon Dynamo是Amazon为不同目的使用的几种数据库之一(其他如SimpleDB或S3,简单存储服务,参见[ Ama10b ]、[ Ama10a ])。因为它对一系列NoSQL数据库尤其是键/值存储的影响,下面的章节将更详细研究Dynamo的影响因素、应用的概念、系统的设计与实现。
4.1.1 Amazon的背景和需求      这些存储服务的技术背景可概述如下(根据DeCandia等人的Amazon Dynamo论文,参见[ DHJ+07,205页 ]):
  •      基础设施是由位于世界各地的许多数据中心的数万台服务器和网络组件组成的。
  •      使用商用硬件。
  •      组件故障是“操作的标准模式”。
  •      “亚马逊采用由数百个服务组成的高度去中心化的,松散耦合的,面向服务的架构。”

     除了这些技术因素,Dynamo的设计也受到商业考量的影响([ DHJ+07,205页 ]):
  •      考虑“性能、可靠性和效率” 的严格的内部服务水平协议(SLAs)必须达到分布的99.9分位值。DeCandia等人认为由Dynamo和其他数据库提供的“状态管理”对满足SLAs的要求是决定性的,服务的业务逻辑是在大多数情况下是轻量级的([ DHJ+07,页207–208 ])。
  •      在Amazon最重要的要求之一是可靠性,“因为即使是最轻微的中断也具有显著的财务后果且影响客户的信任”。
  •      “要支持持续的增长,该平台需要具有高度的可扩展性”。
     在Amazon,Dynamo用于管理服务的状态,具有非常高的可靠性要求,且需要严格控制可用性、一致性、成本效益和性能之间的权衡。DeCandia等人进一步认为大量的服务只需要通过主键访问(如“最佳卖家列表,购物车,客户偏好,会话管理,销售等级,产品目录”),因而一个普通关系数据库的使用“会导致效率低下且限制规模和可用性”([ DHJ+07,205页])。
4.1.2 Dynamo中应用的概念      第3章中提出的概念以外,Dynamo使用带副本的一致性哈希作为一个分区模式。存储在多个节点上的分区中的对象被打上版本(多版本存储)。为在更新过程中保持一致性,Dynamo为去中心化的副本同步使用一种类似仲裁的技术和一个(未进一步指定的)协议。为管理成员并检测机器故障,它采用了一个基于Gossip的协议,允许以“最小的手动管理需要”来添加和删除服务器([ DHJ + 07,页205–206 ])。      DeCandia等人指出,他们对社区研究的贡献是Dynamo作为一个“最终一致性的存储系统可用于生产环境中要求苛刻的应用程序”。
4.1.3 系统设计
考虑和概览
     DeCandia等人在Amazon内反对RDBMS,因为大多数服务“只通过主键存储和检索数据,不需要RDBMS提供的复杂的查询和管理功能”。此外他们认为RDBMS的“可用的副本技术”是“有限的且典型地选择了一致性高于可用性”。他们承认扩展和分区RDBMS已经取得了进步,但也表示这种设置仍然难以配置和操作([ DHJ+07,页206 ])。       因此,他们决定构建Dynamo使用一个简单的以BLOB存储值的键/值界面。一次操作仅限于一个键/值对,所以更新操作仅限于单键,不允许交叉引用到其他的键/值对或操作跨越多个键/值对。此外,Dynamo不支持分层命名空间(如目录服务或文件系统)([ DHJ+07,页206,209 ])。      Dynamo被实现为一个带副本的分区的系统,并定义了一致性窗口。因此,Dynamo“目标为以弱一致性和高可用性操作的应用程序”。它不提供任何隔离保证([ DHJ+07,206页])。DeCandia等人认为,给定Amazon的背景和需求(特别是高可用性和可扩展性),一个同步的复制模式是不可实现的。作为替代,他们选择了一种乐观的复制模式。这种特点将复制和写操作的可能性置于后台,即使在断开的副本出现时(由于网络或机器故障)。所以,Dynamo被设计为“总是可写的(即一个高度可写的数据存储)”,必须在读时解决冲突。此外,DeCandia等人认为一个数据库只能执行简单的冲突解决策略,因此一个客户端应用程序更适合这项任务,因为它“知道数据模式”且“可以决定一种最适合客户体验的冲突解决方法”。如果应用开发人员不想实现这样的业务逻辑特化的协调策略,Dynamo还提供了简单的可直接使用的策略,如“最后的写胜利”,一个基于时间戳的协调(参见[ DHJ+07,页207-208和214)。      为提供简单的可扩展性,Dynamo实现了“一个简单的扩展模式,以解决数据集或请求速率的增长”,以允许“一次增加一个存储主机”([ DHJ+07,页206和208)。      在Dynamo,所有节点都有相同的责任;没有有特殊作用的特殊节点。此外,它的设计倾向于于“去中心化的点对点技术,而非集中控制”,因为后者在过去的Amazon曾“导致中断”。加入系统的存储主机可能具有异构的硬件,Dynamo必须考虑按“个别服务器的能力”的比例来分配工作([ DHJ+07,页208])。      由于Dynamo运行在Amazon自己的管理域,环境和所有节点被认为是非敌对的,因此Dynamo没有实现安全相关的功能如授权和认证([ DHJ+07,页206,209 ])。      表4.1总结了Dynamo在设计中所面临的困难与解决困难应用的技术,以及相应的优点。
问题 技术 优点
Partitioning Consistent Hashing Incremental Scalability
High Availability forwrites Vector clocks with reconciliationduring reads Version size is decoupled from updaterates.
Handling temporary failures Sloppy Quorum andhinted handoff Provides high availability and durabilityguarantee when some of thereplicas are not available.
Recovering from permanentfailures Anti-entropy usingMerkle trees Synchronizes divergent replicas inthe background.
Membership and failuredetection Gossip-based membershipprotocol and failuredetection. Preserves symmetry and avoids havinga centralized registry for storingmembership and node liveness information
表4.1:Amazon的Dynamo——技术总结(来自[ DHJ+07,页209 ])

系统界面
     Dynamo提供给客户端应用程序的界面仅包含两种操作:
  •      get(key),返回一个对象的列表和一个context
  •      put(key, context, object),没有返回值
     如果存储在给定键下的对象存在版本冲突,get操作可能返回多个对象。它还返回一个上下文(context),其中存储了如对象版本之类的系统元数据,在put操作中,客户端除key和object外还必须提供这个context对象作为参数。      键和对象值不回被Dynamo解释,而是作为“一个不透明的字节数组”被处理。键以MD5算法来哈希,以确定存负责这个键/值对的存储节点。
分区算法
     为了提供增量的可扩展性,Dynamo使用一致性哈希来动态分区数据,使其分布到在给定时间内存在于系统中的存储主机上。DeCandia等人认为以原始形式使用一致性哈希有两个缺点:首先,存储主机和数据项的环上随机映射导致数据和负载的不平衡分布;其次,一致性哈希算法相等地对待每个存储节点且不考虑其硬件资源。为克服这两个困难,Dynamo应用了虚拟节点的概念,即对每个存储主机将多个虚拟节点哈希到环上(如3.2.1描述),且每个物理节点的虚拟节点数目通常考虑存储节点的硬件能力(即每个物理节点资源越多则虚拟节点越多,[ DHJ+07,页209–210 ])。
副本
     为确保在一个机器崩溃是“操作的标准模式”的基础设施上的可用性和持久性,Dynamo使用节点之间的数据副本。每个数据项复制N次,N可以对每个Dynamo的“实例”进行配置(典型的N在Amazon是3,参见[ DHJ+07,214页])。负责存储键k的元组的存储节点也负责副本的版本更新,以顺时针方向对键k的元组的N-1个后继者。存在一个节点列表——称为偏好列表——为Dynamo中每一个要被存储的键k确定。这个列表包括多于N个节点,因为N个连续的虚拟节点可能映射到小于N个不同的物理节点(如3.2.1对一致性哈希的讨论)。 NoSQL数据库介绍(4)
图4.1:Amazon的Dynamo——带副本的一致性哈希(来自[ DHJ+07,209页])
数据分版本
     Dynamo被设计为一个最终一致的系统。这意味着更新操作在所有副本节点收到并应用更新前返回。随后的读操作因此可能从不同的副本节点返回不同的版本。在Amazon的平台副本之间的更新传播时间是有限的,如果没有错误发生;但在某些故障场景下,“更新可能在一个延长的时间段内无法到达所有副本”([ DHJ+07,210页])。      这种不一致性需要由应用程序考虑。作为一个例子,购物车应用程序从不拒绝添加到购物车操作。即使当明显地副本没有反映最新版本购物车时(由一个带有更新请求的向量时钟所指示,见下面),它将添加操作应用于本地购物车。      作为更新操作的结果,Dynamo总是创建一个更新数据项的新的且不可改变的版本。在Amazon的生产系统,大多数这些版本线性地一个包含另一个,系统可以通过语义调和来确定最新的版本。然而,因为失败(如网络分区)和并发更新,同一个数据项的多个、冲突的版本可能在系统中同时存在。因为数据存储无法调和这些并行版本,只有客户端应用程序包含有关其数据结构和语义的知识,可以解决版本冲突并从多个相互冲突的版本中调和有效版本(语义调和)。因此,使用Dynamo的客户端应用程序必须注意这个,必须“显式地确认同一数据的多个版本的可能性(为了不失去任何更新)”([ DHJ+07,210页])。      为确定冲突的版本,执行语义调和,并支持客户端应用程序来解决版本冲突,Dynamo采用向量时钟的概念(3.1.3节介绍)。如在Dynamo系统接口的章节中所述(见4.1.3),客户端在发布更新请求时需要提供一个上下文。这个上下文包括一个他们已经读过的数据的向量时钟,所以Dynamo知道更新哪一个版本,且可以正确地设置更新的数据项的向量时钟。如果并发版本的数据项出现,Dynamo将在读请求返回的上下文信息中把他们传输给客户端。在客户端向这样的数据项发出一个更新之前,它必须调和并发版本为一个有效版本。      图4.2说明了向量时钟的使用以及检测与调和并发版本。 NoSQL数据库介绍(4)
图4.2:Amazon的Dynamo——数据项上的并发更新(来自[ DHJ+07,211页])
     在这张图中,客户端首先创建一个数据项,更新请求由一个存储主机Sx处理,因此向量时钟([ SX,1 ])将与它关联。接下来,客户端更新这个数据项,这个更新请求导致版本2也由Sx节点执行。由此产生的向量时钟将是([ Sx,2 ]),于是一个[ SX,1 ]和[ Sx,2 ]间的临时排序可被确定,因为后者显然比前者晚(同存储主机、提升版本号)。因为[ Sx,1 ]只有一个后继([ Sx,2 ]),它不再是临时推理所需的,可以从向量时钟中删除。      接下来,一个客户端发出对版本D2的更新,其被存储主机Sy处理,产生向量时钟([ Sx,2 ],[ Sy,1 ])。在这里,向量时钟的元素[ Sx,2 ]不能省略。这是因为,例如一个客户端发出其已从某节点(称Sx)读到版本D2,而存储主机Sy尚未从它的伙伴节点收到这个版本。于是客户端想要更新一个比节点Sy当前已知的版本更近的版本,这会被接受由于Dynamo的“总是可写”属性。同样的事也发生在另一个客户端已读到D2且发布一个由存储主机Sz处理的更新,Sz当前不知道D2。这导致了版本D4与向量时钟([ Sx,2 ],[Sz,1 ])。      在下一个读请求中,D3和D4两者被传输到客户端,伴随着其向量时钟的汇总——典型的:([ Sx,2 ],[ Sy,1 ],[Sz,1 ])。客户端可以检测到版本D3和D4是冲突的,由于在读操作上下文中提交的向量时钟组合并未反映一个线性、先后的顺序。在客户端可发布另一个更新到系统前,它必须从并发版本D3和D4中调和出版本D5。如果这个更新请求再次被节点Sx处理,这会推动向量时钟中的版本号变为([ Sx,3 ],[ Sy,1 ],[Sz,1 ]),如图4.2所示。
get()和put()操作的执行
     Dynamo允许一个Dynamo实例的任何存储节点接收任何键的get和put请求。为了接触某个节点,客户端应用程序使用或者通用的负载均衡路由,或者一个客户端库,其反映Dynamo的分区模式且能通过计算确定需接触的存储主机。第一个节点选择方法的优势是,它可以保持不关注Dynamo的特定代码。第二个方法减少了延时,因为不需要接触负载均衡器,从而减少了一次网络跳数([ DHJ+07,211页)。      因为任何节点可以接收对环上任何键的请求,请求可能需要被在存储主机间转发。这是基于给定键的包含带优先顺序的需接触存储主机的偏好列表。当一个节点接收到客户端请求时,它将根据偏好列表中的顺序转发到存储主机上(如果节点本身在优先列表中,转发自然变得不必要)。一旦发生读或更新请求,最先N个健康的节点将被考虑(不可存取或宕机的节点只是跳过;参见[ DHJ+07,211页])。      为给客户端提供一个一致性视图,Dynamo应用了一种仲裁式一致性协议,其包含两个可配置的值:R和W分别代表需要参与成功的读或写操作的存储主机的最小数目。为建立一个仲裁式系统,这些值必须设置为R+W>N。DeCandia等人评论说,最慢的副本决定了操作的延时,因此R和W经常被选择使得R+W<N(以减少慢主机进入读写请求的概率;参见[ DHJ+07,211页])。在其他情况下,如建立Dynamo用于“为更重量级的后端存储中数据提供权威性持久缓存的功能”,R通常设置为1,W设置为N,以允许高性能的读操作。另一方面“低的W和R值可以增加不一致的风险,因为写请求会被认为是成功的并返回给客户端,即使他们没有被大部分副本处理过”。此外,持久性在一段时间内仍是脆弱的,当写请求完成但更新的版本只被传播到少数几个节点。DeCandia等人评论“Dynamo的主要优点是,它的客户端应用程序可以调整N、R和W以实现他们期望的性能、可用性和持久性水平”([ DHJ+07,214页])。(考虑性能、持久性、一致性和可用性,典型的适合亚马逊SLAs的配置是N=3,R=2,W = 2,参见[ DHJ+07,215页])。      对写请求(如Dynamo接口的put操作),偏好列表中的第一个回应节点(在Dynamo中称为协调器)为新的版本创建一个新的向量时钟并写到本地。这个过程之后,相同的节点将新版本复制其他负责被写入数据项的存储主机(优先列表中下N个存储主机)。写请求被认为是成功的,如果至少W-1个这些主机回应此更新([ DHJ+07,页211-212)。      一旦存储主机接收到一个读请求,它将向偏好列表上的前N个存储主机请求数据项并等待R−1个主机回应。如果这些节点用不同的版本回复则为冲突(通过向量时钟推理观察到),它将向请求的客户端返回并发版本([ DHJ+07,212页])。
成员
     Dynamo实现了一个增加和删除系统节点的显式机制。Dynamo未实现隐式成员检测,因为临时停机或颠簸的服务器会导致系统重新计算一部分一致性哈希环和相关的数据结构如Merkle树(见下文)。此外,Dynamo已经提供了手段来处理临时不可用的节点,将在下一小节中讨论。因此,Dynamo的管理员必须通过命令行或浏览器界面显式添加或删除节点。这会导致一个成员变更请求,到一个随机选择的节点,其会保存变更和时间戳(来保留一个成员历史记录),并通过一个Gossip基础的协议传播到系统的其他节点。每一秒钟,成员协议要求存储主机随机地联系一个节点以双向协调 “保存的成员变更历史”([ DHJ+07,212页])。这样做,成员的变化被扩散并建立一个最终一致的成员视图。      在上述过程中,在向一个Dynamo系统增加节点时会出现临时的逻辑分区。为了防止这种情况,某些节点可以成为特殊角色——种子,其可以被一种外部机制如静态配置或某种配置服务发现。于是种子被所有节点知道,从而在成员改变的Gossip过程中被联系,使得所有节点将最终协调其成员视图,且“逻辑分区是极不可能出现的”([ DHJ+07,213页])。      当一个节点加入系统时,必须确定被映射到一致性哈希环上的虚拟节点的数目。为了这个目的,为每个虚拟节点选择一个令牌,即一个决定一致性哈希环上虚拟节点位置的值。该组的令牌被持久化到物理节点的磁盘上,且通过与传播成员变化时相同的Gossip基础协议来传播,如前一段看到的(参见[ DHJ+07,页212 ])。每个物理节点的虚拟节点数被选择与物理节点的硬件资源成比例([ DHJ+07,210页])。      通过向系统中添加节点,一致性哈希环上键的所有权会发生变化。当某个节点通过成员扩展协议来知道一个最近添加的节点,并确定它不再负责某部分的键,它将这些键传输到添加的节点。当一个节点正在删除,键以一个相反的过程被重新分配([ DHJ+07,213页])。      考虑到一致性哈希环上节点的映射及其对负载分布、分区、平衡、数据布局、归档和引导的影响,DeCandia等人讨论自Dynamo推出以来实施的三种策略:      1、每节点T个随机令牌,并按令牌值分区(初期策略)      2、每节点T个随机令牌,等大小分区(临时策略,作为策略1至3的一个迁移路径)      3、每节点Q/S个随机令牌,等大小分区(当前的策略;Q是一致性哈希环上等大小分区的数目,S是存储节点的数目)
     这些策略的细节见[ DHJ+07,页215–217 ]。在Amazon的实验和实践表明“策略3是有利的,更容易部署”,以及更高效和保存成员信息的空间需求最少。据DeCandia等人,这个策略主要的好处是([ DHJ+07,217页]): 
  •      “更快的启动/恢复:由于分区范围是固定的,它们可以被存储在单独的文件,意味着一个分区可以通过简单地传输文件来当作一个单元来迁移(避免随机访问所需的特定条目定位)。这简化了启动和恢复的过程。”
  •      “简化归档:对大多数Amazon的存储服务定期归档是一个强制性的要求。归档Dynamo的整个数据集在策略3中更简单,因为分区文件可单独归档。相比之下,在策略1中,令牌是随机选择的,且归档存储在Dynamo的数据需要单独从个别节点检索键,这通常效率低下和缓慢。”

     然而,这种策略的缺点是“改变节点的成员需要协调,以便保持作业所需要的属性”([ DHJ+07,217页])。
故障处理
     为容忍由暂时不可用的存储主机引发的故障,Dynamo不使用任何严格的仲裁方法而是一个草率的版本。这种方法意味着在执行读写操作中,一个数据项的偏好列表的前N个健康节点被考虑。前N个节点顺时针分布在一致性哈希环上是不必要的([ DHJ+07,212页 ])。      处理临时不可用存储主机的次要措施是所谓的提示切换。他们开始工作,如果一个节点在它负责的数据项的写操作中是可访问的。在这种情况下,写协调器将更新复制到一个不同的节点,其通常对此数据项不承担任何责任(以确保N个节点上的持久性)。在这个复制请求中,包含更新请求原本被定向到的节点的标识符作为一个提示。当这个节点正在恢复和再次可用,它将接收到更新;已接收到作为一个替代的更新的节点,可以从本地数据库删除它([ DHJ+07,212页 ])。      DeCandia等人评论说,它足以解决临时的节点不可用,因为永久移除和添加节点是显式完成的,如先前的小节中讨论。在早期版本的Dynamo中,节点故障的全局视图是由一个外部的故障检测器建立的,其通知Dynamo环上的所有节点。“后来确定显式的节点加入和离开的方法能消除全局故障视图的需要。这是因为永久性的节点添加和移除通过显式的加入/离开方法被通知到节点,且临时节点故障被个别节点检测到,当他们不能与其他节点通信(当转发请求时)”,DeCandia等人总结([ DHJ+07,213页])。      为了解决整个数据中心的停电问题,Dynamo被配置成在多个数据中心中确保存储的方式。“在本质上,一个键的偏好列表被构造,使得存储节点分布在多个数据中心”,DeCandia等人说([ DHJ+07,212页 ])。Amazon数据中心的基础设施通过高速网络连接,使数据中心之间复制的延时没有任何问题。      除了上面描述的故障情况外,可能还有持久性的威胁。这通过实现一个副本同步的反熵协议来处理。Dynamo使用Merkle树来有效地检测副本之间的不一致性,并确定需进行同步的数据。“Merkle树是一种哈希树,其叶节点是独立键的哈希值。树中较高层的父节点是他们各自孩子的哈希”,DeCandia等人阐明。这允许对不一致性的高效检查,两个节点通过分层比较其Merkle树的哈希值来确定差异:首先,通过检查该树的根节点,其次——如果检测到不一致性——通过检测其子节点,以此类推。副本同步协议只需要很少的网络带宽,因为只有哈希值必须在网络上传输。它也可以快速操作因为使用树遍历的分治法来比较数据。Dynamo中,存储节点为每个键范围(即在一致性哈希环上的两个存储节点之间的范围)维护一个Merkle树,其负责且可以将这样的Merkle树与负责相同键范围的其他副本的Merkle树做比较。DeCandia等人认为这种方法的缺陷是,当一个节点加入或离开系统,因为键范围变化一些Merkle树会失效([ DHJ+07,212页])。
4.1.4 实现和优化      除了系统设计,DeCandia等人对Dynamo的实现做出了具体评论。根据在Amazon的经验,他们还提供了其优化建议([ DHJ+07,页213 ]):
  •      Dynamo节点上运行的代码包括:一个请求协调器、一个成员和一个失败检测组件——均用Java实现。
  •      Dynamo提供可插拔的持久化组件(如Berkeley Database Transactional Data Store和BDB的Java版[ Ora10d ],MySQL[ ora10c ],带持久化后备存储的内存缓冲)。从Amazon的经验表明,“[应用]基于它们的对象粒度分布选择Dynamo的本地持久化引擎。
  •      负责协调请求的组件被实现“在事件驱动的消息层之上,其消息处理管道被拆分为多个阶段”。节点间通信采用Java NIO通道(见[ Ora10b ])。
  •      当客户端发出读或写请求时,所接触的节点将成为其协调器。它随后创建一个状态机,“包含所有确定负责某个键的节点、发送请求、等待响应、可能做重试、处理答复和包装给客户端的响应的逻辑。每个状态机实例仅处理一个客户端请求。”
  •      如果请求协调器接收到来自节点的过期数据,它将以最新版本更新它。这就是所谓的读修复,“因为它修复了在某个机会主义时刻错过了一次最近更新的副本,并通过这样做缓解了不得不执行反熵协议的情况”。
  •      为了实现平均的负载分配,写请求可寻址到“偏好列表中任意的top N个节点”。
  •      作为一个减少读写请求延时的优化,Dynamo允许客户端应用程序成为这些操作的协调者。在这种情况下,为一个请求创建的状态机在客户端本地维护。为获取存储主机成员当前状态的信息,客户端定期联系一个随机节点,并下载其对成员的视图。这允许客户端对任何给定的键可确定由哪个节点负责,以便它可以协调读请求。一个客户端可将写请求转发到被写键的偏好列表的节点上。另外,客户端可以在本地协调写请求,如果Dynamo实例的版本机制是基于物理时间戳的(而不是向量时钟)。同时,通过使用客户端控制的请求协调器,第99.9百分位数以及平均延迟可以显著降低。这个“改善是因为客户端驱动的方法消除了负载均衡器的开销和额外的网络跳数,当一个请求被分配到一个随机节点时其可能发生”,DeCandia等人总结([ DHJ+07,页217 ])。
  •      由于写请求通常在读请求之后,Dynamo追求一个进一步的优化:在读响应中,以最快的速度响应读协调器的存储节点被发送到客户端;在随后的写请求中,客户端将与此节点联系。“这个优化使我们能得到具有先前读操作获得的数据的节点,从而增加达到‘读自身写’一致性的机会”。
  •      为达到Amazon所需的99.9百分位数性能,一个进一步的优化是在存储节点的主内存中引入一个对象缓冲区。在这个缓冲区内写请求被排队,并由一个写线程周期性地保存到磁盘。在读操作中,对象缓冲区和存储节点保存的数据必须被检查。当一个服务器崩溃时,使用一个异步持久化的内存缓冲区会导致数据丢失。为了减少这种风险,写请求的协调器将选择一个特定的副本节点来执行一个数据项的永久写操作。这个永久写不会影响请求的性能,因为协调器在应答客户端前只等待W−1个节点([ DHJ+07,页215 ])。
  •      Dynamo节点不仅要服务于客户端请求,也要执行一些背景任务。为请求处理间的资源分配和背景任务,一个名为许可控制器的组件负责“当执行一个’前台’的put/get操作时不断监测资源访问的行为”。监测包括磁盘I/O延迟、锁争用、导致失败的数据库访问的事务超时、请求队列的等待时长。通过监测反馈,许可控制器将决定给予背景任务的资源访问或消耗的时间片的数量。它也协调背景任务的执行,其必须通过许可控制器显式地申请资源([ DHJ+07,页218 ])

4.1.5 评价      为总结Amzon Dynamo的讨论,Bob Ippolito的文章“放弃ACID并思考数据”中的一个简要评价应被展示,如表4.2。
优点 缺点
“No master”
“Highly available for write” operations
“Knobs for tuning reads” (as well as
writes)
“Simple”
“Proprietary to Amazon”
“Clients need to be smart“ (support vector
clocks and conflict resolution, balance
clusters)
“No compression” (client applications may
compress values themselves, keys cannot be
compressed)
“Not suitable for column-like workloads”
“Just a Key/Value store“ (e. g. range
queries or batch operations are not possible)
表4.1:Amazon的Dynamo——Ippolito的评价([ Ipp09 ])

4.2 Project Voldemort      Project Voldemort是一种键/值存储,由LinkedIn最初开发并仍在使用。它提供了一个包括以下功能的API:([ K+10b ]):
  • get(key),返回一个value对象
  • put(key, value)
  • delete(key)
     键和值两者均可以是复杂的、复合的对象,同样也包括list和map。在Project Voldemort的设计文档中讨论了——与关系型数据库相比——键/值存储的简单数据结构和API不能提供复杂的查询功能:连接必须在客户端应用程序中实现,外键的约束是不可能的;此外,不能设置触发器和视图。然而,键/值存储这种简单的概念提供了一些优势([ K+10b ]):
  • 只允许有效率的查询。
  • 预计的查询性能可以相当不错。
  • 数据可以很容易地分布到一个群集或一个节点的集合中。
  • 在面向服务的体系结构中,没有外键约束、以及在应用程序代码中做连接并非罕见,因为数据被检索和存储在多个服务或数据源中。
  • 在关系型数据库中获得性能往往导致非规范化的数据结构或存储更复杂的对象,如BLOBs或XML文档。
  • 应用逻辑和存储可以很好地分离(相比关系型数据库,应用开发人员可能会鼓励将业务逻辑与存储操作混合,或在数据库中实现业务逻辑,如使用存储过程以优化性能)。
  • 在应用程序的面向对象范式和数据存储范式间没有那种不匹配,如关系型数据库中出现的。

4.2.1 系统架构      Project Voldemort指定一个包括若干层的逻辑架构,如图4.3所示。 NoSQL数据库介绍(4)
图4.3:Project Voldemort——逻辑架构(来自[ K+10b ])
     逻辑架构的每一层有其自身的责任(如TCP/IP网络通信,序列化,版本恢复,节点间的路由),还实现了一个包含get, put, delete操作的接口。这些被Project Voldemort作为整体暴露。如果get操作在路由层被调用,则其负责并行地分发这个操作到所有节点的并行,也负责处理可能出现的错误。      分层的逻辑架构为部署Project Voldemort提供了一定的灵活性,因为层可以被混合和匹配以满足应用程序的要求。例如,一个压缩层可以在序列化层中引入,以压缩所有交换的数据。同样地,智能路由(即确定管理包含请求数据的分区的节点)可被数据存储透明地提供,如果网络层被放置在路由层之上;如果这些层被干扰,则应用程序可以自己来做路由以减少网络跳数带来的延迟。 NoSQL数据库介绍(4)
图4.4:Project Voldemort——物理架构的选项(来自[ K+10b ])
     图4.4描述了Project Voldemort的物理部署的选项,重点是路由和负载均衡。
  •      左侧显示了一个通用的三层体系架构,具有2个负载均衡组件(可用硬件或软件实现,例如在一个round-robin进程中)。
  •      中间图避免了后端服务和Voldemort集群之间的负载均衡器。这种方法虽然减少了延时,却是紧耦合的。这种情况是因为后端服务必须确定数据的分区和它们位于的节点,以便从前端路由请求(“分区路由”)。
  •      右图中后端服务甚至包含了Voldemort实例。前端已经尝试使用分区路由以启用高性能设置。这种情况前端的请求立即指向包含数据分区的后端服务(这可能是错误的,但随后通过后端服务之间的路由得到纠正)
     图4.4中所描述的权衡是通过最小化网络跳数来减少延时 vs. 通过软件栈前移到前端的路由逻辑来实现强耦合。      进一步的研究可以减少磁盘I/O的影响,其是存储系统中最大的性能杀手。Project Voldemort的设计文档建议使用数据分区和大缓存来减少磁盘I/O造成的性能瓶颈,特别是磁盘的寻道时间和低磁盘缓存效率。因此Project Voldemort——如同Dynamo——使用带副本数据的一致性哈希来容忍存储节点停机。
4.2.2 数据格式和查询      Project Voldemort允许键/值对的命名空间被称为“stores”,其中键是唯一的。每一个键都与一个值关联,值允许包含list和map以及标量值。      Project Voldemort的操作对一个键/值对是原子的。一旦一个get操作被执行,值通过游标从服务器流出。Project Voldemort的文档认为这种方法在与包含大list的值(“其必须保存在服务器上并通过游标懒流化”)结合时不能很好地工作;在这种情况下,将查询分拆为子查询被认为效率更高。
4.2.3 版本和一致性      如Amazon的Dynamo,Project Voldemort被设计成对写操作高可用,允许并发修改数据,并使用向量时钟以允许不同版本间的临时推理(见第3.1.3和4.1.3)。如果数据存储本身不能解决版本冲突,客户端应用程序被要求在读时解决冲突。这种读调和的方法比强一致但低效的两阶段提交(2PC)方法以及Paxos风格的一致性协议更受到青睐。这是因为它很少需要协调且提供更高的可用性、效率和容错性。缺点是,客户端应用程序必须实现冲突解决逻辑,而这在2PC和Paxos风格的一致性协议是不必要的。
4.2.4 持久层和存储引擎      如图4.3,Project Voldemort提供了可插拔的持久性,因为最低层的逻辑架构允许不同的存储引擎。在框架之外,BerkeleyDB(默认存储引擎)、MySQL以及内存存储可被提供给Project Voldemort。如需使用另一个现存的或自写的存储引擎,操作get、put和delete以及一个值的迭代器需要被实现。
4.2.5 数据模型和序列化      在Project Voldemort的最低层,键和值是简单的字节数组。为了让应用程序使用一个更复杂的键和值的概念,可为每个Voldemort存储配置数据模型。这些数据模型定义了序列化器,其负责将字节数组转换为所需的数据结构和格式。Project Voldemort已经包含以下数据结构和格式:      JSON(JavaScript Object Notation)是一个二进制和有类型的数据模型,其支持的数据类型如list,map,date,boolean以及不同精度的数字([ Cro06 ])。JSON可以从字节和字符串序列化和反序列化。通过使用JSON数据类型,通过管理工具如Voldemort命令行与Project Voldemort实例进行通信是可能的。      字符串  以存储未解释的字符串,也可以为XML blobs而使用。      Java序列化  由实现了java.o.Serializable接口的Java类提供([ Ora10a ]、[ Blo01,页213 ])。      Protocol Buffers  是“Google的语言中立、平台中立、可扩展的结构化数据序列化机制”,它还包含一个接口描述语言来为自定义数据交换生成代码。Protocol buffers广泛用在谷歌“几乎所有的内部RPC协议和文件格式”([ Goo10a ]、[ Goo10b ])。      恒等  完全不序列化或反序列化,只是简单地递交字节数组。
     Project Voldemort可以通过进一步的自定义序列化器来扩展。为了允许正确的数据交换,它们必须被数据存储逻辑和客户端应用程序双方知道。      Project Voldemort设计文档对JSON格式做了进一步的评论。它被认为很好地与多种编程语言建立映射,因为它提供了常见数据类型(字符串,数字,liat,map,对象),没有对象的关系映射失配的问题。另一方面,JSON内部是无模式的,这对应用处理JSON格式的数据会产生一些问题(如数据存储希望JSON值的基本类型检查)。每个JSON文件可以包含一个单独的模式定义,但考虑到在数据存储中大量数据共享相同的结构这会很浪费的。Project Voldemort因此提供指定键和值的数据格式的可能性,如表4.3所示。
类型 可存储子类型 使用字节 Java类型 JSON示例 定义示例
number int8, int16,
int32, int64,
float32,
float64, date
8, 16, 32, 64,32, 64, 32 Byte, Short,
Integer, Long
Float, Double,
Date
1 "int32"
string string, bytes 2 + length ofstring or bytes String, byte[] "hello" "string"
boolean boolean 1 Boolean true "boolean"
object object 1 + size ofcontents Map<String,Object> {"key1":1,
"key2":"2",
"key3":false}
{"name":"string","height":"int16"}
array array size *sizeof(type) List<?> [1, 2, 3] ["int32"]
表4.3:Project Voldemort——JSON序列化格式数据类型(来自[ K+10b ])
     JSON序列化格式的数据类型定义允许Project Voldemort检查值和有效地存储它们,尽管数据类型的值不能用于数据查询和请求。为防止由数据类型重定义导致的无效数据,Project Voldemort与数据一起存储一个版本,以允许模式迁移。
4.2.6 索引预计算      Project Voldemort允许离线预建索引(如在Hadoop上),上传到数据存储中,并透明地切换到它们。这对批处理操作特别有用,如数据作为一个整体上传时的插入或更新大量数据造成索引重建,或在一块小空间内低效插入导致的索引碎片。在Project Voldemort中大量的数据可以被作为整体插入,创建离线索引的特征使线上系统从全量索引重建或索引碎片中解脱。

4.3 其它键/值存储 4.3.1 Tokyo Cabinet和Tokyo Tyrant      这个数据存储建立在一系列软件组件之上,其中Tokyo Cabinet和Tokyo Tyrant是最重要的。Tokyo Cabinet([ FAL10a ])是这个数据存储的核心库,持久化数据,并将一个键/值接口暴露给客户端,从而从内部数据结构如哈希表或B+树索引中得到抽象。Tokyo Tyrant([ FAL10b ])提供通过网络对该数据存储库的访问,可能通过一个专有的二进制协议、HTTP或通过memcached协议。Tokyo Cabinet管理磁盘上和内存中的数据存储,以一种类似分页/交换的方式。内存页周期性地刷新到磁盘,“这会留下一个数据丢失的漏洞”,如Hoff评论的([ Hof09a ])。另一方面,Tokyo Cabinet允许使用LZW算法压缩页,可以达到很好的压缩比(见例如[ Lin09 ])。Tokyo Cabinet自动对数据分区,因此使用类似MySQL的策略进行复制。除了通过键查找,如果键是排序的它可以匹配键的前缀或范围。值得一提的是事务特性,Tokyo Cabinet提供预写式日志(注:WAL)和影子分页。Tokyo套装被积极开发,文档良好,并被广泛认为是高性能的:100万条记录可以在0.7秒内存储(使用哈希表的引擎),或1.6秒内(使用B树),根据North(参见[ Nor09 ],[ Ipp09 ],[ See09 ],[ Lin09 ],[ Hof09a ])。
4.3.2 Redis      Redis是一个相对较新的数据存储,它的开发者不特定地称之为“数据结构存储”;它通常包含在键/值存储下,因其map/dictionary的API。Redis特别的是它允许匹配键的范围,如匹配数字范围或正则表达式。相比其他的键/值存储,Redis不仅存储字节作为值,也通过直接支持允许列表和集合作为值。关于Redis的一个主要缺点是内存量限制可存储的数据量。这不能通过使用硬盘扩展。Ippolito认为,因为这个原因Redis可能比较适合缓存层([ Ipp09 ])。
4.3.3 Memcached和MemcacheDB      Memcached,流行和快速的内存缓存解决方案,广泛使用在大型和超大型规模的web站点以减轻数据库负载,已在3.2节分区中提及。Memcached服务器——如同在本章中讨论的键/值存储——提供了一个由get、put和remove操作组成的map/dictionary API。虽然不是为持久存储而设计([ F+10b ]),有一个现成的解决方案命名为MemcacheDB([ Chu09 ]),符合Memcached协议([ F+10c ]),增加了基于Berkeley DB的持久性([ Nor09 ]、[ Ipp09 ])。由于memcached不提供任何节点之间的复制,也不能对机器故障容错,只添加一个持久性存储是不够的,如博主Last.fm的Richard Jones评论的。他注意到解决方案如repcached可以复制整个memcached服务器(在主从设置下),但因没有分区容错性,它会造成管理和维护的开销([ Jon09 ])。
4.3.4 Scalaris      Scalaris是一种Erlang编写的键/值存储,从这种语言的方法中获益,如为原子事务实现的非阻塞提交协议。Scalaris使用改编版的chord服务([ SMK+01 ])来暴露一个分布式哈希表给客户端。因为它以字典序储存键,前缀的范围查询是可能的。与其他键/值存储相比Scalaris具有严格的一致性模型,提供对称的副本,并允许复杂的查询(通过编程语言库)。它也为并发事务保证了ACID特性,通过实现一个Paxos一致性协议的适用版本([ Lam98 ])。如memcached,Scalaris是纯内存的键/值存储([ Nor09 ]、[ Jon09 ])。