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页 ]):
- 基础设施是由位于世界各地的很多数据中心的数万台server和网络组件组成的。
- 使用商用硬件。
- 组件故障是“操作的标准模式”。
- “亚马逊採用由数百个服务组成的高度去中心化的,松散耦合的,面向服务的架构。”
除了这些技术因素,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的协议,同意以“最小的手动管理须要”来加入和删除server([ 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等人觉得一个数据库仅仅能运行简单的冲突解决策略。因此一个client应用程序更适合这项任务,由于它“知道数据模式”且“能够决定一种最适合客户体验的冲突解决方法”。假设应用开发者不想实现这种业务逻辑特化的协调策略,Dynamo还提供了简单的可直接使用的策略。如“最后的写胜利”,一个基于时间戳的协调(參见[
DHJ+07,页207-208和214)。
为提供简单的可扩展性,Dynamo实现了“一个简单的扩展模式。以解决数据集或请求速率的增长”,以同意“一次添加一个存储主机”([ DHJ+07,页206和208)。
在Dynamo,全部节点都有同样的责任。没有有特殊作用的特殊节点。此外,它的设计倾向于于“去中心化的点对点技术,而非集中控制”,由于后者在过去的Amazon曾“导致中断”。增加系统的存储主机可能具有异构的硬件,Dynamo必须考虑按“个别server的能力”的比例来分配工作([ DHJ+07,页208])。
因为Dynamo执行在Amazon自己的管理域。环境和全部节点被觉得是非敌对的,因此Dynamo没有实现安全相关的功能如授权和认证([ DHJ+07,页206,209 ])。
表4.1总结了Dynamo在设计中所面临的困难与解决困难应用的技术,以及对应的长处。
问题 |
技术 |
长处 |
Partitioning |
Consistent Hashing |
Incremental Scalability |
High Availability for writes |
Vector clocks with reconciliation during reads |
Version size is decoupled from update rates. |
Handling temporary failures |
Sloppy Quorum and hinted handoff |
Provides high availability and durability guarantee when some of the replicas are not available. |
Recovering from permanent failures |
Anti-entropy using Merkle trees |
Synchronizes divergent replicas in the background. |
Membership and failure detection |
Gossip-based membership protocol and failure detection. |
Preserves symmetry and avoids having a centralized registry for storing membership and node liveness information |
表4.1:Amazon的Dynamo——技术总结(来自[ DHJ+07。页209 ])
系统界面
Dynamo提供给client应用程序的界面仅包括两种操作:
- get(key),返回一个对象的列表和一个context
- put(key, context, object),没有返回值
假设存储在给定键下的对象存在版本号冲突,get操作可能返回多个对象。它还返回一个上下文(context)。当中存储了如对象版本号之类的系统元数据,在put操作中。client除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对一致性哈希的讨论)。
图4.1:Amazon的Dynamo——带副本的一致性哈希(来自[ DHJ+07。209页])
数据分版本号
Dynamo被设计为一个终于一致的系统。这意味着更新操作在全部副本节点收到并应用更新前返回。
随后的读操作因此可能从不同的副本节点返回不同的版本号。在Amazon的平台副本之间的更新传播时间是有限的,假设没有发生错误。但在某些故障场景下,“更新可能在一个延长的时间段内无法到达全部副本”([ DHJ+07,210页])。
这样的不一致性须要由应用程序考虑。作为一个样例,购物车应用程序从不拒绝加入到购物车操作。即使当明显地副本没有反映最新版本号购物车时(由一个带有更新请求的向量时钟所指示。见以下)。它将加入操作应用于本地购物车。
作为更新操作的结果,Dynamo总是创建一个更新数据项的新的且不可改变的版本号。在Amazon的生产系统,大多数这些版本号线性地一个包括还有一个。系统能够通过语义调和来确定最新的版本号。然而。由于失败(如网络分区)和并发更新。同一个数据项的多个、冲突的版本号可能在系统中同一时候存在。
由于数据存储无法调和这些并行版本号。仅仅有client应用程序包括有关其数据结构和语义的知识,能够解决版本号冲突并从多个相互冲突的版本号中调和有效版本号(语义调和)。因此。使用Dynamo的client应用程序必须注意这个,必须“显式地确认同一数据的多个版本号的可能性(为了不失去不论什么更新)”([
DHJ+07,210页])。
为确定冲突的版本号,运行语义调和。并支持client应用程序来解决版本号冲突,Dynamo採用向量时钟的概念(3.1.3节介绍)。如在Dynamo系统接口的章节中所述(见4.1.3)。client在公布更新请求时须要提供一个上下文。
这个上下文包含一个他们已经读过的数据的向量时钟,所以Dynamo知道更新哪一个版本号。且能够正确地设置更新的数据项的向量时钟。假设并发版本号的数据项出现,Dynamo将在读请求返回的上下文信息中把他们传输给client。
在client向这种数据项发出一个更新之前,它必须调和并发版本号为一个有效版本号。
图4.2说明了向量时钟的使用以及检測与调和并发版本号。
图4.2:Amazon的Dynamo——数据项上的并发更新(来自[ DHJ+07,211页])
在这张图中,client首先创建一个数据项。更新请求由一个存储主机Sx处理。因此向量时钟([ SX,1 ])将与它关联。接下来,client更新这个数据项,这个更新请求导致版本号2也由Sx节点运行。
由此产生的向量时钟将是([ Sx。2 ]),于是一个[ SX,1 ]和[ Sx,2 ]间的暂时排序可被确定,由于后者显然比前者晚(同存储主机、提升版本号号)。由于[ Sx,1 ]仅仅有一个后继([ Sx,2 ])。它不再是暂时推理所需的。能够从向量时钟中删除。
接下来。一个client发出对版本号D2的更新,其被存储主机Sy处理,产生向量时钟([ Sx,2 ],[ Sy。1 ])。在这里,向量时钟的元素[ Sx,2 ]不能省略。这是由于。比如一个client发出其已从某节点(称Sx)读到版本号D2。而存储主机Sy尚未从它的伙伴节点收到这个版本号。
于是client想要更新一个比节点Sy当前已知的版本号更近的版本号,这会被接受由于Dynamo的“总是可写”属性。相同的事也发生在还有一个client已读到D2且公布一个由存储主机Sz处理的更新。Sz当前不知道D2。这导致了版本号D4与向量时钟([
Sx。2 ]。[Sz。1 ])。
在下一个读请求中,D3和D4两者被传输到client。伴随着其向量时钟的汇总——典型的:([ Sx,2 ]。[ Sy,1 ],[Sz。1 ])。client能够检測到版本号D3和D4是冲突的,因为在读操作上下文中提交的向量时钟组合并未反映一个线性、先后的顺序。在client可公布还有一个更新到系统前,它必须从并发版本号D3和D4中调和出版本号D5。
假设这个更新请求再次被节点Sx处理。这会推动向量时钟中的版本号号变为([ Sx,3 ],[ Sy,1 ],[Sz。1
])。如图4.2所看到的。
get()和put()操作的运行
Dynamo同意一个Dynamo实例的不论什么存储节点接收不论什么键的get和put请求。
为了接触某个节点,client应用程序使用或者通用的负载均衡路由,或者一个client库,其反映Dynamo的分区模式且能通过计算确定需接触的存储主机。第一个节点选择方法的优势是。它能够保持不关注Dynamo的特定代码。第二个方法降低了延时。由于不须要接触负载均衡器,从而降低了一次网络跳数([ DHJ+07,211页)。
由于不论什么节点能够接收对环上不论什么键的请求,请求可能须要被在存储主机间转发。
这是基于给定键的包括带优先顺序的需接触存储主机的偏好列表。当一个节点接收到client请求时,它将依据偏好列表中的顺序转发到存储主机上(假设节点本身在优先列表中。转发自然变得不必要)。一旦发生读或更新请求。最先N个健康的节点将被考虑(不可存取或宕机的节点仅仅是跳过。參见[ DHJ+07,211页])。
为给client提供一个一致性视图,Dynamo应用了一种仲裁式一致性协议,其包括两个可配置的值:R和W分别代表须要參与成功的读或写操作的存储主机的最小数目。为建立一个仲裁式系统。这些值必须设置为R+W>N。DeCandia等人评论说,最慢的副本决定了操作的延时,因此R和W常常被选择使得R+W<N(以降低慢主机进入读写请求的概率;參见[ DHJ+07,211页])。在其它情况下,如建立Dynamo用于“为更重量级的后端存储中数据提供权威性持久缓存的功能”,R通常设置为1,W设置为N。以同意高性能的读操作。
还有一方面“低的W和R值能够添加不一致的风险。由于写请求会被觉得是成功的并返回给client,即使他们没有被大部分副本处理过”。此外,持久性在一段时间内仍是脆弱的,当写请求完毕但更新的版本号仅仅被传播到少数几个节点。DeCandia等人评论“Dynamo的主要长处是,它的client应用程序能够调整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个主机回应。
假设这些节点用不同的版本号回复则为冲突(通过向量时钟推理观察到),它将向请求的client返回并发版本号([ DHJ+07,212页])。
成员
Dynamo实现了一个添加和删除系统节点的显式机制。Dynamo未实现隐式成员检測,由于暂时停机或颠簸的server会导致系统又一次计算一部分一致性哈希环和相关的数据结构如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是有利的,更easy部署”,以及更高效和保存成员信息的空间需求最少。据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 ])。
- 当client发出读或写请求时,所接触的节点将成为其协调器。它随后创建一个状态机,“包括全部确定负责某个键的节点、发送请求、等待响应、可能做重试、处理答复和包装给client的响应的逻辑。每一个状态机实例仅处理一个client请求。”
- 假设请求协调器接收到来自节点的过期数据,它将以最新版本号更新它。这就是所谓的读修复,“由于它修复了在某个机会主义时刻错过了一次近期更新的副本,并通过这样做缓解了不得不执行反熵协议的情况”。
- 为了实现平均的负载分配,写请求可寻址到“偏好列表中随意的top N个节点”。
- 作为一个降低读写请求延时的优化。Dynamo同意client应用程序成为这些操作的协调者。在这样的情况下,为一个请求创建的状态机在client本地维护。为获取存储主机成员当前状态的信息,client定期联系一个随机节点,并下载其对成员的视图。这同意client对不论什么给定的键可确定由哪个节点负责,以便它能够协调读请求。一个client可将写请求转发到被写键的偏好列表的节点上。
另外,client能够在本地协调写请求,假设Dynamo实例的版本号机制是基于物理时间戳的(而不是向量时钟)。同一时候,通过使用client控制的请求协调器。第99.9百分位数以及平均延迟能够显著降低。这个“改善是由于client驱动的方法消除了负载均衡器的开销和额外的网络跳数,当一个请求被分配到一个随机节点时其可能发生”。DeCandia等人总结([
DHJ+07,页217 ])。 - 由于写请求通常在读请求之后。Dynamo追求一个进一步的优化:在读响应中,以最快的速度响应读协调器的存储节点被发送到client。在随后的写请求中。client将与此节点联系。
“这个优化使我们能得到具有先前读操作获得的数据的节点,从而添加达到‘读自身写’一致性的机会”。
- 为达到Amazon所需的99.9百分位数性能。一个进一步的优化是在存储节点的主内存中引入一个对象缓冲区。在这个缓冲区内写请求被排队,并由一个写线程周期性地保存到磁盘。在读操作中。对象缓冲区和存储节点保存的数据必须被检查。
当一个server崩溃时。使用一个异步持久化的内存缓冲区会导致数据丢失。为了降低这样的风险。写请求的协调器将选择一个特定的副本节点来运行一个数据项的永久写操作。
这个永久写不会影响请求的性能。由于协调器在应答client前仅仅等待W?1个节点([
DHJ+07,页215 ])。 - Dynamo节点不仅要服务于client请求,也要运行一些背景任务。为请求处理间的资源分配和背景任务。一个名为许可控制器的组件负责“当运行一个’前台’的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不能提供复杂的查询功能:连接必须在client应用程序中实现,外键的约束是不可能的;此外,不能设置触发器和视图。
然而。键/值存储这样的简单的概念提供了一些优势([ K+10b ]):
- 仅仅同意有效率的查询。
- 估计的查询性能能够相当不错。
- 数据能够非常easy地分布到一个群集或一个节点的集合中。
- 在面向服务的体系结构中,没有外键约束、以及在应用程序代码中做连接并不是罕见。由于数据被检索和存储在多个服务或数据源中。
- 在关系型数据库中获得性能往往导致非规范化的数据结构或存储更复杂的对象。如BLOBs或XML文档。
- 应用逻辑和存储能够非常好地分离(相比关系型数据库,应用开发者可能会鼓舞将业务逻辑与存储操作混合,或在数据库中实现业务逻辑。如使用存储过程以优化性能)。
- 在应用程序的面向对象范式和数据存储范式间没有那种不匹配,如关系型数据库中出现的。
4.2.1 系统架构
Project Voldemort指定一个包含若干层的逻辑架构,如图4.3所看到的。
图4.3:Project Voldemort——逻辑架构(来自[ K+10b ])
逻辑架构的每一层有其自身的责任(如TCP/IP网络通信。序列化,版本号恢复。节点间的路由),还实现了一个包括get, put, delete操作的接口。这些被Project Voldemort作为总体暴露。假设get操作在路由层被调用,则其负责并行地分发这个操作到全部节点的并行,也负责处理可能出现的错误。
分层的逻辑架构为部署Project Voldemort提供了一定的灵活性,由于层能够被混合和匹配以满足应用程序的要求。比如,一个压缩层能够在序列化层中引入,以压缩全部交换的数据。
相同地,智能路由(即确定管理包括请求数据的分区的节点)可被数据存储透明地提供,假设网络层被放置在路由层之上;假设这些层被干扰。则应用程序能够自己来做路由以降低网络跳数带来的延迟。
图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操作被运行,值通过游标从server流出。Project Voldemort的文档觉得这样的方法在与包括大list的值(“其必须保存在server上并通过游标懒流化”)结合时不能非常好地工作;在这样的情况下。将查询分拆为子查询被觉得效率更高。
4.2.3 版本号和一致性
如Amazon的Dynamo,Project Voldemort被设计成对写操作高可用,同意并发改动数据,并使用向量时钟以同意不同版本号间的暂时推理(见第3.1.3和4.1.3)。假设数据存储本身不能解决版本号冲突,client应用程序被要求在读时解决冲突。这样的读调和的方法比强一致但低效的两阶段提交(2PC)方法以及Paxos风格的一致性协议更受到青睐。这是由于它非常少须要协调且提供更高的可用性、效率和容错性。
缺点是,client应用程序必须实现冲突解决逻辑,而这在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能够通过进一步的自己定义序列化器来扩展。为了同意正确的数据交换。它们必须被数据存储逻辑和client应用程序两方知道。
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 of string or bytes |
String, byte[] |
"hello" |
"string" |
boolean |
boolean |
1 |
Boolean |
true |
"boolean" |
object |
object |
1 + size of contents |
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 ])是这个数据存储的核心库。持久化数据,并将一个键/值接口暴露给client,从而从内部数据结构如哈希表或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节分区中提及。Memcachedserver——如同在本章中讨论的键/值存储——提供了一个由get、put和remove操作组成的map/dictionary API。尽管不是为持久存储而设计([ F+10b ]),有一个现成的解决方式命名为MemcacheDB([ Chu09 ]),符合Memcached协议([ F+10c
]),添加了基于Berkeley DB的持久性([ Nor09 ]、[ Ipp09 ])。因为memcached不提供不论什么节点之间的复制,也不能对机器故障容错,仅仅加入一个持久性存储是不够的,如博主Last.fm的Richard Jones评论的。他注意到解决方式如repcached能够复制整个memcachedserver(在主从设置下)。但因没有分区容错性,它会造成管理和维护的开销([ Jon09 ])。
4.3.4 Scalaris
Scalaris是一种Erlang编写的键/值存储。从这样的语言的方法中获益。如为原子事务实现的非堵塞提交协议。
Scalaris使用改编版的chord服务([ SMK+01 ])来暴露一个分布式哈希表给client。
由于它以字典序储存键,前缀的范围查询是可能的。
与其它键/值存储相比Scalaris具有严格的一致性模型,提供对称的副本,并同意复杂的查询(通过编程语言库)。它也为并发事务保证了ACID特性,通过实现一个Paxos一致性协议的适用版本号([
Lam98 ])。
如memcached,Scalaris是纯内存的键/值存储([ Nor09 ]、[ Jon09 ])。