1. 简介
2. 背景
3. 相关工作
4. 系统结构
5. 实现
6. 经验和教训
7. 结论
摘要:大规模的可靠性是我们在 Amazon.com 面临的最大挑战之一,它是世界上最大的电子商务运营之一;即使是最轻微的停机也会带来严重的财务后果,并影响客户的信任。Amazon.com 平台为世界各地的许多网站提供服务,它是在位于世界各地许多数据中心的数万台服务器和网络组件的基础架构之上构建的。在这种规模下,大大小小的组件不断地发生故障,而面对这些故障时管理持久状态的方式驱动着软件系统的可靠性和可伸缩性。
本文介绍了 Dynamo 的设计和实现,Dynamo 是一个高度可用的键值存储系统,亚马逊的一些核心服务使用它来提供 "永远在线" 的体验。为了达到这种可用性水平,Dynamo 在某些故障情况下牺牲了一致性。它广泛使用对象版本控制和应用程序辅助的冲突解决方式,为开发人员提供了一个新颖的接口。
处理由数百万个组件组成的基础架构中的故障是我们正常的操作模式;在任何给定时间,总是有少量的服务器和网络组件出现故障。因此,亚马逊的软件系统需要以一种将故障处理视为正常情况而不影响可用性或性能的方式来构建。
为了满足可靠性和可扩展性需求,亚马逊开发了多种存储技术,其中最著名的可能是亚马逊简单存储服务(也可在亚马逊之外获得,Amazon Simple Storage Service 称为亚马逊 S3)。本文介绍了 Dynamo 的设计和实现,Dynamo 是为亚马逊平台构建的另一个高度可用和可扩展的分布式数据存储。Dynamo 用于管理具有非常高的可靠性要求并且需要严格控制可用性、一致性、成本效益和性能之间权衡的服务状态。亚马逊的平台有一套非常多样化的应用程序,具有不同的存储要求。一组选定的应用程序需要一种足够灵活的存储技术,以便应用程序设计人员能够根据这些权衡来适当地配置他们的数据存储,从而以最经济高效的方式实现高可用性和有保证的性能。
亚马逊的平台上有许多服务只需要对数据存储进行主键访问。对于许多服务,例如那些提供畅销书列表、购物车、客户偏好、会话管理、销售排名和产品目录的服务,使用关系数据库的常见模式会导致效率低下,并限制规模和可用性。Dynamo 提供了一个简单的主键接口来满足这些应用程序的要求。
Dynamo 综合了一些众所周知的技术来实现可伸缩性和可用性:使用一致性哈希算法对数据进行分区和复制,并且通过对象版本控制来促进一致性。通过类似仲裁的技术和去中心化的副本同步协议来维护更新过程中副本之间的一致性。Dynamo 采用基于 gossip 的分布式故障检测和成员协议。Dynamo 是一个完全去中心化的系统,只需要很少的人工管理。可以在 Dynamo 中添加和删除存储节点,而不需要任何手动分区或重新分配。
过去一年,Dynamo 一直是亚马逊电子商务平台许多核心服务的底层存储技术。在繁忙的假日购物季节,它能够高效地扩展到极端峰值负载,而没有任何停机时间。例如,维护购物车的服务(购物车服务)为数千万个请求提供服务,这些请求在一天内导致超过 300 万次结帐,而管理会话状态的服务处理了数十万个并发活动的会话。
这项工作对研究界的主要贡献是评估如何将不同的技术结合起来,以提供一个高度可用的系统。它表明,最终一致的存储系统可以用于要求苛刻的应用程序的生产中。它还提供了对这些技术的调整,以满足对性能要求非常严格的生产系统的要求。
论文结构如下。第 2 节介绍了背景,第 3 节介绍了相关工作。第 4 节介绍了系统设计,第 5 节描述了实现。第 6 节详细介绍了在生产中运行 Dynamo 获得的经验和见解,第 7 节总结了本文。本文中有许多地方可能需要额外的信息,但保护亚马逊的商业利益需要我们减少一些细节。因此,第 6 节中的数据中心内部和数据中心之间的延迟、第 6.2 节中的绝对请求率以及第 6.3 节中的停机时间和工作负载是通过聚合度量而不是绝对细节来提供的。
亚马逊的电子商务平台由数百项服务组成,这些服务协同工作,提供从推荐、订单执行到欺诈检测等功能。每个服务都通过一个明确明确定义的接口公开,并可通过网络访问。这些服务托管在一个基础架构中,该基础架构由遍布全球许多数据中心的数万台服务器组成。这些服务中的一些是无状态的(即聚合来自其他服务的响应的服务),一些是有状态的(即通过对存储在持久存储中的状态执行业务逻辑来生成响应的服务)。
传统上,生产系统将状态存储在关系数据库中。然而,对于许多更常见的状态持久化使用模式,关系数据库是一个远非理想的解决方案。这些服务中的大多数只按主键存储和检索数据,不需要关系数据库管理系统提供的复杂查询和管理功能。这种多余的功能需要昂贵的硬件和高技能人员来操作,这使得它成为一种非常低效的解决方案。此外,可用的复制技术有限,通常选择一致性而不是可用性。尽管近年来取得了许多进展,但扩展数据库或使用智能分区方案进行负载平衡仍然不容易。
本文描述了 Dynamo,一种高度可用的数据存储技术,解决了这些重要服务类别的需求。Dynamo有一个简单的键/值接口,具有明确定义的一致性窗口,高度可用,资源使用高效,并有一个简单的横向扩展方案来解决数据集大小或请求速率的增长。每个使用 Dynamo 的服务都运行自己的 Dynamo 实例。
此类服务的存储系统有以下要求:
查询模型:对由关键字唯一标识的数据项的简单读写操作。状态存储为由唯一键标识的二进制对象(即 Blobs)。没有跨越多个数据项的操作,也不需要关系模式。这一要求是基于这样的观察,即亚马逊的很大一部分服务可以使用这个简单的查询模型,并且不需要任何关系模式。Dynamo 面向需要存储相对较小(通常小于1 MB)的对象的应用程序。
ACID 属性:ACID(原子性、一致性、隔离性、持久性)是一组保证数据库事务得到可靠处理的属性。在数据库环境中,对数据的单个逻辑操作称为事务。亚马逊的经验表明,提供ACID 保证的数据存储往往可用性较差。这已经得到业界和学术界的广泛认可。Dynamo 的目标是一致性较弱的应用程序(ACID 中的 "C")。Dynamo 不提供任何隔离保证,只允许单键更新。
效率:系统需要在商用硬件基础设施上运行。在亚马逊的平台上,服务有严格的延迟要求,通常在分布的 99.9% 进行测量。鉴于状态访问在服务操作中起着至关重要的作用,存储系统必须能够满足如此严格的服务层协议(参见下文第 2.2 节)。服务必须能够配置 Dynamo,使其始终达到延迟和吞吐量要求。权衡是在性能、成本效率、可用性和耐用性保证方面。
其他假设:Dynamo 只被亚马逊内部服务使用。其操作环境被认为是友好的,并且没有诸如认证和授权的安全相关要求。此外,由于每项服务都使用其独特的 Dynamo 实例,其最初的设计目标是多达数百台存储主机的规模。我们将在后面的章节中讨论 Dynamo 的可伸缩性限制和可能的可伸缩性相关扩展。
为了保证应用程序能够在有限的时间内交付其功能,平台中的每个依赖项都需要以更严格的限制来交付其功能。客户和服务签订服务层协议,这是一种正式协商的合同,客户和服务就几个与系统相关的特征达成一致,其中最突出的包括客户对特定应用编程接口的预期请求速率分布以及在这些条件下的预期服务延迟。简单 SLA 的一个例子是一个服务,它保证它将在 300 毫秒内对其 99.9% 的请求提供响应,以达到每秒 500 个请求的峰值。
在亚马逊去中心化的面向服务的基础设施中,服务层协议扮演着重要的角色。例如,对一个电子商务站点的页面请求通常需要呈现引擎通过向 150 多个服务发送请求来构造其响应。这些服务通常有多个依赖项,这些依赖项通常是其他服务,因此应用程序的调用图有多个级别并不罕见。为了确保页面呈现引擎能够在页面交付上保持明确的界限,调用链中的每个服务都必须遵守其性能契约。
图 1 显示了亚马逊平台架构的抽象视图,其中动态网页内容是由页面呈现组件生成的,而页面呈现组件又会查询许多其他服务。服务可以使用不同的数据存储来管理其状态,并且这些数据存储只能在其服务边界内访问(these data stores are only accessible within its service boundaries)。一些服务充当聚合器(Aggregators),通过使用其他几个服务来产生复合响应。通常,聚合器服务是无状态的,尽管它们使用大量的缓存。
行业中形成面向性能的服务层协议的一种常见方法是使用平均值、中位数和预期方差来描述它。在亚马逊,我们发现,如果目标是建立一个所有客户都有良好体验的系统,而不仅仅是大多数客户,那么这些指标就不够好。例如,如果使用广泛的个性化技术,那么客户历史较早的请求更多的处理,这会影响高端分布的性能。以平均或中位响应时间表示的服务层协议不能解决这一重要客户群体的问题。为了解决这个问题,在亚马逊,服务层协议是在 99.9% 的分布中被传递和被测量的。99.9% 甚至更高百分比的选择是基于成本效益分析,该分析表明成本显著增加,从而大大提高了性能。亚马逊生产系统的经验表明,与那些满足基于平均值或中间值定义的服务层协议的系统相比,这种方法提供了更好的整体体验。
在这篇文章中,有许多对这 99.9% 分布的引用,这反映了亚马逊工程师从客户体验的角度对性能的不懈关注。许多论文都是关于平均值的,所以为了便于比较,这些都包含在内了。然而,亚马逊的工程和优化工作并不关注平均值。一些技术,如写协调器(write coordinators)的负载平衡选择,纯粹是为了控制 99.9% 的性能。
存储系统通常在建立服务的服务层协议中起着重要的作用,尤其是在业务逻辑相对轻量级的情况下,就像许多亚马逊服务一样。然后,状态管理成为服务的服务层协议的主要组成部分。 Dynamo 的主要设计考虑之一是让服务控制它们的系统属性,比如持久性和一致性,并让服务在功能、性能和成本效益之间进行权衡。
商业系统中使用的数据复制算法传统上执行同步副本协调(synchronous replica coordination),以便提供高度一致的数据访问接口。为了达到这种程度的一致性,这些算法被迫在某些故障情况下权衡数据的可用性。例如,不是处理答案正确性/不确定性,而是在完全确定答案是正确的之前,数据是不可用的。从早期的复制数据库工作来看,众所周知,当处理网络故障的可能性时,强一致性和高数据可用性不能同时实现。因此,系统和应用程序需要知道在什么条件下可以实现哪些特性。
对于容易出现服务器和网络故障的系统,可以通过使用乐观复制技术来提高可用性,在乐观复制技术中,允许将更改传播到后台的副本,并且允许并发、断开连接的工作。这种方法的挑战在于,它可能导致必须被检测和被解决的冲突性的变更(The challenge with this approach is that it can lead to conflicting changes which must be detected and resolved)。这个解决冲突的过程引入了两个问题:什么时候解决,谁来解决。Dynamo 被设计成一个最终一致的数据存储;也就是说,所有更新最终都会到达所有副本。
一个重要的设计考虑是决定何时执行解决更新冲突的过程,即冲突是否应该在读取或写入期间解决。许多传统数据存储在写入期间执行冲突解决,并保持简单的读取复杂性。在这种系统中,如果数据存储在给定时间不能到达所有(或大部分)副本,则写入可能会被拒绝。另一方面,Dynamo 的目标是 "始终可写" 数据存储的设计空间(即,对写入高度可用的数据存储)。对于许多亚马逊服务来说,拒绝客户更新可能会导致糟糕的客户体验。例如,购物车服务必须允许客户在网络和服务器出现故障的情况下在购物车中添加和删除商品。这一要求迫使我们将冲突解决的复杂性推到读取过程上,以确保写入永远不会被拒绝。(何时解决)
下一个设计选择是由谁来执行冲突解决过程。这可以通过数据存储或应用程序来完成。如果冲突解决是由数据存储完成的,那么它的选择是相当有限的。在这种情况下,数据存储只能使用简单的策略来解决冲突的更新,例如 "最后写入成功"。另一方面,由于应用程序知道数据模式,它可以决定最适合其客户体验的冲突解决方法。例如,维护客户购物车的应用程序可以选择 "合并" 冲突的版本,并返回一个统一的购物车。尽管有这种灵活性,一些应用程序开发人员可能不想编写他们自己的冲突解决机制,而是选择将其下推到数据存储,而数据存储又选择一个简单的策略,如 "最后一次写入获胜"。(由谁解决)
设计中包含的其他关键原则有:
增量可扩展性:Dynamo 应该能够一次横向扩展一个存储主机(以下简称为 "节点"),对系统操作员和系统本身的影响最小。
对称性:Dynamo中的每个节点都应该和它的对等节点有相同的一套职责;不应该有一个或多个特殊的节点承担特殊的角色或额外的责任。根据我们的经验,对称简化了系统配置和维护的过程。
去中心化:对称性的延伸,设计应该支持去中心化的点对点技术,而不是集中控制。过去,集中控制导致停机,目标是尽可能避免停机。这导致了一个更简单、更可扩展、更可用的系统。
异构性:系统需要能够在其运行的基础设施中利用异构性。例如,工作分配必须与各个服务器的能力成比例。这对于添加具有更高容量的新节点而不必一次升级所有主机是至关重要的。
有几个 P2P 系统已经研究了数据存储和分发的问题。第一代 P2P 系统,如 Freenet 和Gnutella,主要用作文件共享系统。这些是非结构化 P2P 网络的例子,其中对等体之间的覆盖链路是任意建立的。在这些网络中,搜索查询通常会在网络中泛滥,以找到尽可能多的共享数据的对等点。P2P 系统发展到下一代,成为众所周知的结构化 P2P 网络。这些网络采用全球一致的协议,以确保任何节点都可以高效地将搜索查询路由到拥有所需数据的对等点。像 Pastry 和和 Chord 这样的系统使用路由机制来确保查询可以在有限的跳数内得到回答。为了减少由多跳路由引入的额外延迟,一些 P2P 系统采用 O(1) 路由,其中每个对等体在本地维护足够的路由信息,使得它可以在恒定的跳数内将请求(访问数据项)路由到适当的对等体。
各种存储系统,如 Oceanstore 和 PAST 是建立在这些路由覆盖的基础上。Oceanstore 提供了一种全局、事务性、持久性存储服务,支持对广泛复制的数据进行序列化更新。为了允许并发更新,同时避免广域锁定固有的许多问题,它使用了基于冲突解决的更新模型。有的系统中引入了冲突解决,以减少事务中止的次数。Oceanstore 通过处理一系列更新来解决冲突,在它们之间选择一个总顺序,然后以该顺序自动应用它们。它是为在不受信任的基础架构上复制数据的环境而构建的。相比之下,PAST 为持久和不可变的对象提供了一个简单的抽象层。它假设应用程序可以在其上构建必要的存储语义(例如可变文件)。
在文件系统和数据库系统社区中,为了性能、可用性和持久性而分发数据已经得到了广泛的研究。与只支持扁平的名称空间(flat namespaces)的 P2P 存储系统相比,分布式文件系统通常支持分层名称空间。像 Fixes 和 Coda 这样的系统以牺牲一致性为代价来复制文件以获得高可用性。更新冲突通常使用专门的冲突解决程序来管理。Farsite 系统是一个分布式文件系统,不像 NFS 那样使用任何集中式服务器。Farsite 使用复制实现高可用性和可扩展性。GFS 是另一个分布式文件系统,用于托管谷歌内部应用程序的状态。GFS 使用一个简单的设计,只有一个 master 来托管整个元数据,数据被分成块并存储在块服务器中。Bayou 是一个分布式关系数据库系统,它允许断开连接的操作,并提供最终的数据一致性。
在这些系统中,Bayou、Coda 和 Ficus 允许断开连接的操作,并对网络分区和中断等问题具有弹性。这些系统在冲突解决程序上有所不同。例如,Coda 和 Ficus 执行系统级冲突解决,Bayou 允许应用程序级解决。然而,所有这些都保证了最终的一致性。与这些系统类似, Dynamo 允许读写操作甚至在网络分区期间继续进行,并使用不同的冲突解决机制来解决更新的冲突。像 FAB 这样的分布式块存储系统将大型对象分割成较小的块,并以高度可用的方式存储每个块。与这些系统相比,键值存储在这种情况下更合适,因为:(a) 它旨在存储相对较小的对象(大小< 1M)。 (b) 键值存储更容易根据每个应用程序进行配置。Antiquity 是一个广域分布式存储系统,旨在处理多个服务器故障。它使用安全日志(secure log)来保持数据完整性,在多台服务器上复制每个日志以获得持久性,并使用拜占庭容错协议来确保数据一致性。与 Antiquity 相比,Dynamo 不关注数据完整性和安全性问题,而是为可信环境而构建。Bigtable 是一个用于管理结构化数据的分布式存储系统。它维护一个稀疏的多维排序 map,并允许应用程序使用多个属性访问它们的数据。与 Bigtable 相比,Dynamo 的目标应用程序只需要键/值访问,主要关注高可用性,即使在网络分区或服务器出现故障时,更新也不会被拒绝。
传统的复制关系数据库系统关注于保证复制数据的强一致性问题。虽然强大的一致性为应用程序作者提供了一个方便的编程模型,但这些系统在可扩展性和可用性方面受到限制。这些系统不能处理网络分区,因为它们通常提供强大的一致性保证。
Dynamo 在目标要求方面不同于上述去中心化存储系统。首先,Dynamo 主要面向那些需要 "始终可写" 数据存储的应用程序,在这些应用程序中,不会有因失败或并发写入而被拒绝的更新。这是许多亚马逊应用程序的一个关键要求。其次,如前所述,Dynamo 是为单个管理域内的基础架构而构建的,其中所有节点都被认为是可信的。第三,使用 Dynamo 的应用程序不需要支持分层名称空间(许多文件系统中的一种规范)或复杂的关系模式(由传统数据库支持)。第四,Dynamo 是为延迟敏感的应用程序而构建的,这些应用程序需要在数百毫秒内执行至少 99.9% 的读写操作。为了满足这些严格的延迟要求,我们必须避免通过多个节点路由请求(这是 Chord 和 Pastry 等几个分布式哈希表系统采用的典型设计)。这是因为多跳路由增加了响应时间的可变性,从而增加了更高百分比的延迟。Dynamo 可以被描述为零跳分布式哈希表(Dynamo can be characterized as a zero-hop DHT),其中每个节点在本地维护足够的路由信息,以将请求直接路由到适当的节点。
需要在生产环境中运行的存储系统的体系结构非常复杂。除了实际的数据持久性组件之外,系统还需要针对负载均衡、成员资格和故障检测、故障恢复、副本同步、过载处理、状态转移、并发和作业调度、请求编组、请求路由、系统监控和报警以及配置管理等方面提供可扩展且强健的解决方案。描述每一个解决方案的细节是不可能的,所以本文主要关注 Dynamo 中使用的核心分布式系统技术:分区、复制、版本控制、成员资格、故障处理和扩展。表 1 总结了 Dynamo 使用的技术及其各自的优势。
Dynamo 通过一个简单的接口存储与一个 key 相关联的对象;它公开了两个操作:get() 和put() 。get(key) 操作在存储系统中定位与 key 相关联的对象副本,并返回单个对象或具有冲突版本的对象列表以及上下文。put(key, context, object) 操作根据关联的 key 确定对象的副本应该放在哪里,并将副本写入磁盘。context 对调用方不透明的关于对象的系统元数据进行编码,并包括诸如对象版本等信息。上下文信息与对象一起存储,以便系统可以验证 put 请求中提供的上下文对象的有效性。
Dynamo 将调用者提供的 key 和对象都视为不透明的字节数组。它对 key 应用 MD5 哈希来生成 128 位标识符,该标识符用于确定负责提供 key 的存储节点。
Dynamo 的关键设计要求之一是它必须逐步扩展。这需要一种在系统中的一组节点(即存储主机)上动态划分数据的机制。Dynamo 的分区方案依靠一致性哈希算法在多个存储主机之间分配负载。在一致性哈希算法中,哈希函数的输出范围被视为固定的循环空间或 "环"(即最大的哈希值绕到最小的哈希值)。系统中的每个节点在这个空间内被分配一个随机值,代表它在环上的 "位置"。由 key 标识的每个数据项通过哈希数据项的 key 以产生其在环上的位置而被分配给一个节点,然后顺时针遍历环以找到位置大于该项的位置的第一个节点。因此,每个节点对其与环上的前一个节点之间的环中的区域负责。一致性哈希的主要优点是,一个节点的离开或到达只影响它的近邻,而其他节点不受影响。
基本的一致性哈希算法带来了一些挑战。首先,环上每个节点的随机位置分配导致数据和负载分布不均匀。第二,基本算法忽略了节点性能的异质性(不同结点处理数据的能力不同)。为了解决这些问题,Dynamo 使用了一致性哈希算法的变体:不是将一个节点映射到圆中的一个点,而是将每个节点分配到环中的多个点。为此,Dynamo 使用了 "虚拟节点" 的概念。虚拟节点看起来像系统中的单个节点,但是每个节点可以负责多个虚拟节点。实际上,当一个新节点被添加到系统中时,它会在环中被分配多个位置(此后称为 "tokens")。第 6 节讨论了微调 Dynamo 分区方案的过程。使用虚拟节点有以下优点:
(1) 如果某个节点变得不可用(由于故障或日常维护),则该节点处理的负载将平均分布在剩余的可用节点上。
(2) 当一个节点再次变得可用时,或者一个新节点被添加到系统中时,新的可用节点从每个其他可用节点接受大致相等的负载量。
(3) 考虑到物理基础架构的异构性,节点负责的虚拟节点数量可以根据其容量来决定。
为了实现高可用性和持久性,Dynamo 在多台主机上复制其数据。每个数据项在 N 个主机上复制,其中 N 是一个 "每个实例" 配置的参数。每个密钥 k 被分配给一个协调器节点(在前一节中有描述)。协调器负责复制其范围内的数据项。除了在本地存储其范围内的每个密钥之外,协调器还在环中的 N-1 个顺时针后续节点上复制这些密钥。这导致一个系统,其中每个节点负责它和它的第 N 个前身之间的环的区域。在图 2 中,节点 B 除了在本地存储密钥 k 之外,还在节点 C 和节点 D 复制密钥 k。节点 D 将存储属于范围 (A,B]、(B,C] 和 (C,D] 的键。
负责存储特定 key 的节点列表称为首选项列表。该系统的设计将在第 4.8 节中进行解释,以便系统中的每个节点都可以确定对于任何特定的键,哪些节点应该在该列表中。考虑到节点故障,首选列表包含 N 个以上的节点。注意,通过使用虚拟节点,特定密钥的前 N 个后继位置可能由少于 N 个不同的物理节点拥有(即,一个节点可以持有前 N 个位置中的多于一个)。为了解决这个问题,通过跳过环中的位置来构建键的偏好列表,以确保该列表仅包含不同的物理节点。
Dynamo 提供了最终的一致性,允许更新异步传播到所有副本。在所有副本应用更新之前,put() 调用可能会返回到其调用者,这可能会导致后续 get() 操作可能会返回没有最新更新的对象的情况。如果没有失败,则更新传播时间有一个界限。但是,在某些故障情况下(例如,服务器中断或网络分区),更新可能不会在很长一段时间内到达所有副本。
亚马逊的平台上有一类应用可以容忍这种不一致,并且可以被构建为在这些条件下运行。例如,购物车应用程序要求 "添加到购物车" 操作永远不能被忘记或拒绝。如果购物车的最新状态不可用,并且用户对购物车的旧版本进行了更改,则该更改仍然有意义,应该保留。但与此同时,它不应该取代购物车当前不可用的状态,购物车本身可能包含应该保留的更改。请注意,"添加到购物车" 和 "从购物车中删除项目" 操作都被转换为向 Dynamo 发出的请求。当顾客想要将物品添加到购物车(或从购物车中移除)并且最新版本不可用时,该物品被添加到旧版本(或从旧版本中移除),并且不同的版本稍后被协调处理。
为了提供这种保证,Dynamo 将每次修改的结果视为数据的新的不可变版本。它允许一个对象的多个版本同时出现在系统中。大多数情况下,新版本包含以前的版本,系统本身可以确定权威版本(语法协调)。但是,在出现故障和并发更新的情况下,可能会发生版本分支,从而导致对象的版本冲突。在这些情况下,系统无法协调同一对象的多个版本,客户端必须执行协调,以便将数据演化的多个分支折叠回一个分支(语义协调)。折叠操作的一个典型例子是 "合并" 客户购物车的不同版本。使用这种协调机制,"添加到购物车" 操作永远不会丢失。但是,删除的项目可能会重新出现。
重要的是要理解,某些故障模式可能会导致系统不仅有两个版本,而且有几个版本的相同数据。存在网络分区和节点故障时的更新可能会导致对象具有不同的版本,系统需要在将来对其进行协调。这要求我们设计明确承认同一数据的多个版本的可能性的应用程序(为了永远不会丢失任何更新)。
Dynamo 使用矢量时钟来捕捉同一对象不同版本之间的因果关系。向量时钟实际上是(node, counter) 对的列表。一个矢量时钟与每个对象的每个版本相关联。人们可以通过检查一个对象的矢量时钟来确定它的两个版本是在平行的分支上还是有因果关系。如果第一个对象时钟上的计数器小于或等于第二个时钟上的所有节点,那么第一个是第二个的祖先,可以被忘记。否则,这两个变化被认为是冲突的,需要和解。
在 Dynamo 中,当客户端希望更新一个对象时,它必须指定要更新哪个版本。这是通过传递它从先前的读取操作中获得的上下文来完成的,该上下文包含向量时钟信息。在处理一个读请求时,如果 Dynamo 访问了多个无法在语法上协调的分支,它将返回叶子处的所有对象,以及上下文中相应的版本信息。使用该上下文的更新被认为已经协调了不同的版本,并且分支被折叠成单个新版本。
为了说明矢量时钟的使用,让我们考虑图 3 所示的例子。客户端写入新对象。处理这个键的写操作的节点(比如Sx)增加了它的序列号,并使用它来创建数据的向量时钟。系统现在有了对象 D1 及其相关的时钟 [(Sx,1)]。客户端更新对象。假设同一个节点也处理这个请求。系统现在也有对象 D2 和它相关的时钟 [(Sx,2)]。D2 是 D1 的后裔,因此对 D1 写得过多,然而可能有 D1 的复制品在尚未见到 D2 的节点上徘徊。让我们假设同一个客户端再次更新对象,并且不同的服务器(比如Sy)处理该请求。系统现在有数据 D3 及其相关时钟[(Sx,2),(Sy,1)]。
接下来,假设一个不同的客户端读取 D2,然后尝试更新它,另一个节点(比如Sz)执行写入。系统现在有 D4(D2 的后裔),它的版本时钟是 [(Sx,2),(Sz,1)]。知道 D1 或 D2 的节点可以在接收到 D4 及其时钟时确定 D1 和 D2 被新数据覆盖并且可以被垃圾回收。知道 D3 并接收 D4 的节点会发现它们之间没有因果关系。换句话说,D3 和 D4 的变化没有相互反映。数据的两个版本都必须保存并呈现给客户端(在读取时),以便进行语义协调。
现在假设一些客户端读取 D3 和 D4(上下文将反映这两个值都是通过读取找到的)。读者的上下文是 D3 和 D4 时钟的摘要,即 [(Sx,2),(Sy,1),(Sz,1)]。如果客户端执行协调,并且节点 Sx 协调写入,Sx 将更新其在时钟中的序列号。新数据 D5 将有以下时钟:[(Sx,3),(Sy,1),(Sz,1)]。
向量时钟的一个可能问题是,如果许多服务器协调对对象的写入,向量时钟的大小可能会增加。实际上,这是不可能的,因为写入通常由首选项列表中的前 N 个节点之一处理。在网络分区或多个服务器出现故障的情况下,写请求可能由不在首选项列表前 N 个节点中的节点处理,导致矢量时钟的大小增加。在这些情况下,最好限制矢量时钟的大小。为此,Dynamo 采用了以下时钟截断方案:与每个 (node, counter) 对一起,Dynamo 存储一个时间戳,该时间戳指示节点上次更新数据项的时间。当向量时钟中的 (node, counter) 对的数量达到阈值(比如 10)时,从时钟中移除最老的对。显然,这种截断方案会导致协调效率低下,因为不能准确地导出后代关系。然而,这个问题在生产中没有出现,因此这个问题没有得到彻底调查分析。
Dynamo 中的任何存储节点都有资格接收任何 key 的客户端 get 和 put 操作。在本节中,为了简单起见,我们描述了如何在无故障环境中执行这些操作,在后续章节中,我们描述了如何在故障期间执行读写操作。
get 和 put 操作都是使用亚马逊的基础设施特定的请求处理框架通过 HTTP 协议调用的。客户端可以使用两种策略来选择节点:(1) 通过通用负载平衡器路由其请求,该负载平衡器将根据负载信息选择节点,或者 (2) 使用分区感知客户端库,该库将请求直接路由到适当的协调器节点。第一种方法的优点是客户端不必在其应用程序中链接任何特定于 Dynamo 的代码,而第二种策略可以实现更低的延迟,因为它跳过了潜在的转发步骤。
处理读或写操作的节点称为协调器。通常,这是首选项列表中前 N 个节点中的第一个。如果请求是通过负载均衡器接收的,则访问 key 的请求可以被路由到环中的任何随机节点。在这种情况下,如果接收请求的节点不在所请求 key 的首选项列表的前 N 名中,则该节点将不会协调它。相反,该节点会将请求转发给首选项列表中前 N 个节点中的第一个。读写操作涉及首选项列表中的前 N 个健康节点,跳过那些关闭或不可访问的节点。当所有节点都正常时,访问键的首选列表中的前 N 个节点。当存在节点故障或网络分区时,访问在首选项列表中排名较低的节点。
为了保持副本之间的一致性,Dynamo 使用了类似于使用法定人数系统(quorum systems)中使用的一致性协议。该协议有两个关键的可配置值:R 和 W。R 是成功读取操作必须参与的最小节点数。W 是成功写入操作必须参与的最小节点数。设置 R 和 W,使得 R + W > N 产生一个类似群体的系统。在这个模型中,get(或 put)操作的延迟由最慢的复制决定。因此,R 和 W 通常配置为小于 N,以提供更好的延迟。
当接收到一个 key 的 put() 请求时,协调器为新版本生成向量时钟,并在本地写入新版本。然后,协调器将新版本(连同新的矢量时钟)发送到 N 个排名最高的可到达节点。如果至少有 W-1 节点响应,则认为写入成功。
类似地,对于 get() 请求,协调器从该关键字的首选项列表中排名最高的 N 个可达节点请求该关键字的所有现有数据版本,然后在将结果返回给客户端之前等待 R 个响应。如果协调器最终收集了数据的多个版本,它会返回所有它认为不相关的版本。然后,不同的版本被协调,取代当前版本的协调版本被写回。
如果 Dynamo 使用传统的方法,它将在服务器故障和网络分区期间不可用,并且即使在最简单的故障条件下也会降低耐用性。为了弥补这一点,它没有强制执行严格的法定人数成员,而是使用了 "草率的法定人数";所有读和写操作都是在首选项列表中的前 N 个健康节点上执行的,这可能不总是在遍历一致性哈希环时遇到的前 N 个节点。
考虑图 2 中给出的 Dynamo 配置的例子,N = 3。在本例中,如果节点 A 在写入操作期间暂时关闭或不可访问,则通常位于节点 A 上的副本现在将被发送到节点 D。这样做是为了保持所需的可用性和持久性保证。发送到 D 的副本在其元数据中将有一个提示,提示哪个节点是副本的预期接收者(在本例中为 A)。收到提示副本的节点会将它们保存在一个单独的本地数据库中,并定期进行扫描。一旦检测到 A 已经恢复,D 将尝试将副本传送给 A。一旦传输成功,D 可以从其本地存储中删除该对象,而不会减少系统中副本的总数。
使用提示切换,Dynamo 确保读写操作不会因临时节点或网络故障而失败。需要最高级别可用性的应用程序可以将 W 设置为 1,这可以确保只要系统中的单个节点将 key 持久写入其本地存储,写入就可以被接受。因此,只有当系统中的所有节点都不可用时,写请求才会被拒绝。但实际上,生产中的亚马逊大部分服务都设置了较高的 W,以满足期望的耐用性水平。第 6 节将更详细地讨论配置 N、R 和 W。
高度可用的存储系统必须能够处理整个数据中心的故障。数据中心故障是由于断电、冷却故障、网络故障和自然灾害造成的。Dynamo 配置为每个对象在多个数据中心之间复制。本质上,key 的首选列表是这样构建的,即存储节点分布在多个数据中心。这些数据中心通过高速网络连接在一起。这种跨多个数据中心复制的方案允许我们在不中断数据的情况下处理整个数据中心的故障。
如果系统成员流失率较低并且节点故障是暂时的,则暗示切换效果最佳。在某些情况下,提示副本(hinted replicas)在返回到原始副本节点之前变得不可用。为了处理这种和其他对持久性的威胁,Dynamo 实现了一个反熵(anti-entropy)(副本同步)协议来保持副本的同步。
为了更快地检测副本之间的不一致,并最大限度地减少传输的数据量,Dynamo 使用了Merkle 树。Merkle 树是一种 hash 树,叶子是各个键的值的哈希。树中较高的父节点是它们各自子节点的 hash。Merkle 树的主要优点是可以独立检查树的每个分支,而不需要节点下载整个树或整个数据集。此外,Merkle 树有助于减少需要传输的数据量,同时检查副本之间的不一致性。例如,如果两棵树的根的哈希值相等,则树中叶节点的值相等,并且节点不需要同步。如果没有,则意味着某些副本的值不同。在这种情况下,节点可以交换子节点的哈希值,并且该过程继续进行,直到到达树叶,此时主机可以识别 "不同步" 的 key。Merkle 树最大限度地减少了同步所需传输的数据量,并减少了反熵过程中执行的磁盘读取次数。
Dynamo 使用 Merkle 树进行反熵,如下所示:每个节点为其托管的每个 key 范围(虚拟节点覆盖的密钥集)维护一个单独的 Merkle 树。这允许节点比较一个键范围内的键是否是最新的。在该方案中,两个节点交换对应于它们共同拥有的 key 范围的 Merkle 树的根。随后,使用上述树遍历方案,节点确定它们是否有任何差异,并执行适当的同步动作。该方案的缺点是,当节点加入或离开系统时,许多关键范围会改变,从而需要重新计算树。然而,这个问题是通过第 6.2 节中描述的细化分区方案来解决的。
在亚马逊的环境中,节点中断(由于故障和维护任务)通常是短暂的,但可能会持续很长时间。节点中断很少意味着永久脱离,因此不应导致分区分配的重新平衡或不可达副本的修复。类似地,手动错误可能会导致新 Dynamo 节点的意外启动。由于这些原因,使用显式机制来启动从 Dynamo 环添加和移除节点被认为是合适的。管理员使用命令行工具或浏览器连接到 Dynamo 节点,并发布成员资格更改,以将节点加入环或从环中删除节点。服务于该请求的节点将成员变更及其发布时间写入持久存储。成员关系的变化形成了一个历史,因为节点可以多次删除和添加回来。基于 gossip 协议传播成员关系的变化,并维护最终一致的成员关系视图。每个节点每秒钟都会联系一个随机选择的对等节点,这两个节点可以有效地协调它们持久的成员关系变化历史。
当一个节点第一次启动时,它选择它的令牌集(set of tokens)(一致性哈希空间中的虚拟节点),并将节点映射到它们各自的令牌集。映射保存在磁盘上,最初只包含本地节点和令牌集。存储在不同 Dynamo 节点上的映射在协调成员资格改变历史的相同通信交换期间被协调。因此,分区和放置信息也通过基于 gossip 协议传播,并且每个存储节点都知道其对等节点处理的令牌范围。这允许每个节点将 key 的读/写操作直接转发到正确的节点集。
上述机制可能暂时导致一个逻辑分区的 Dynamo 环。例如,管理员可以联系节点 A 将节点 A 加入环,然后联系节点 B 将节点 B 加入环。在这种情况下,节点 A 和节点 B 都认为自己是环中的一员,但两者都不会立即意识到对方。为了防止逻辑分区,一些 Dynamo 节点扮演种子的角色。种子是通过外部机制被发现,同时,所有节点都知道它的存在。因为所有节点最终都用一个种子来协调它们的成员关系,所以逻辑分区是极不可能的。种子可以从静态配置或配置服务中获得。种子通常是 Dynamo 环中功能齐全的节点。
Dynamo中的故障检测用于避免在 get() 和 put() 操作期间以及传输分区和提示副本时尝试与不可达的对等体通信。为了避免失败的通信尝试,故障检测的纯本地概念是完全足够的:如果节点 B 不响应节点 A 的消息(即使节点 B 响应节点 C 的消息),节点 A 可以认为节点 B 失败。在 Dynamo 环中产生节点间通信的客户端请求的稳定速率存在的情况下,当节点 B 未能响应消息时,节点 A 迅速发现节点 B 无响应;然后,节点 A 使用备用节点来服务映射到节点 B 的分区的请求;甲定期重试乙,以检查后者的恢复。在没有客户端请求来驱动两个节点之间的流量的情况下,两个节点都不需要知道另一个节点是否可达和响应。
去中心化故障检测协议使用简单的 gossip 协议,使系统中的每个节点能够了解其他节点的加入(或离开)。关于去中心化式故障检测器和影响其准确性的参数的详细信息,请感兴趣的读者参考。Dynamo 的早期设计使用去中心化的故障检测器来保持故障状态的全局一致性。后来确定,显式节点加入和离开方法消除了对故障状态全局视图的需要。这是因为节点通过显式节点加入和离开方法被通知永久节点添加和移除,并且当单个节点无法与其他节点通信时(在转发请求时),临时节点故障被检测到。
当一个新节点(比如说 X)被添加到系统中时,它会被分配一些令牌,这些令牌随机分布在环上。对于分配给节点 X 的每个密钥范围,可能有许多节点(小于或等于 N)当前负责处理属于其令牌范围内的密钥。由于将密钥范围分配给 X,一些现有的节点不再需要它们的一些密钥,这些节点将这些密钥传输给 X。让我们考虑一个简单的自举场景,其中节点 X 被添加到图 2 所示的 A 和 B 之间的环中。当 X 被添加到系统中时,它负责将密钥存储在范围(F,G]、(G,A] 和 (A,X] 中。因此,节点 B、C 和 D 不再需要存储这些各自范围内的密钥。因此,节点 B、C 和 D 将向 X 提供并在 X 确认后传输适当的密钥集。当一个节点从系统中移除时,密钥的重新分配以相反的过程发生。
运行经验表明,这种方法在存储节点之间均匀地分配密钥分配负载,这对于满足延迟要求和确保快速引导非常重要。最后,通过在源节点和目标节点之间添加一轮确认,可以确保目标节点在给定的密钥范围内不会接收到任何重复的传输。
在 Dynamo 中,每个存储节点都有三个主要的软件组件:请求协调、成员资格和故障检测以及本地持久性引擎。所有这些组件都是用 Java 实现的。
Dynamo 的本地持久组件允许插入不同的存储引擎。正在使用的引擎是 Berkeley 数据库(BDB)事务数据存储、BDB Java 版、MySQL 和一个带有持久后备存储的内存缓冲区。设计可插拔持久性组件的主要原因是选择最适合应用程序访问模式的存储引擎。例如,BDB 通常可以处理几十 kb 的对象,而 MySQL 可以处理更大的对象。应用程序根据其对象大小分布选择 Dynamo 的本地持久性引擎。Dynamo 的大多数生产实例使用 BDB 交易数据存储。
请求协调组件建立在事件驱动的消息传递基础之上,其中消息处理流水线被分成多个阶段,类似于 SEDA 架构。所有通信都是使用 Java NIO 通道实现的。协调器通过从一个或多个节点收集数据(在读取的情况下)或在一个或多个节点存储数据(用于写入)来代表客户端执行读取和写入请求。每个客户端请求都会导致在接收客户端请求的节点上创建一个状态机。状态机包含识别负责密钥的节点、发送请求、等待响应、可能进行重试、处理响应以及将响应打包到客户端的所有逻辑。每个状态机实例只处理一个客户端请求。例如,读操作实现以下状态机:(1) 向节点发送读请求,(2) 等待所需响应的最小数量,(3) 如果在给定的时间范围内收到太少的响应,则请求失败,(4) 否则收集所有数据版本并确定要返回的版本,以及(5) 如果启用了版本控制,则执行语法协调并生成包含所有剩余版本的矢量时钟的不透明写上下文。为了简洁起见,省略了故障处理和重试状态。
在读响应返回给调用方之后,状态机等待一小段时间以接收任何未完成的响应。如果在任何响应中返回了过时版本,协调器会用最新版本更新这些节点。这个过程被称为读取修复,因为它在一个时刻修复了错过最近更新的副本,并且解除了反熵协议必须做的事情。
如前所述,写请求由首选项列表中的前 N 个节点之一协调。尽管总是希望让前 N 个节点中的第一个节点来协调写入,从而在单个位置序列化所有写入,但这种方法导致了不均匀的负载分布,从而导致违反服务层协议。这是因为请求负载不是均匀分布在对象之间的。为了解决这个问题,首选项列表中的前 N 个节点中的任何一个都可以协调写入。特别地,由于每次写操作通常跟随读操作,所以写操作的协调器被选择为对存储在请求的上下文信息中的先前读操作做出最快响应的节点。这种优化使我们能够选择包含由之前的读取操作读取的数据的节点,从而增加了获得 "读-写" 一致性的机会。它还减少了请求处理性能的可变性,从而将性能提高到 99.9%。
Dynamo 被几种不同配置的服务使用。这些实例因其版本协调逻辑和读/写仲裁特征而不同。以下是 Dynamo 使用的主要模式:
业务逻辑特定的协调:这是 Dynamo 的一个常见用例。每个数据对象跨多个节点复制。在不同版本的情况下,客户端应用程序执行自己的协调逻辑。前面讨论的购物车服务就是这类服务的一个典型例子。它的业务逻辑通过合并客户购物车的不同版本来协调对象。
基于时间戳的协调:这种情况与前一种情况的不同之处仅在于协调机制。在不同版本的情况下,Dynamo 执行简单的基于时间戳的 "最后写入获胜" 的协调逻辑;即具有最大物理时间戳值的对象被选为正确的版本。维护客户会话信息的服务是使用这种模式的服务的一个很好的例子。
高性能读取引擎:虽然 Dynamo 被构建为一个 "始终可写" 的数据存储,但一些服务正在调整其仲裁特征,并将其用作高性能读取引擎。通常,这些服务的读取请求率很高,只有少量更新。在这种配置中,通常将 R 设置为 1,将 W 设置为 N。对于这些服务,Dynamo 提供了跨多个节点分区和复制数据的能力,从而提供了增量可扩展性。其中一些实例充当存储在更大权重后备存储中的数据的权威持久性缓存。维护产品目录和促销项目的服务属于这一类。
Dynamo 的主要优势是其客户端应用程序可以调整 N、R 和 W 的值,以实现所需的性能、可用性和耐用性水平。例如,N 的值决定了每个物体的耐久性。Dynamo 的用户使用的一个典型的 N 值是 3。
W 和 R 的值影响对象的可用性、持久性和一致性。例如,如果 W 设置为 1,则只要系统中至少有一个节点可以成功处理写请求,系统就永远不会拒绝写请求。然而,较低的 W 和 R 值会增加不一致的风险,因为写请求被认为是成功的,并返回给客户端,即使它们没有被大多数副本处理。这还会在写入请求成功返回到客户端时引入一个持久性漏洞窗口,即使它只在少数节点上保持不变(This also introduces a vulnerability window for durability when a write request is successfully returned to the client even though it has been persisted at only a small number of nodes)。
传统方法认为耐用性和可用性是相辅相成的。然而,这里不一定是这样。例如,可以通过增加 W 来减少持久性的漏洞窗口。这可能会增加拒绝请求的概率(从而降低可用性),因为需要更多的存储主机处于活动状态才能处理写请求。Dynamo 的几个实例使用的常见配置 (N,R,W) 是(3,2,2)。选择这些值是为了满足必要的性能、持久性、一致性和可用性级别协议。
本节中介绍的所有测量都是在运行配置为(3,2,2)的实时系统上进行的,该系统运行数百个具有相同硬件配置的节点。如前所述,Dynamo 的每个实例都包含位于多个数据中心的节点。这些数据中心通常通过高速网络链接连接。回想一下,要生成成功的 get(或 put)响应,节点需要响应协调器。显然,数据中心之间的网络延迟会影响响应时间,节点(及其数据中心位置)的选择会满足应用程序的目标服务层协议。
虽然 Dynamo 的主要设计目标是建立一个高度可用的数据存储,但性能在亚马逊的平台上是一个同样重要的标准。如前所述,为了提供一致的客户体验,亚马逊的服务将性能目标设置在较高的百分点(如 99.9% 或 99.99%)。使用 Dynamo 的服务需要的典型服务层协议是 99.9% 的读写请求在 300 毫秒内执行。
由于 Dynamo 运行在标准的商用硬件组件上,这些组件的输入/输出吞吐量远远低于高端企业服务器,因此为读写操作提供一致的高性能是一项非常重要的任务。多个存储节点参与读写操作使其更具挑战性,因为这些操作的性能受到最慢的读写副本的限制。图 4 显示了 30 天内 Dynamo 读写操作的平均延迟和 99.9% 延迟。如图所示,延迟呈现出明显的昼夜模式,这是传入请求速率的昼夜模式的结果(即,白天和夜晚之间的请求速率存在显著差异)。此外,写延迟明显高于读延迟,因为写操作总是导致磁盘访问。还有,99.9% 的延迟约为 200 毫秒,比平均值高一个数量级。这是因为 99.9% 的延迟受几个因素的影响,如请求负载的可变性、对象大小和位置模式。
虽然这种性能水平对于许多服务来说是可以接受的,但是一些面向客户的服务需要更高的性能水平。对于这些服务,Dynamo 提供了权衡耐用性保证和性能的能力。在优化中,每个存储节点在其内存中维护一个对象缓冲区。每个写操作都存储在缓冲区中,并由写线程定期写入存储。在该方案中,读取操作首先检查所请求的密钥是否存在于缓冲区中。如果是这样,则从缓冲区而不是存储引擎中读取对象。
这种优化使得 99.9% 的延迟在高峰流量期间降低了 5 倍,即使是一千个对象的非常小的缓冲区也是如此(参见图 5)。此外,如图所示,写缓冲可以消除较高百分比的延迟。显然,这个方案是以性能换取耐用性的。在这种方案中,服务器崩溃会导致缓冲区中排队的写入丢失。为了降低持久性风险,写入操作被细化为让协调器从 N 个副本中选择一个来执行 "持久性写入"。因为协调器只等待 W 响应,所以写操作的性能不受单个副本执行的持久写操作的性能的影响。
Dynamo 使用一致的哈希算法在副本之间划分其 key 空间,并确保均匀的负载分布。假设 key 的访问分布不是高度倾斜的,统一的 key 分布可以帮助我们实现统一的负载分布。具体来说,Dynamo 的设计假设即使在访问分布中存在明显的偏斜,在分布的流行端也有足够的 key,以便处理热点 key 的负载可以通过分区均匀地分布在节点上。本节讨论了 Dynamo 中的负载不平衡以及不同分区策略对负载分布的影响。
为了研究负载不平衡及其与请求负载的相关性,对每个节点接收的请求总数进行了 24 小时的测量,细分为 30 分钟的间隔。在给定的时间窗口内,如果节点的请求负载偏离平均负载的值小于某个阈值(此处为 15%),则该节点被视为 "不平衡"。否则,该节点被视为 "失衡"。图 6 显示了这段时间内 "失衡"(以下称为 "失衡比率")的节点比例。作为参考,整个系统在此期间接收到的相应请求负载也被绘制出来。如图所示,不平衡率随着负载的增加而降低。例如,在低负载期间,不平衡率高达 20%,而在高负载期间,不平衡率接近 10%。直观地说,这可以用以下事实来解释:在高负载下,大量热点 key 被访问,并且由于密钥的均匀分布,负载被均匀分布。然而,在低负载(负载是测量的峰值负载的 1/8)期间,访问的热点 key 较少,从而导致更高的负载不平衡。本节讨论了 Dynamo 的分区方案是如何随着时间的推移而演变的,以及它对负载分布的影响。
策略1:每个节点测试随机令牌,并根据令牌值进行分区:这是在生产中部署的初始策略(在第 4.2 节中有所描述)。在这个方案中,每个节点都被分配了 T 个令牌(从散列空间中统一随机选择)。所有节点的标记根据它们在哈希空间中的值进行排序。每两个连续的标记定义一个范围。最后一个标记和第一个标记形成一个范围,该范围在散列空间中从最高值到最低值 "环绕"。因为令牌是随机选择的,所以范围大小不一。随着节点加入和离开系统,令牌集会发生变化,因此范围也会发生变化。请注意,维护每个节点的成员资格所需的空间随着系统中节点数量的增加而线性增加。
使用此策略时,遇到了以下问题。首先,当一个新节点加入系统时,它需要从其他节点 "窃取" 它的密钥范围。然而,将密钥范围移交给新节点的节点必须扫描它们的本地持久性存储来检索适当的数据项集合。请注意,在生产节点上执行这样的扫描操作很棘手,因为扫描是高度资源密集型操作,并且需要在后台执行,而不会影响客户性能。这要求我们以最低优先级运行引导任务。然而,这大大降低了引导过程,在繁忙的购物季节,当节点每天处理数百万个请求时,引导几乎需要一天才能完成。第二,当一个节点加入/离开系统时,由许多节点处理的 key 范围发生变化,并且需要重新计算新范围的 Merkle 树,这是一个在生产系统上执行的非常重要的操作。最后,由于密钥范围的随机性,没有简单的方法来拍摄整个密钥空间的快照,这使得存档过程变得复杂。在这个方案中,归档整个密钥空间需要我们分别从每个节点检索密钥,这是非常低效的。
这种策略的根本问题是数据分区和数据放置的方案是相互交织的。例如,在某些情况下,为了处理请求负载的增加,最好向系统添加更多的节点。但是,在这种情况下,不可能在不影响数据分区的情况下添加节点。理想情况下,最好使用独立的分区和放置方案。为此,评估了以下战略:
策略2:每个节点 T 个随机令牌和相等大小的分区:在这个策略中,散列空间被分成 Q 个相等大小的分区/范围,每个节点被分配 T 个随机令牌。Q 通常设置为 Q >> N 和 Q >> S*T,其中 S 是系统中的节点数。在这种策略中,令牌仅用于构建将散列空间中的值映射到有序节点列表的函数,而不是决定分区。一个分区被放置在从该分区的末端顺时针遍历一致性哈希环时遇到的前 N 个唯一节点上。图 7 说明了 N = 3 的策略。在这个例子中,节点 A、B、C 在从包含密钥 k1 的分区的末端遍历环时遇到。这种策略的主要优点是:(1) 分区和分区放置的解耦,以及 (2) 能够在运行时更改放置方案。
策略3:每个节点 Q/S 令牌,大小相等的分区:与策略 2 类似,该策略将哈希空间划分为Q 个大小相等的分区,分区的放置与分区方案解耦。此外,每个节点都被分配了 Q/S 令牌,其中 S 是系统中的节点数。当一个节点离开系统时,它的令牌被随机分配给剩余的节点,这样这些属性被保留。类似地,当一个节点加入系统时,它会以保留这些属性的方式从系统中的节点 "窃取" 令牌。
对于一个 S = 30,N = 3 的系统,对这三种策略的效率进行了评估。然而,公平地比较这些不同的策略是困难的,因为不同的策略有不同的配置来调整它们的效率。例如,策略 1 的负载分布属性取决于令牌的数量(即 T),而策略 3 取决于分区的数量(即 Q)。比较这些策略的一个公平的方法是评估负载分布的偏差,而所有策略都使用相同的空间来维护其成员信息。例如,在策略 1 中,每个节点需要维护环中所有节点的令牌位置,而在策略 3 中,每个节点需要维护关于分配给每个节点的分区的信息。
在我们的下一个实验中,通过改变相关参数(T 和 Q)来评估这些策略。每种策略的负载平衡效率是针对每个节点需要维护的不同大小的成员信息来衡量的,其中负载平衡效率定义为每个节点服务的平均请求数与最热节点服务的最大请求数之比。
结果如图 8 所示。如图所示,策略 3 实现了最好的负载平衡效率,而策略 2 的负载平衡效率最差。在很短的时间内,策略 2 充当了从使用策略 1 迁移到策略 3 的过程中的临时设置。与策略 1 相比,策略 3 实现了更高的效率,并将每个节点上维护的成员信息的大小减少了三个数量级。虽然存储不是主要问题,但是节点周期性地散布成员信息,因此希望尽可能紧凑地保存该信息。除此之外,由于以下原因,策略 3 是有利的,并且部署更简单:(1) 更快的引导/恢复:由于分区范围是固定的,它们可以存储在单独的文件中,这意味着可以通过简单地传输文件来将分区作为一个单元重新定位(避免定位特定项目所需的随机访问)。这简化了引导和恢复过程。(2) 易于存档:数据集的定期存档是大多数亚马逊存储服务的强制性要求。在策略 3 中,归档由 Dynamo 存储的整个数据集更简单,因为分区文件可以单独归档。相比之下,在策略 1 中,令牌是随机选择的,并且将存储在 Dynamo 中的数据存档需要分别从各个节点检索密钥,这通常是低效和缓慢的。策略 3 的缺点是改变节点成员需要协调,以保持分配所需的属性。
如前所述,Dynamo 旨在权衡一致性和可用性。为了了解不同故障对一致性的精确影响,需要多种因素的详细数据:停机时间、故障类型、组件可靠性、工作负载等。详细介绍这些数字超出了本文的范围。但是,本节讨论了一个很好的总结指标:在实际生产环境中应用程序所看到的不同版本的数量。
数据项的不同版本出现在两种情况下。第一种是当系统面临诸如节点故障、数据中心故障和网络分区等故障情况时。第二种情况是,系统正在处理大量并发写入单个数据项的操作,而多个节点最终会同时协调更新。从可用性和效率的角度来看,最好在任何给定的时间将不同版本的数量保持在尽可能低的水平。如果版本不能单独基于向量时钟进行语法协调,它们必须被传递给业务逻辑进行语义协调。语义协调会给服务带来额外的负载,因此最好尽量减少对它的需求。
在我们的下一个实验中,返回购物车服务的版本数量被分析了 24 小时。在此期间,99.94%的请求只看到一个版本;0.00057% 的请求看到 2 个版本;0.00047% 的请求看到了 3 个版本,0.00009% 的请求看到了 4 个版本。这说明发散版本很少被创造出来(This shows that divergent versions are created rarely)。
经验表明,不同版本数量的增加不是由失败造成的,而是由于并发编写器数量的增加。并发写入数量的增加通常是由忙碌的机器人(自动化客户端程序)触发的,很少是由人类触发的。由于故事的敏感性,这个问题没有详细讨论。
如第 5 节所述,Dynamo 有一个请求协调组件,它使用状态机来处理传入的请求。负载平衡器将客户端请求统一分配给环中的节点。任何 Dynamo 节点都可以作为读取请求的协调者。另一方面,写请求将由键的当前首选项列表中的一个节点协调。这种限制是由于这些优选节点具有创建新版本标记的额外责任,该新版本标记必然包含由写请求更新的版本。请注意,如果 Dynamo 的版本方案基于物理时间戳,任何节点都可以协调写请求。
请求协调的另一种方法是将状态机移动到客户端节点。在这个方案中,客户端应用程序使用一个库在本地执行请求协调。客户端定期选择一个随机的 Dynamo 节点,并下载其 Dynamo 成员状态的当前视图。使用这些信息,客户端可以确定哪组节点构成了任何给定键的首选列表。读取请求可以在客户端节点处被协调,从而避免了额外的网络跳跃,如果请求被负载均衡器分配给一个随机的 Dynamo 节点,则会产生额外的网络跳跃。如果 Dynamo 使用基于时间戳的版本控制,则写操作要么转发到键的首选项列表中的节点,要么可以在本地进行协调。
客户端驱动的协调方法的一个重要优点是不再需要负载平衡器来统一分配客户端负载。通过将密钥近乎统一地分配给存储节点,公平的负载分布得到了隐含的保证。显然,这个方案的效率取决于客户端的成员信息有多新。目前,客户端每 10 秒钟轮询一个随机的 Dynamo 节点以获取成员更新。基于 pull 的方法比基于 push 的方法更受欢迎,因为前者可以更好地适应大量客户端,并且需要在服务器上维护非常少的客户端状态。但是,在最坏的情况下,客户端可能会暴露于过期成员身份达 10 秒钟。在这种情况下,如果客户端检测到其成员资格表过时(例如,当某些成员不可访问时),它将立即刷新其成员资格信息。
表 2 显示了与服务器驱动的方法相比,使用客户端驱动的协调在 24 小时内观察到的99.9% 的延迟改善和平均值。如表中所示,对于 99.9% 的延迟,客户端驱动的协调方法将延迟减少了至少 30 毫秒,并将平均值减少了 3 到 4 毫秒。延迟的改善是因为客户端驱动的方法消除了负载平衡器的开销和当请求被分配给随机节点时可能产生的额外网络跳。如表中所示,平均潜伏期往往明显低于 99.9% 的潜伏期。这是因为 Dynamo 的存储引擎缓存和写缓冲区有很好的命中率。此外,由于负载平衡器和网络会给响应时间带来额外的可变性,因此 99.9% 的响应时间增益高于平均值。
除了正常的前台 put/get 操作之外,每个节点还执行不同种类的后台任务,用于副本同步和数据切换(由于提示或添加/移除节点)。在早期的生产环境中,这些后台任务触发了资源争用问题,并影响了常规 put 和 get 操作的性能。因此,有必要确保后台任务仅在常规关键操作未受到显著影响时运行。为此,后台任务与准入控制机制相结合。每个后台任务都使用这个控制器来保留资源(例如数据库)的运行时间片,在所有后台任务之间共享。基于前台任务的监控性能的反馈机制被用来改变可用于后台任务的切片的数量。
准入控制器在执行 "前台" put/get 操作时,不断监视资源访问的行为。受监控的方面包括磁盘操作的延迟、由于锁争用和事务超时导致的数据库访问失败以及请求队列等待时间。此信息用于检查给定跟踪时间窗口中延迟(或失败)的百分比是否接近所需的阈值。例如,后台控制器检查第 99% 数据库读取延迟(过去 60 秒内)与预设阈值(比如 50 毫秒)的接近程度。控制器使用这种比较来评估前台操作的资源可用性。随后,它决定多少时间片可用于后台任务,从而使用反馈循环来限制后台活动的侵入性。
本节总结了 Dynamo 在实施和维护过程中获得的一些经验。在过去的两年里,亚马逊的许多内部服务都使用了 Dynamo,并且为其应用程序提供了很高的可用性。特别是,应用程序 99.9995% 的请求都收到了成功的响应(没有超时),迄今为止没有发生数据丢失事件。
此外,Dynamo 的主要优势是它提供了必要的旋钮,使用三个参数来根据他们的需要调整他们的实例。与流行的商业数据存储不同,Dynamo 向开发人员公开了数据一致性和协调逻辑问题。一开始,人们可能会认为应用程序逻辑会变得更加复杂。然而,从历史上看,亚马逊的平台是为高可用性而构建的,许多应用程序都是为处理不同的故障模式和可能出现的不一致而设计的。因此,移植这样的应用程序来使用 Dynamo 是一个相对简单的任务。对于希望使用 Dynamo 的新应用程序,在开发的初始阶段需要进行一些分析,以选择合适的冲突解决机制来适当地满足业务案例。最后,Dynamo 采用了完全成员模型,其中每个节点都知道其对等体托管的数据。为此,每个节点主动与系统中的其他节点共享完整的路由表。该模型适用于包含数百个节点的系统。然而,将这种设计扩展为运行数万个节点并不容易,因为维护路由表的开销会随着系统大小的增加而增加。这个限制可以通过在 Dynamo 中引入分层扩展来克服。此外,请注意,这个问题由 O(1) DHT系统主动解决。
本文描述了一个高度可用和可扩展的数据存储库 Dynamo,用于存储亚马逊电子商务平台的一些核心服务的状态。Dynamo 提供了所需的可用性和性能水平,并成功地处理了服务器故障、数据中心故障和网络分区。Dynamo 具有增量可伸缩性,允许服务所有者根据其当前的请求负载进行上下伸缩。Dynamo 允许服务所有者通过调整参数 N、R 和 W 来定制他们的存储系统,以满足他们期望的性能、耐用性和一致性服务层协议。
Dynamo 在过去一年的生产使用表明,去中心化技术可以结合起来提供一个单一的高可用性系统。它在最具挑战性的应用程序环境中的成功表明,最终一致的存储系统可以成为高可用性应用程序的构建模块