时间:2021-07-01 10:21:17 帮助过:19人阅读
键/值存储通常有一个简单的数据模型:一个map/dictionary,同意客户按键来存放和请求数值。
除了数据模型和API。现代键/值存储倾向于高扩展性而非一致性,因此它们中的大多数也省略了富ad-hoc查询和分析功能(尤其是联接和聚合操作被取消)。通常,可存储的键的长度被限制为一定的字节数,而在值上的限制较少([ Ipp09 ],[ Nor09 ])。
键/值存储已经存在了非常长一段时间(如BerkeleyDB [ Ora10d ]),但过去几年中出现的大量的这类NoSQL存储已经受到了Amazon Dynamo的巨大影响。其将在本章进行彻底研究。在免费和开源键/值存储的巨大多样性中。Project Voldemort将被进一步检视。
本章最后,一些其它著名的键/值存储将简要地评判。
因此。他们决定构建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 |
DeCandia等人觉得以原始形式使用一致性哈希有两个缺点:首先,存储主机和数据项的环上随机映射导致数据和负载的不平衡分布。其次,一致性哈希算法相等地对待每一个存储节点且不考虑其硬件资源。为克服这两个困难。Dynamo应用了虚拟节点的概念,即对每一个存储主机将多个虚拟节点哈希到环上(如3.2.1描写叙述),且每一个物理节点的虚拟节点数目通常考虑存储节点的硬件能力(即每一个物理节点资源越多则虚拟节点越多,[ DHJ+07。页209–210 ])。
负责存储键k的元组的存储节点也负责副本的版本号更新,以顺时针方向对键k的元组的N-1个后继者。存在一个节点列表——称为偏好列表——为Dynamo中每一个要被存储的键k确定。这个列表包含多于N个节点,由于N个连续的虚拟节点可能映射到小于N个不同的物理节点(如3.2.1对一致性哈希的讨论)。
随后的读操作因此可能从不同的副本节点返回不同的版本号。在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说明了向量时钟的使用以及检測与调和并发版本号。由此产生的向量时钟将是([ 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所看到的。
为了接触某个节点,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系统添加节点时会出现暂时的逻辑分区。为了防止这样的情况,某些节点能够成为特殊角色——种子,其能够被一种外部机制如静态配置或某种配置服务发现。于是种子被全部节点知道,从而在成员改变的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是存储节点的数目)在Amazon的实验和实践表明“策略3是有利的,更easy部署”,以及更高效和保存成员信息的空间需求最少。据DeCandia等人,这个策略基本的优点是([ DHJ+07,217页]):
这简化了启动和恢复的过程。”
”
他们開始工作,假设一个节点在它负责的数据项的写操作中是可訪问的。在这样的情况下,写协调器将更新拷贝到一个不同的节点。其通常对此数据项不承担不论什么责任(以确保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页])。
从Amazon的经验表明。“[应用]基于它们的对象粒度分布选择Dynamo的本地持久化引擎。
节点间通信採用Java NIO通道(见[ Ora10b ])。
另外,client能够在本地协调写请求,假设Dynamo实例的版本号机制是基于物理时间戳的(而不是向量时钟)。同一时候,通过使用client控制的请求协调器。第99.9百分位数以及平均延迟能够显著降低。这个“改善是由于client驱动的方法消除了负载均衡器的开销和额外的网络跳数,当一个请求被分配到一个随机节点时其可能发生”。DeCandia等人总结([ DHJ+07,页217 ])。
“这个优化使我们能得到具有先前读操作获得的数据的节点,从而添加达到‘读自身写’一致性的机会”。
当一个server崩溃时。使用一个异步持久化的内存缓冲区会导致数据丢失。为了降低这样的风险。写请求的协调器将选择一个特定的副本节点来运行一个数据项的永久写操作。
这个永久写不会影响请求的性能。由于协调器在应答client前仅仅等待W?1个节点([ DHJ+07,页215 ])。
长处 | 缺点 |
“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) |
它提供了一个包含下面功能的API:([ K+10b ]):
然而。键/值存储这样的简单的概念提供了一些优势([ K+10b ]):
相同地,智能路由(即确定管理包括请求数据的分区的节点)可被数据存储透明地提供,假设网络层被放置在路由层之上;假设这些层被干扰。则应用程序能够自己来做路由以降低网络跳数带来的延迟。
前端已经尝试使用分区路由以启用高性能设置。
这样的情况前端的请求马上指向包括数据分区的后端服务(这可能是错误的,但随后通过后端服务之间的路由得到纠正)
Project Voldemort的设计文档建议使用数据分区和大缓存来降低磁盘I/O造成的性能瓶颈。特别是磁盘的寻道时间和低磁盘缓存效率。
因此Project Voldemort——如同Dynamo——使用带副本数据的一致性哈希来容忍存储节点停机。
缺点是,client应用程序必须实现冲突解决逻辑,而这在2PC和Paxos风格的一致性协议是不必要的。
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 ])。 恒等 全然不序列化或反序列化,仅仅是简单地递交字节数组。它被觉得非常好地与多种编程语言建立映射。由于它提供了常见数据类型(字符串,数字,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"] |
这对批处理操作特别实用。如数据作为一个总体上传时的插入或更新大量数据造成索引重建,或在一块小空间内低效插入导致的索引碎片。
在Project Voldemort中大量的数据能够被作为总体插入,创建离线索引的特征使线上系统从全量索引重建或索引碎片中解脱。
内存页周期性地刷新到磁盘,“这会留下一个数据丢失的漏洞”。如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 ])。