Lec 15 DynamoDB
在数据库系统课程中,学习了它的前身Dynamo,实现了服务的高可用性,但是为了弹性的需求,出现了SimpleDB(Amazon S3),当时由于其限制,结合两个数据库的优先设计和实践,推出了Amazon DynamoDB。
阅读资料
思考如下问题:
为了避免干扰,如果某个客户的应用程序超过其读取或写入容量,DynamoDB 可能会对其进行限流,这可能导致应用程序不可用。那么,DynamoDB 如何避免数据库表中的单个热点分区导致应用程序不可用呢?
论文阅读:Amazon DynamoDB
摘要
Amazon DynamoDB 是一个 NoSQL 云数据库服务,提供在任何规模下都一致的性能。数十万客户依赖 DynamoDB 的基本特性:一致的性能、可用性、持久性以及自托管的无服务器体验。在 2021 年的 66 小时 Amazon Prime Day 购物活动期间,Amazon 系统——包括 Alexa、Amazon.com 网站和 Amazon 配送中心——向 DynamoDB 发出了数万亿次 API 调用,峰值达到了每秒 8920 万次请求,同时保持单数字毫秒级别的高可用性。
可靠性至关重要,因为即使是最轻微的中断也会对客户产生重大影响。本文介绍了我们在大规模运营 DynamoDB 的经验,以及架构如何持续演进以满足客户工作负载不断增长的需求。
1. 引言
DynamoDB 是一个基础 AWS 服务,服务于成千上万的客户,使用分布在全球数据中心的海量服务器。DynamoDB 支持多个高流量的 Amazon 服务和系统,包括 Alexa、Amazon.com 网站以及所有 Amazon 配送中心。此外,许多 AWS 服务,如 AWS Lambda、AWS Lake Formation 和 Amazon SageMaker,都是基于 DynamoDB 构建的,此外还有成千上万的客户应用程序。

DynamoDB 的用户依赖其在低延迟下处理请求的能力。对于 DynamoDB 客户而言,一致的性能比中位数请求服务时间更为重要,因为意外的高延迟请求可能会通过依赖 DynamoDB 的应用程序层放大,从而导致不良的客户体验。DynamoDB 设计的目标是以低单数字毫秒的延迟完成所有请求。此外,使用 DynamoDB 的大规模且多样化的客户群体依赖不断扩展的功能集,如图 1 所示。随着 DynamoDB 在过去十年的演进,一个关键挑战是如何在不影响运营要求的情况下添加新功能。为了惠及客户和应用开发者,DynamoDB 独特地整合了以下六个基本系统属性:
- DynamoDB 是一个完全托管的云服务。通过 DynamoDB API,应用程序可以创建表并读取和写入数据,而无需关注这些表的存储位置或管理方式。DynamoDB 让开发者摆脱了修补软件、管理硬件、配置分布式数据库集群以及管理持续集群操作的负担。DynamoDB 处理资源配置、自动从故障中恢复、加密数据、管理软件升级、执行备份以及完成作为完全托管服务所需的其他任务。
- DynamoDB 采用多租户架构。DynamoDB 在相同的物理机器上存储来自不同客户的数据,以确保资源的高利用率,从而将节省的成本传递给客户。资源预留、紧密配置和监控使用提供了共同表格工作负载之间的隔离。
- DynamoDB 为表提供无界规模。每个表可以存储的数据量没有预定义的限制。表会根据客户应用程序的需求弹性增长。DynamoDB 旨在根据需要将分配给表的资源从几个服务器扩展到成千上万台服务器。当数据存储量和吞吐量需求增长时,DynamoDB 会将应用程序的数据分布到更多的服务器上。
- DynamoDB 提供可预测的性能。简单的 DynamoDB API(包括 GetItem 和 PutItem 操作)使其能够以一致的低延迟响应请求。在与数据位于同一 AWS 区域的应用程序中,通常可以看到 1 KB item服务端的平均延迟在低单数字毫秒范围内。最重要的是,DynamoDB 的延迟是可预测的。即使表的大小从几兆字节增长到数百太字节,由于 DynamoDB 数据分布和请求路由算法的分布式特性,延迟仍然保持稳定。DynamoDB 通过水平扩展处理任何级别的流量,并自动分区和重新分区数据,以满足应用程序的 I/O 性能需求。
- DynamoDB 是高可用的。DynamoDB 在多个数据中心(AWS 中称为可用区)之间复制数据,并在磁盘或节点故障时自动重新复制,以满足严格的可用性和持久性要求。客户还可以创建全球表,这些表在选定的区域之间进行地理复制,提供灾难恢复,并确保从任何地方访问的低延迟。DynamoDB 提供 99.99% 的可用性 SLA(对于常规表)和 99.999% 的可用性 SLA(对于全球表,其中 DynamoDB 在多个 AWS 区域之间复制表)。
- DynamoDB 支持灵活的使用场景。DynamoDB 不强制开发者采用特定的数据模型或一致性模型。DynamoDB 表没有固定schema,而是允许每个数据项包含任意数量的具有不同类型的属性,包括多值属性。表使用KV或文档数据模型。开发者可以在读取表中的项时请求强一致性或最终一致性。
在本文中,我们描述了 DynamoDB 如何作为一个分布式数据库服务发展,以满足客户的需求,同时保持其向每个使用多租户架构的客户提供单租户体验的关键特性。本文解释了系统面临的挑战以及服务如何应对这些挑战。本文总结了我们多年来的经验教训:
- 适应客户的流量模式,通过重塑数据库表的物理分区方案来改善客户体验。
- 持续验证静态数据是保护硬件故障和软件漏洞的可靠方式,以满足高耐久性目标。
- 在系统演变过程中保持高可用性需要谨慎的操作纪律和工具。正式的算法证明、游戏日(混乱和负载测试)、升级/降级测试和部署安全机制提供了在不担心妥协正确性的情况下安全调整和实验代码的自由。
- 设计系统时优先考虑可预测性而非绝对效率,有助于提高系统稳定性。虽然缓存等组件可以提升性能,但不应让它们掩盖在没有缓存时需要执行的工作,确保系统始终能够应对突发情况。
本文的结构如下:第 2 节扩展了 DynamoDB 的历史并解释了其起源,源自最初的 Dynamo 系统;第 3 节介绍了 DynamoDB 的架构概述;第 4 节介绍了 DynamoDB 从预配置表到按需表的演变;第 5 节介绍了 DynamoDB 如何确保强耐久性;第 6 节描述了可用性挑战及其解决方案;第 7 节提供了基于 Yahoo! 云服务基准(YCSB)的实验结果;第 8 节总结了本文。
2. 历史
DynamoDB 的设计源于我们在其前身 Dynamo 的经验。Dynamo 是 Amazon 开发的第一个 NoSQL 数据库系统,旨在满足购物车数据对高度可扩展、可用和持久的键值数据库的需求。
高可用性是数据库服务的一个关键属性,因为任何停机会影响依赖于该数据的客户。另一个关键需求是提供可预测的性能,以便应用能够为用户提供一致的体验。为实现这些目标,Amazon 在设计 Dynamo 时从基本原则出发。Dynamo 的应用逐渐扩大,因为它是唯一一个在规模上提供高可靠性的数据库服务。然而,Dynamo 仍然保留了自我管理( self-managed)大型数据库系统的操作复杂性。它是一个单租户系统,各团队需自行管理其 Dynamo 实例,因而需要对数据库服务的各个部分非常精通。这种操作复杂性成为其推广的一大障碍。
与此同时,Amazon 推出了新的服务(如 Amazon S3 和 Amazon SimpleDB),这些服务专注于托管和弹性体验,从而减轻了操作负担。虽然 Dynamo 的功能通常更契合应用需求,但 Amazon 的工程师更倾向于使用这些服务,因为它们免去了管理数据库的复杂性,让开发人员可以专注于应用本身。
Amazon 的首个数据库即服务是 SimpleDB,这是一种完全托管的弹性 NoSQL 数据库服务。SimpleDB 提供了多数据中心复制、高可用性和高持久性,而无需客户设置、配置或修补数据库。与 Dynamo 一样,SimpleDB 也提供了一个简单的表接口和受限的查询集,为许多开发人员提供了构建基础。然而,SimpleDB 存在一些限制。一个限制是表的存储容量较小(10GB)且请求吞吐量有限。另一个限制是不可预测的查询和写入延迟,这是因为所有表的属性都被索引,每次写入都需要更新索引。这些限制为开发人员带来了新的操作负担,他们不得不将数据分散到多个表中以满足应用的存储和吞吐量需求。
我们意识到,消除 SimpleDB 的限制并提供具有可预测性能的可扩展 NoSQL 数据库服务,不能通过 SimpleDB 的 API 实现。我们得出的解决方案是结合 Dynamo 的最佳设计(如增量扩展性和可预测的高性能)与 SimpleDB 的最佳特性(如云服务的易于管理、一致性,以及比单纯键值存储更丰富的表数据模型)。经过多次架构讨论,我们最终推出了 Amazon DynamoDB,这是一项于 2012 年上线的公共服务,虽然与之前的 Dynamo 系统共享相同的名称,但架构完全不同。DynamoDB 集结了我们从构建大规模非关系型数据库以及 AWS 高度可扩展和可靠云计算服务中所学到的所有经验。
3. 架构
DynamoDB 表是由多个items组成的,每个item是由一组属性(attributes)构成的。每个item通过主键(primary key)唯一标识。主键的schema在创建表时指定,包含了一个分区键或者是分区+排序的复合键。分区键的值始终作为输入传递给内部哈希函数。哈希函数的输出和排序键的值(如果存在)共同决定item的存储位置。DynamoDB 还支持二级索引。
DynamoDB 提供了一个简单的接口,用于存储或检索表或索引中的item。

表 1 列出了 DynamoDB 表中可供客户端执行的主要操作。任何操作都可以指定一个条件,只有满足该条件时,操作才能成功。DynamoDB 支持 ACID 事务。
DynamoDB 表有多个分片,以处理表的吞吐量和存储需求。每个分区存储表的一个不重叠且连续的键范围。每个分片有多个副本,分布在不同的AZ中,以确保高可用性和持久性。这些副本形成一个复制组(replication group)。复制组使用 Multi-Paxos 进行Leader选举和一致性协议,新的领导副本在旧领导副本的租约到期之前,不会处理任何写操作或一致性读取
在收到写请求时,复制组的Leader副本会生成WAL记录,并将其发送给其他副本(peer)。写操作一旦达到多数副本并且它们将日志记录持久化到本地日志中,操作就会向应用确认。DynamoDB 支持强一致性和最终一致性读取。复制组的任何副本都可以处理最终一致性读取。
复制组由存储副本组成,这些副本既包含WAL,也存储用于键值数据的 B-Tree,如图 2 所示。为了提高可用性和持久性,复制组还可以包含仅持久化最近WAL条目的副本,如图 3 所示。这些副本称为日志副本(log replicas),类似于 Paxos 中的Acceptors。日志副本不存储键值数据。第 5 和第 6 节讨论了日志副本如何帮助 DynamoDB 提高可用性和持久性。

DynamoDB 由数十个微服务组成。DynamoDB 的一些核心服务包括元数据服务(metadata service)、请求路由服务(request routing service)、存储节点(storage nodes)和自动管理服务(autoadmin service),如图 4 所示。元数据服务存储有关表、索引和键的路由信息。请求路由服务负责授权、认证并将每个请求路由到相应的服务器。例如,所有读取和更新请求都路由到存储客户数据的存储节点。请求路由器从元数据服务中查找路由信息。所有资源创建、更新和数据定义请求都路由到自动管理服务。存储服务负责在存储节点集群中存储客户数据,每个存储节点托管不同分区的多个副本。

自动管理服务作为 DynamoDB 的大脑,负责监控整个集群的健康状况、分区健康状况、表的扩展和执行所有控制平面请求。该服务持续监控所有分区的健康状态,并替换任何被认为不健康的副本(如缓慢响应、无响应或硬件故障)。服务还会执行对 DynamoDB 所有核心组件的健康检查,替换任何故障或正在故障的硬件。例如,如果自动管理服务检测到某个存储节点不健康,它会启动恢复过程,替换该节点上的副本,确保系统恢复到稳定状态。
图 4 中未展示的其他 DynamoDB 服务支持如时间点恢复(point-in-time restore)、按需备份(on-demand backups)、更新流(update streams)、全局表(global tables)、全局二级索引(global secondary indices)和事务等功能。
4. 从预置模式到按需模式的演进
当 DynamoDB 发布时,我们引入了一种名为“分区”(partitions)的内部抽象,用于动态扩展表的容量和性能。在最初的 DynamoDB 版本中,用户需要明确指定表所需的吞吐量,具体以读取容量单元(RCU)和写入容量单元(WCU)来表示。对于大小最多为 4 KB 的item,一个 RCU ,每秒可以执行一次强一致性读取请求。对于大小最多为 1 KB 的item,一个 WCU ,每秒可以执行一次标准写入请求。这些 RCU 和 WCU 合称为“预置吞吐量”(provisioned throughput)。最初的系统将表拆分为多个分区,使其内容能够分布在多个存储节点上,以利用这些节点的存储空间和性能。当表的需求发生变化(例如表的规模增长或负载增加)时,分区可以进一步拆分或迁移,从而支持表的弹性扩展。分区这一抽象被证明非常有价值,并且仍然是 DynamoDB 设计的核心。然而,早期版本中容量和性能的分配与具体分区紧密耦合,这也带来了挑战。
DynamoDB 使用“请求控制”(admission control)来确保存储节点不会过载,以避免共同驻留的表分区之间的干扰,同时实施用户请求的吞吐量限制。在过去十年中,DynamoDB 的请求控制机制不断演化。早期的一个表请求控制由这个表的所有存储节点共同负责。这些存储节点根据分配其本地存储分区的独立执行请求控制。由于一个存储节点可能承载多个表的分区,因此每个分区的分配吞吐量被用来隔离各个工作负载。DynamoDB 对单个分区的最大吞吐量设置了上限,并确保由一个存储节点承载的所有分区的总吞吐量不会超过该节点物理存储设备允许的最大吞吐量。
当表的整体吞吐量变化或分区被拆分为子分区时,分区的吞吐量分配会相应调整。当分区因大小拆分时,父分区的分配吞吐量会平均分配给子分区。当分区因吞吐量拆分时,新分区的吞吐量分配基于表的预置吞吐量。例如,假设一个分区最多可承载 1000 WCU 的预置吞吐量。当一个表的初始预置吞吐量为 3200 WCU 时,DynamoDB 会创建四个分区,每个分区分配 800 WCU。如果将表的预置吞吐量增加到 3600 WCU,则每个分区的容量增加到 900 WCU。如果将预置吞吐量增加到 6000 WCU,则分区会被拆分成八个子分区,每个分区分配 750 WCU。如果表的容量减少到 5000 WCU,则每个分区的容量减少到 675 WCU。
这种基于分区的吞吐量均匀分布假设应用程序会均匀访问表中的键,并且分区因大小拆分时其性能也能平均分配。然而,我们发现应用程序工作负载往往在时间和键范围上表现出非均匀的访问模式。当表中的请求速率不均匀时,分区拆分并按比例分配性能可能导致热点分区的可用性能比拆分前更少。由于吞吐量是静态分配的,并且在分区级别强制执行,这种非均匀工作负载偶尔会导致应用程序的读取和写入请求被拒绝(称为“限流”或 throttling),即使表的总预置吞吐量足以满足其需求。
应用程序面临的两个最常见的挑战是“热点分区”(hot partitions)和“吞吐量稀释”(throughput dilution)。热点分区出现在应用程序流量始终集中于表的少数几个item时。这些热点item可能长期属于某些固定的分区,也可能在不同分区之间跳跃。而吞吐量稀释通常发生在因大小拆分的表中。当分区因大小拆分时,分区的吞吐量会平均分配到新创建的子分区,因此每个分区的吞吐量会减少。
在这两种情况下,从用户的角度来看,限流导致其应用程序经历一段时间的不可用性,尽管服务的行为符合预期。经历限流的用户通常通过增加表的预置吞吐量来解决问题,但未必会使用所有容量。也就是说,表通常会被过度预置。这虽然使用户能够达到所需的性能,但体验并不好,因为难以准确估算表的合适性能预置水平。
4.1 入门改进:准入控制
热点分区和吞吐量稀释问题源于将刚性性能分配严格绑定到每个分区,并在分区拆分时按比例分配该性能。尽管将性能分配限制在分区级别避免了分布式准入控制的复杂性,但事实证明,这种控制不足以满足需求。在 DynamoDB 推出后不久,为解决这些问题,系统引入了突发流量机制(bursting)和自适应容量(adaptive capacity)两项改进。
4.4.1 预留突发容量
对分区访问呈现非均匀分布的观察同样表明,并非每个存储节点上托管的分区都同时使用其分配的吞吐量。因此,为了吸收分区级别的临时负载峰值,DynamoDB 引入了突发的概念。突发的核心思想是允许应用在分区级别上,尽力利用未使用的容量来吸收短期内的吞吐量峰值。DynamoDB 会保留分区未使用容量的一部分作为未来突发吞吐量使用,最多可持续 300 秒。只有当分区的消耗量超过其预配置容量时,才会利用这部分未使用容量(称为突发容量)。
为了保证工作负载的隔离性,系统确保只有在节点级别存在未使用吞吐量时分区才能突发。突发容量通过多个令牌桶进行管理:每个分区有两个令牌桶(分配和突发),节点本身也有一个令牌桶。这些桶共同执行准入控制。当读写请求到达存储节点时,如果分区的分配令牌桶中有令牌,请求会被接受,并从分区和节点级别的令牌桶中扣减令牌。如果分区已用尽配置的令牌,请求只能在突发令牌桶和节点级令牌桶中都有令牌时被接受。读请求基于本地令牌桶处理,而写请求还需额外检查分区其他副本节点的节点级令牌桶情况。分区的领导副本会定期收集成员节点的容量信息。
4.4.2 自适应容量
DynamoDB 推出了自适应容量,以更好地应对突发容量无法吸收的长期负载峰值。自适应容量可以有效处理分区间访问模式高度倾斜的工作负载。系统会主动监控所有表的配置容量和消耗容量。如果某张表出现了限流(throttling),但表的整体吞吐量未达到上限,则会使用比例控制算法自动增加(提升)表中分区的分配吞吐量。如果表的消耗量超过了配置容量,则会减少这些分区的容量。自动管理系统会确保获得提升的分区被重新分配到合适的节点上,以便处理增加的吞吐量。不过,与突发一样,自适应容量也是一种尽力而为的机制,但它消除了 99.99% 的由倾斜访问模式引起的限流现象。
4.2 全局准入控制
尽管通过突发和自适应容量,DynamoDB 在处理非均匀访问模式方面取得了显著进展,但这些方案仍存在局限性:
- 突发仅适用于短期流量高峰,且依赖于节点是否有额外吞吐量支持突发
- 自适应容量是被动的,只有在发生限流之后才会启动,这意味着使用表的应用已经经历了短暂的不可用期。
一个显著的教训是,突发和自适应容量将分区级别的容量与准入控制紧密耦合。准入控制是分布式的,并在分区级别执行。为解决这一问题,DynamoDB 用全局准入控制(GAC)替代了自适应容量。
GAC 基于令牌桶的相同原理。GAC 服务集中跟踪表容量的总消耗,以令牌的形式表示。每个请求路由器维护一个本地令牌桶,用于做出准入决策,并定期(间隔约几秒钟)与 GAC 通信以补充令牌。GAC 维护一种基于客户端请求动态计算的临时状态。每个 GAC 服务器都可以随时停止或重启,而不会对服务的整体运行产生影响。每个 GAC 服务器可以独立跟踪一个或多个配置独立的令牌桶。所有 GAC 服务器都是一个独立哈希环的一部分。
请求路由器在本地管理多个时间有限的令牌。当应用程序的请求到达时,请求路由器会扣减令牌。由于消耗或令牌到期,请求路由器最终会耗尽令牌。当令牌耗尽时,请求路由器会向 GAC 请求更多令牌。GAC 实例根据客户端提供的信息,估算全局令牌消耗情况,并向客户端分配其对应的下一时间单位内可用的令牌份额。通过这种方式,GAC 确保了发送到部分数据项的非均匀工作负载也能执行到最大分区容量。
除了全局准入控制机制外,分区级别的令牌桶仍然保留,作为一种深度防御措施。这些令牌桶的容量被限制,从而确保单个应用程序不会消耗存储节点上的全部或大部分资源。
4.3 消耗容量的平衡
允许分区始终突发(burst)要求 DynamoDB 高效管理突发容量。DynamoDB 运行在各种硬件实例类型上,这些实例类型的吞吐量和存储能力各不相同。最新一代的存储节点托管着数千个分区副本。单个存储节点上托管的分区可能完全无关,属于不同的表。这些表可能来自不同的客户,且具有多样化的流量模式。因此,需要设计一种分配方案,以确定哪些副本可以安全共存,而不违反关键属性,例如可用性、性能可预测性、安全性和弹性。
在预置吞吐量表的场景下,共址(colocation)是一个相对简单的问题。这是因为静态分区的存在使得预置模式下的管理更容易。静态分区使分配方案相对简单,对于不允许突发或自适应容量的预置表,分配过程只需根据分配的容量找到能容纳该分区的存储节点即可。由于分区不能承载超过其分配容量的流量,因此不会出现“吵闹邻居”(noisy neighbors)问题。而且,在某一时刻,存储节点上的所有分区通常不会利用其全部容量。
然而,当工作负载变化时允许突发会导致存储节点可能超出其预定容量,这使得租户的共址管理变得更加复杂。因此,系统会以超过存储节点整体预置容量的副本集合对节点进行填充。为了降低因紧密分布的副本带来的可用性风险,DynamoDB 实施了一种主动平衡分区分配的系统,该系统基于吞吐量消耗和存储情况对分区进行动态调整。每个存储节点独立监控其托管副本的整体吞吐量和数据大小。当吞吐量超出节点最大容量的某个阈值时,节点会向自动管理(autoadmin)服务报告候选分区副本的列表,以便从当前节点迁移。自动管理服务会为这些分区找到新的存储节点,这些节点位于同一或其他可用区,并且不包含该分区的副本。
4.4 为消耗进行分裂
即使有了 GAC 和分区始终突发的能力,如果某些表的流量集中在特定的数据项上,仍可能发生流量限制(throttling)。为了解决这个问题,DynamoDB 会根据吞吐量消耗自动扩展分区。当某个分区的吞吐量消耗超过一定阈值时,系统会将该分区拆分以分散负载。拆分点根据分区观察到的键分布来选择。观察到的键分布可作为应用程序访问模式的代理信息,比简单地在键范围中点进行拆分更为有效。分区拆分通常在几分钟内完成。
然而,仍有一些工作负载无法从消费拆分中获益。例如,某个分区只对单个数据项产生高流量,或某个分区的键范围被顺序访问。在这种情况下,拆分无法改善性能。DynamoDB 会检测这些访问模式,并避免对该分区进行拆分。
4.5 按需预置
许多迁移到 DynamoDB 的应用程序之前运行在本地或自托管数据库上。在这些场景中,应用开发人员需要预置服务器。DynamoDB 提供了一种简化的无服务器操作模型以及新的预置模式——读写容量单位。由于容量单位这一概念对客户来说是新的,有些客户很难预测所需的预置吞吐量。正如本节开头提到的,客户要么过度预置(导致利用率低),要么不足预置(导致流量限制)。
为了改善客户在尖峰工作负载中的体验,我们推出了按需表。按需表消除了客户为表确定正确预置容量的负担。DynamoDB 根据消耗容量动态调整按需表的预置情况,收集读写信号并立即支持高达之前流量峰值两倍的突发流量。如果应用程序需要超过两倍的流量峰值,DynamoDB 会随着流量增加自动分配更多容量,以确保工作负载不发生限制。按需模式通过分区拆分来扩展表,其拆分决策算法基于流量模式。
GAC 允许 DynamoDB 监控并保护系统,防止某个应用程序消耗所有资源。通过基于消耗容量的智能平衡,按需表的分区可以被合理地分配,从而避免达到节点级别的限制。
5 数据持久性与正确性
数据在提交后绝不应丢失。然而,在实践中,由于硬件故障、软件漏洞或硬件缺陷,可能会发生数据丢失。DynamoDB 通过预防、检测和修复潜在数据丢失的机制,设计为高度持久的系统。
5.1 硬件故障
与大多数数据库管理系统一样,DynamoDB 中的WAL(write-ahead logs, WAL)是实现数据持久性和崩溃恢复的核心。WAL存储在分区的三个副本中,为了更高的持久性,这些日志会定期归档到设计为具有 11 个九(99.999999999%)耐久性的 S3 对象存储中。每个副本仍然保留最新的WAL,通常这些日志等待被归档。未归档的日志通常只有几百兆字节大小。
在大型服务中,内存或磁盘等硬件故障是常见现象。当某个节点故障时,托管在该节点上的所有复制组将降至两个副本。修复存储副本的过程可能需要几分钟,因为修复涉及复制 B-Tree和WAL。一旦检测到某个存储副本状态异常,复制组的Leader会添加一个日志副本以确保数据持久性不受影响。添加日志副本只需几秒钟,因为系统只需从健康副本中复制最近的WAL,而不涉及 B-Tree。通过快速使用日志副本修复受影响的复制组,DynamoDB 确保了最新写入的高度持久性。
5.2 静默数据错误
某些硬件故障可能导致错误数据被存储,例如存储介质、CPU 或内存导致的错误。检测这些错误非常困难,且可能发生在系统的任何部分。DynamoDB 广泛使用校验和来检测静默错误。通过在每条日志记录、消息和日志文件中维护校验和,DynamoDB 验证了两节点间每次数据传输的数据完整性。这些校验和如同护栏,防止错误传播到系统的其他部分。例如,每条节点或组件之间的消息都会生成校验和,并在消息经过多个层的转换后在目标处进行验证。
每个归档到 S3 的日志文件都附带一个清单,其中包含日志的信息(如表名、分区及数据的起止标记)。负责归档日志文件到 S3 的代理在上传数据前会执行各种检查,例如:验证每条日志记录属于正确的表和分区,校验校验和以检测静默错误,并确认日志文件在序列号中没有缺口。通过所有检查后,日志文件及其清单会被归档。归档代理运行在复制组的三个副本上。如果某个代理发现日志文件已归档,它会下载已上传的文件并通过与本地WAL进行对比来验证数据完整性。每个日志文件和清单文件都以内容校验和的形式上传至 S3,S3 在上传操作中会检查校验和,以防止传输中的数据错误。
5.3 持续验证
DynamoDB 还会对静态数据进行持续验证,以检测系统中的静默数据错误或比特腐烂(bit rot)。例如,清理(scrub)过程旨在检测意外错误,例如比特腐烂。清理过程验证两点:复制组中三个副本的数据一致性,以及实时副本数据与使用归档日志离线构建的副本数据一致性。通过对实时副本的校验和与归档日志生成的快照进行对比,清理机制提供了深入防御,以检测实时存储副本与历史日志构建副本之间的偏差。这些全面的检查提高了系统运行的可靠性。类似的持续验证技术也用于全球表的副本验证。DynamoDB 的经验表明,对静态数据进行持续验证是防止硬件故障、静默数据损坏以及软件漏洞的最可靠方法。
5.4 软件漏洞
DynamoDB 是一个建立在复杂基础上的分布式键值存储系统。高复杂性增加了设计、编码和操作中的人为错误概率。系统中的错误可能导致数据丢失、数据损坏或违反客户依赖的接口契约。DynamoDB 广泛使用形式化方法确保复制协议的正确性。核心复制协议使用 TLA+ 规范描述。每当添加影响复制协议的新功能时,它们会被纳入规范并进行模型检查。模型检查帮助捕获可能导致持久性和正确性问题的微妙错误,从而在代码投入生产前解决问题。
此外,DynamoDB 进行广泛的故障注入测试和压力测试,以确保每个部署软件的正确性。除了验证数据平面的复制协议外,形式化方法还用于验证控制平面和分布式事务等功能的正确性。
5.5 备份与恢复
为防止物理介质损坏,DynamoDB 提供了备份与恢复功能,以防止客户应用程序漏洞引起的逻辑损坏。备份与恢复不会影响表的性能或可用性,因为它们基于归档到 S3 的WAL构建。备份在多个分区间保持一致,时间精确到最近的秒。备份是 DynamoDB 表的完整副本,存储在 Amazon S3 存储桶中,数据可随时恢复到新的 DynamoDB 表中。
DynamoDB 还支持时间点恢复(point-in-time restore)。客户可以将过去 35 天内表的任意时刻内容恢复到同一区域的另一张 DynamoDB 表中。对于启用了时间点恢复的表,DynamoDB 会为其分区定期创建快照并上传到 S3。快照周期基于分区积累的WAL量决定。恢复时,DynamoDB 找到最接近恢复请求时间的快照,应用日志至请求时间戳,生成表快照并进行恢复。
6. 可用性
为了实现高可用性,DynamoDB 表被分布并复制到一个区域内的多个可用区(AZ)。本节将扩展过去十年中为确保高可用性所解决的一些挑战。
6.1 写入和一致性读取的可用性
一个分区的写入可用性依赖于其是否具有健康的Leader和健康的写入仲裁。在 DynamoDB 中,健康的写入仲裁由来自不同 AZ 的三副本中的两副本组成。如果需要的副本数量不足,分区将无法进行写入。【只要有 2 个副本存活(其中一个是 leader),就能继续写】。如果其中一个副本长时间无响应,Leader会将一个日志副本添加到组中。添加日志副本是确保写入仲裁始终满足的最快方法,从而最大限度地减少由于写入仲裁不健康引起的写入可用性中断。【“顶替”掉不健康的副本,让写 quorum 继续满足 2/3 的条件,如果该副本回来了,就重新恢复其地位】
Leader副本处理一致性读取。当Leader副本发生故障时,其他副本会检测到并选出一个新Leader,以尽量减少一致性读取可用性中断。而最终一致性读取可以由任意副本提供。引入日志副本对系统进行了重大改动,但经过形式化证明的 Paxos 实现使得团队能够安全地调整和实验,以实现更高的可用性。当前,DynamoDB 在一个区域内可以运行数百万个带日志副本的 Paxos 组。
6.2 故障检测
新选出的Leader在开始服务流量前需要等待旧Leader租约的过期。虽然这一过程只需几秒,但在此期间,新Leader无法接受新的写入或一致性读取流量,导致短暂的可用性中断。故障检测是高度可用系统的重要组成部分。它需要快速且可靠,以尽量减少中断。故障检测中的误判可能会增加可用性中断的次数。对于每个副本都无法与Leader通信的故障场景,故障检测效果良好。然而,节点可能经历灰色网络故障,这种故障可能由于Leader与Follower之间的通信问题而发生。
为了解决灰色故障引起的可用性问题,试图触发故障转移的Follower会向其他副本发送消息,询问是否能够与Leader通信。如果其他副本返回“Leader健康”的消息,该Follower就会放弃触发Leader选举的尝试。这种故障检测算法的改进显著减少了系统中的误判次数,以及由此引发的虚假Leader选举次数。
6.3 可用性测量
DynamoDB 的设计目标是全球表达到 99.999% 的可用性,区域表达到 99.99% 的可用性。可用性按照每 5 分钟间隔计算,即 DynamoDB 成功处理的请求的百分比。为了确保达到这些目标,DynamoDB 持续监控服务和表级别的可用性。跟踪的可用性数据用于分析客户感知的可用性趋势,并在客户遇到超出某个阈值的错误时触发警报。这些警报称为客户感知警报(CFA),旨在报告任何与可用性相关的问题,并通过自动化或操作员干预来主动缓解问题。
除了实时跟踪,系统每天运行作业,触发聚合以计算每位客户的整体可用性指标。这些聚合结果会上传到 S3,以便定期分析可用性趋势。此外,DynamoDB 还通过两组客户端来测量和警报客户端观察到的可用性。这两组客户端包括内部 Amazon 服务(作为数据存储使用 DynamoDB)和 DynamoDB 的金丝雀应用程序。金丝雀应用程序从区域内的每个 AZ 运行,通过所有公共端点与 DynamoDB 通信。真实的应用流量帮助团队分析客户可能遇到的 DynamoDB 可用性和延迟问题。
6.4 部署
与传统的关系型数据库不同,DynamoDB 在不需要维护窗口、并且不会影响客户体验的情况下,完成软件部署。部署包括新功能、错误修复以及性能优化。部署过程涉及对多个服务的更新,通常按照固定频率推送。DynamoDB 在组件级别运行升级和降级测试,并故意执行回滚测试,以验证回滚过程的有效性。这些测试有助于发现可能导致回滚失败的问题。
在分布式系统中,部署并不是原子的。在任何给定时刻,可能会有一些节点运行旧代码,而其他节点运行新代码。为此,DynamoDB 采用读写部署(Read-Write Deployment)流程。首先,更新所有节点的代码以支持读取新消息格式或协议,然后再更新代码以发送新消息。这种流程确保了系统在新旧消息格式共存的情况下正常运行,即便需要回滚,系统仍能理解两种消息格式。
所有部署首先在少量节点上进行,以减少潜在影响。DynamoDB 在部署期间对可用性指标设置了警报阈值(参见 6.3)。如果错误率或延迟超过阈值,系统会自动触发回滚。此外,存储节点的部署会触发Leader故障转移,设计上避免对可用性产生任何影响。
6.5 对外部服务的依赖
为了确保高可用性,DynamoDB 在请求路径中所依赖的所有服务必须比 DynamoDB 本身更高可用。或者,即使依赖的服务出现问题,DynamoDB 也应能够继续运行而不牺牲这些系统提供的安全特性。例如,DynamoDB 在请求路径中依赖 AWS 身份与访问管理服务(IAM)以及 AWS 密钥管理服务(AWS KMS)(适用于使用客户密钥加密的表)。DynamoDB 使用 IAM 和 AWS KMS 对每个客户请求进行身份验证。尽管这些服务高度可用,但 DynamoDB 被设计为即使这些服务不可用也能继续运行,同时保持其安全特性。
对于 IAM 和 AWS KMS,DynamoDB 采用了静态稳定设计(statically stable design)。这种设计的核心是,即使依赖服务受损,系统整体仍能继续运行。尽管受损的依赖服务可能无法提供最新信息,但在受损之前的所有功能仍然可以正常工作。DynamoDB 会将 IAM 和 AWS KMS 的结果缓存到执行身份验证的请求路由器中,并异步定期刷新缓存结果。如果 IAM 或 KMS 不可用,请求路由器将继续使用缓存结果一段预设时间。那些发送请求到没有缓存结果的路由器的客户端可能会受到影响,但实践中,这种影响通常很小。此外,缓存通过减少外部调用提高了响应速度,尤其是在系统负载较高时表现尤为显著。
6.6 元数据可用性
请求路由器所需的最重要的元数据之一是表的主键与存储节点之间的映射。最初,DynamoDB 将这些元数据存储在其自身中。这些路由信息包括表的所有分区、每个分区的键范围,以及存储分区的节点。当路由器收到之前未见过的表的请求时,它会下载整个表的路由信息并将其缓存到本地。由于分区副本的配置信息很少发生变化,缓存命中率大约为 99.75%。
这种方法的缺点是缓存引入了双峰行为。在冷启动情况下(即请求路由器缓存为空),每个 DynamoDB 请求都会触发一次元数据查找,因此服务需要扩展以支持与 DynamoDB 相同速率的请求。这种现象在增加请求路由器容量时尤为明显,元数据服务流量偶尔会激增至 75%。这种情况不仅会影响性能,还可能导致系统不稳定。此外,无效的缓存可能会导致数据源因过高的直接负载而发生级联故障。
为了减少对请求路由器本地缓存的依赖,并避免影响客户请求的延迟,DynamoDB 构建了一个名为 MemDS 的内存分布式数据存储。MemDS 将所有元数据存储在内存中,并在 MemDS 节点群中复制。MemDS 可以水平扩展,以处理 DynamoDB 的全部请求流量。数据高度压缩,且 MemDS 节点使用了一种称为 Perkle 的数据结构(Patricia 树与 Merkle 树的混合体)。Perkle 树支持通过完整键或键前缀插入键值对并进行查询,同时支持键的有序存储,因此可以执行范围查询(如小于、大于或介于之间的查询)。此外,Perkle 树还支持两个特殊的查询操作:floor 和 ceiling。floor 操作接受一个键并返回键小于或等于指定键的存储条目;ceiling 操作则返回键大于或等于指定键的条目。
DynamoDB 在每个请求路由器主机上部署了新的分区映射缓存,避免了原始请求路由器缓存的双峰行为。在新缓存中,即使命中缓存,也会触发一次异步调用来刷新缓存。这种设计确保了 MemDS 节点群始终处理恒定流量,而不受缓存命中率影响。这种恒定流量相比传统缓存增加了元数据服务的负载,但能有效防止缓存失效时对系统其他部分造成的级联故障。
DynamoDB 存储节点是分区成员关系数据的权威来源。分区成员关系更新由存储节点推送到 MemDS。每次分区成员关系更新都会传播到所有 MemDS 节点。如果 MemDS 提供的分区成员关系是过期的,则被错误联系的存储节点会返回最新成员关系(如果已知)或返回错误码,以触发请求路由器再次查找 MemDS。
7 微基准测试
为了展示扩展规模不会影响应用程序观察到的延迟,我们运行了 YCSB(Yahoo! Cloud Serving Benchmark)[8] 的两种工作负载:类型 A(50% 读取和 50% 更新)和类型 B(95% 读取和 5% 更新)。两种基准测试均使用均匀键分布,数据项大小为 900 字节。这些工作负载在北弗吉尼亚地区的生产环境 DynamoDB 上运行,吞吐量从每秒 10 万次总操作扩展到每秒 100 万次总操作。

图 5 展示了两种工作负载在 50% 和 99% 分位数下的读取延迟。图表旨在证明,即使在不同的吞吐量下,DynamoDB 的读取延迟几乎没有变化,且随着工作负载吞吐量的增加,延迟保持不变。类型 B 工作负载的读取吞吐量是类型 A 的两倍,但延迟仍然表现出非常小的变化。
图 6 展示了两种工作负载在 50% 和 99% 分位数下的写入延迟。与读取延迟类似,无论工作负载的吞吐量如何,写入延迟保持不变。在 YCSB 的测试中,类型 A 工作负载的吞吐量高于类型 B,但两者的写入延迟分布特性相似。
8. 结论
DynamoDB 开创了云原生 NoSQL 数据库领域。它是数千个日常应用的重要组成部分,这些应用涵盖购物、食品、交通、银行、娱乐等各个方面。开发者依赖其处理数据工作负载时的扩展能力,同时确保稳定的性能、高可用性和低操作复杂性。超过十年来,DynamoDB 一直保持这些核心特性,并通过颠覆性的功能进一步吸引应用开发者,例如按需容量、时间点备份与恢复、多区域复制以及原子事务等功能。