Amazon Aurora:高吞吐量云原生关系数据库的设计考虑
Amazon Aurora:高吞吐量云原生关系数据库的设计考虑
摘要
亚马逊 Aurora 是亚马逊网络服务(AWS)提供的一种面向联机事务处理(OLTP)工作负载的关系数据库服务。在本文中,我们将描述 Aurora 的架构以及促成该架构的设计考虑因素。
我们认为,在高吞吐量数据处理中,核心限制已经从计算和存储转移到了网络上。Aurora 为关系数据库带来了一种新颖的架构以应对这一限制,最显著的是将重做日志处理推送到为 Aurora 专门构建的多租户横向扩展存储服务中。
我们描述了这样做如何不仅减少了网络流量,还允许快速的崩溃恢复、在不丢失数据的情况下故障转移到副本以及实现容错、自我修复的存储。然后,我们描述了 Aurora 如何使用高效的异步方案在众多存储节点上就持久状态达成共识,避免了昂贵且频繁通信的恢复协议。最后,在将 Aurora 作为生产服务运行了超过 18 个月后,我们分享了从客户那里学到的关于现代云应用程序对其数据库层的期望的经验教训。
1.介绍
随着云服务的广泛推广, 数据库的云服务化也在开展之中。许多客户需要关系型联机事务处理(OLTP)数据库, 他们期望提供与本地部署数据库同等或更优越的功能。
在现代分布式云服务中,弹性和可扩展性越来越多地通过将计算与存储解耦以及在多个节点上复制存储来实现。这样做使我们能够处理诸如替换行为异常或无法访问的主机、添加副本、从写入节点故障转移到副本、扩大或缩小数据库实例的规模等操作。
在这种环境下,传统数据库系统面临的 I/O 瓶颈发生了变化。由于 I/O 可以分布在多租户集群中的许多节点和许多磁盘上,单个磁盘和节点不再是热点。相反,瓶颈转移到了请求 I/O 的数据库层与执行这些 I/O 的存储层之间的网络上。除了每秒数据包数(PPS)和带宽的基本瓶颈之外,由于高性能数据库会并行地向存储集群发出写操作,所以流量会被放大。异常的存储节点、磁盘或网络路径的性能可能会主导响应时间。
虽然数据库中的大多数操作可以相互重叠,但有几种情况需要同步操作。这些情况会导致停顿和上下文切换。
- 其中一种情况是由于数据库缓冲缓存未命中而进行的磁盘读取。读取线程在其读取完成之前无法继续。缓存未命中还可能会导致额外的惩罚,即逐出并刷新一个脏缓存页以容纳新页。诸如检查点和脏页写入之类的后台处理可以减少这种惩罚的发生,但也可能会导致停顿、上下文切换和资源争用。
- 事务提交是另一个干扰源;一个事务提交的停顿会阻碍其他事务的进展。在云规模的分布式系统中,使用两阶段提交(2PC)等多阶段同步协议来处理提交是具有挑战性的。这些协议不能容忍故障,而大规模分布式系统持续存在硬故障和软故障的 "背景噪声"。它们的延迟也很高,因为大规模系统分布在多个数据中心。
在本文中,我们描述了亚马逊 Aurora,这是一种新的数据库服务,它通过在高度分布式的云环境中更积极地利用重做日志(redo log)来解决上述问题。我们使用一种新颖的面向服务的架构(见图 1),其中包括一个多租户横向扩展存储服务,该服务抽象出一个虚拟化的分段重做日志,并与一组数据库实例松散耦合。尽管每个实例仍然包含传统内核的大部分组件(查询处理器、事务、锁定、缓冲缓存、访问方法和撤销管理),但有几个功能(重做日志记录、持久存储、崩溃恢复以及备份 / 还原)被卸载到存储服务中。
图1:
我们的架构与传统方法相比有三个显著优势:
- 首先,通过在多个数据中心构建独立的容错且能自我修复的存储服务,我们使数据库免受网络层或存储层的性能波动以及瞬时或永久性故障的影响。
- 其次,通过只向存储写入重做日志记录,我们能够将网络 IOPS(每秒输入 / 输出操作次数)降低一个数量级。一旦我们消除了这个瓶颈,我们就能够积极地优化众多其他的竞争点,从而在我们所基于的 MySQL 代码基础上获得显著的吞吐量提升。
- 第三,我们将一些最复杂和关键的功能(备份和重做恢复)从数据库引擎中一次性的昂贵操作转变为在大型分布式集群中分摊的连续异步操作。这产生了无需检查点的近乎即时的崩溃恢复,以及不干扰前台处理的低成本备份。
在本文中,我们描述了三个贡献:
- 如何在云规模下考虑持久性以及如何设计对相关故障具有弹性的仲裁系统。(第 2 节)。
- 如何通过将传统数据库的底层四分之一部分卸载到智能存储层来利用智能存储。(第 3 节)。
- 如何在分布式存储中消除多阶段同步、崩溃恢复和检查点。(第 4 节)。
然后,我们在第 5 节展示如何将这三个理念结合起来设计 Aurora 的整体架构,接着在第 6 节回顾我们的性能结果,在第 7 节介绍我们所学到的经验教训。最后,我们在第 8 节简要综述相关工作,并在第 9 节给出结论性评论。
2.大规模下的持久性
如果一个数据库系统不做其他任何事情,它必须满足这样一个约定:一旦数据被写入,就可以被读取。并非所有系统都能做到这一点。在本节中,我们将讨论我们的仲裁模型背后的基本原理、我们为什么对存储进行分段,以及这两者如何结合起来不仅提供持久性、可用性和减少抖动,还帮助我们解决大规模管理存储集群的操作问题。
2.1 复制与相关故障
实例的生命周期与存储的生命周期没有很好的相关性。实例会出现故障。客户会关闭它们。他们会根据负载对其进行大小调整。由于这些原因,将存储层与计算层解耦是有帮助的。
一旦这样做了,那些存储节点和磁盘也可能会出现故障。因此,它们必须以某种形式进行复制,以提供对故障的弹性。在大规模的云环境中,存在持续的低水平的节点、磁盘和网络路径故障的背景噪声。每个故障可能有不同的持续时间和不同的影响范围。例如,可能会出现对一个节点的网络可用性暂时缺失、重启时的临时停机,或者磁盘、节点、机架、叶节点或骨干网络交换机的永久故障,甚至是数据中心的故障。
在复制系统中容忍故障的一种方法是使用基于仲裁的投票协议。如果复制数据项的 V 个副本中的每一个都被分配一个投票权,那么读操作或写操作必须分别获得 Vr
个读仲裁投票或 Vw
个写仲裁投票。为了实现一致性,仲裁必须遵守两个规则。首先,每次读操作必须知道最近的写操作,表述为 Vr + Vw > V
。这个规则确保用于读操作的节点集合与用于写操作的节点集合有交集,并且读仲裁包含至少一个具有最新版本的位置。
其次,每次写操作必须知道最近的写操作以避免冲突的写操作,表述为 Vw > V/2
。因为两次写一定可以用重复的元素。
接下来论文分析的是为什么Aurora选择了6副本的方式。
一种常见的容忍单个节点丢失的方法是(3副本):
- 将数据复制到(V = 3)个节点
- 依赖 2/3 的写仲裁(Vw = 2)
- 依赖 2/3 的读仲裁(Vr = 2)
我们认为2/3的法定人数(quorum)是不够的。首先,让我们了解一下AWS中可用区(Availability Zone, AZ)的概念。
可用区是一个区域(Region)中的子集,通过低延迟链路与同一区域中的其他可用区连接,但在大多数故障情况下(包括电源故障、网络问题、软件部署、洪水等)能够保持隔离。将数据副本分布到不同的可用区中可以确保在大规模运行时,仅会影响一个数据副本,从而容忍典型的故障模式。这意味着只需将三个副本分别放置在三个不同的可用区中,就可以抵御大型故障事件以及较小的单个故障。
然而,在一个大型存储集群中,故障的背景噪声意味着在任何给定的时间点,一些磁盘或节点的子集可能已经出现故障并正在进行修复。这些故障可能独立地分布在可用区 A、B 和 C 的每个节点中。但是,由于火灾、屋顶坍塌、洪水等原因导致可用区 C 出现故障,将打破任何同时在可用区 A 或可用区 B 中出现故障的副本的仲裁。在这种情况下,在 2/3 的读仲裁模型中,我们将丢失两个副本,并且无法确定第三个副本是否是最新的。换句话说,虽然每个可用区中的副本的单个故障是不相关的,但一个可用区的故障是该可用区中所有磁盘和节点的相关故障。仲裁需要容忍可用区故障以及同时发生的背景噪声故障。
在Aurora中,我们选择了以下设计点:
- 在不丢失数据的情况下容忍丢失一个完整的可用区(AZ)和额外的一个节点(即AZ+1的故障)(读)
- 丢失一个完整的可用区时,写入能力不会受到影响。(AZ故障)(写)
我们通过在3个可用区中将每个数据项复制6次来实现这一目标,每个可用区中有2份副本。我们使用了一个具有6票(V = 6)的法定人数模型,写入法定人数为4/6(Vw = 4),读取法定人数为3/6(Vr = 3)。通过这种模型,我们可以:
- 在丢失一个可用区和额外一个节点(共计3个节点故障)的情况下保持读取可用性;
- 丢失任意两个节点,包括一个单一可用区故障的情况下,仍然保持写入可用性。
确保读取法定人数可用使我们能够通过添加额外的副本来重建写入法定人数。
最初,读到这里时,我产生的第一个疑问是4/6在数值上不就是等于2/3么,二者有何区别?
实际上,作者在这里想解释的是为什么AWS选择了6副本而不是经典的3副本,原因是3副本只能抵挡一个AZ的宕机,但是无法在此基础之上接受故障的背景噪声(随机故障)。 而六副本抵挡风险的能力更强,在丢失一个可用区和额外一个节点(共计3个节点故障)的情况下保持读取可用性,丢失任意两个节点,包括一个单一可用区故障的情况下,仍然保持写入可用性。
2.2 分段存储
让我们考虑一下 AZ+1 是否提供了足够的持久性的问题。为了在这个模型中提供足够的持久性,必须确保在修复其中一个故障(平均修复时间 ——MTTR)所需的时间内,不相关故障上的双重故障概率(平均故障间隔时间 ——MTTF)足够低。
如果双重故障的概率足够高,我们可能会在可用区故障时看到这些情况,从而打破仲裁。在一定程度之后,很难降低独立故障的平均故障间隔时间(MTTF)的概率。相反,我们专注于减少平均修复时间(MTTR),以缩小易受双重故障影响的时间窗口。我们通过将数据库卷分割成小的固定大小的段来实现这一点,目前段的大小为 10GB。这些段以 6 种方式复制到保护组(PG)中,以便每个 PG 由六个 10GB 的段组成,分布在三个可用区中,每个可用区有两个段。存储卷是一组连接的 PG,在物理上使用大量的存储节点来实现,这些存储节点被配置为带有附加固态硬盘(SSD)的虚拟主机,使用亚马逊弹性计算云(EC2)。组成卷的 PG 随着卷的增长而分配。我们目前支持在未复制的基础上可以增长到 64TB 的卷。
现在,段是我们独立的背景噪声故障和修复的单位。我们作为服务的一部分对故障进行监控并自动修复。在 10Gbps 的网络链路上,一个 10GB 的段可以在 10 秒内修复。我们需要在同一个 10 秒窗口内看到两次这样的故障,再加上一个不包含这两个独立故障中任何一个的可用区的故障,才会失去仲裁。按照我们观察到的故障发生率,这种情况极不可能发生,即使对于我们为客户管理的数据库数量来说也是如此。
2.3 弹性的操作优势
一旦设计出一个对长时间故障具有天然弹性的系统,它自然也对较短时间的故障具有弹性。一个能够处理可用区长期丢失的存储系统也能够处理由于电力事件或需要回滚的不良软件部署而导致的短暂中断。一个能够处理仲裁成员多秒可用性丢失的系统能够处理网络拥塞或存储节点上的短暂负载时期。
由于我们的系统对故障具有很高的容忍度,因此我们可以利用这一点进行导致段不可用的维护操作。例如,热管理很简单。我们可以将热磁盘或节点上的一个段标记为损坏,法定人数将通过迁移到存储集群中的其他较冷的节点而迅速得到修复。操作系统和安全补丁对于正在打补丁的那个存储节点来说是一个短暂的不可用事件。甚至我们对存储集群的软件升级也是以这种方式进行管理的。我们一次在一个可用区中执行升级,并确保一个归置组中同时进行打补丁的成员不超过一个。这使我们能够在存储服务中使用敏捷方法和快速部署。
这里作者想表达的意思是,一个高可用的系统,天生对于运维具有优势。
3.日志即数据库
在本节中,我们解释为什么在如第 2 节所述的分段复制存储系统上使用传统数据库会在网络输入 / 输出和同步停顿方面带来难以承受的性能负担。然后,我们解释我们的方法,即我们将日志处理转移到存储服务中,并通过实验展示我们的方法如何能够显著减少网络输入 / 输出。最后,我们描述了在存储服务中使用的各种技术,以最大限度地减少同步停顿和不必要的写入。
3.1 放大写入的负担
我们对存储卷进行分段,并以 4/6 写入法定人数将每个段复制 6 次的模型为我们带来了高弹性。不幸的是,对于像 MySQL 这样的传统数据库,这种模型会导致难以承受的性能表现,因为对于每次应用程序写入,MySQL 会产生许多不同的实际输入 / 输出操作。高输入 / 输出量会因复制而被放大,从而带来沉重的每秒数据包(PPS)负担。此外,这些输入 / 输出操作会导致出现同步点,使管道停滞并延长延迟。虽然链式复制及其替代方案可以降低网络成本,但它们仍然会遭受同步停滞和累加延迟的影响。
让我们来研究一下传统数据库中的写入是如何工作的。
像 MySQL 这样的系统将数据页写入它所暴露的对象(例如,堆文件、B 树等),同时将redo log记录写入预写日志(WAL)。每个redo log记录都由被修改页面的后像和前像之间的差异组成。日志记录可以应用于页面的前像以生成其后像。
在实际中,还必须写入其他数据。例如,考虑一个同步镜像的 MySQL 配置,该配置在数据中心之间实现高可用性,并以主备配置运行,如图 2 所示。在可用区 1(AZ1)中有一个活动的 MySQL 实例,其在亚马逊弹性块存储(EBS)上具有网络存储。在可用区 2(AZ2)中也有一个备用的 MySQL 实例,同样在 EBS 上具有网络存储。对主 EBS 卷的写入通过软件镜像与备用 EBS 卷同步。

图 2 展示了引擎需要写入的各种类型的数据:redo log、为了支持时间点恢复而归档到亚马逊简单存储服务(S3)的bin log、修改后的数据页、为防止页损坏而对数据页进行的第二次临时写入(双写)以及最终的元数据FRM文件(表结构文件)。该图还展示了实际 I/O 流的顺序如下。在步骤 1 和步骤 2 中,写入被发送到 EBS,EBS 又将其发送到一个可用区本地镜像,并且当两者都完成时会收到确认。接下来,在步骤 3 中,使用同步块级软件镜像将写入暂存到备用实例。最后,在步骤 4 和步骤 5 中,写入被写入到备用 EBS 卷和相关镜像。
上文所述的镜像 MySQL 模型不是所期望的,不仅是因为数据的写入方式,还因为写入的数据内容。
- 首先,步骤 1、3 和 5 是顺序且同步的。延迟是累加的,因为许多写入是顺序进行的。抖动被放大,因为即使在异步写入中,也必须等待最慢的操作,这使得系统受异常值的影响。从分布式系统的角度来看,这个模型可以被视为具有 4/4 的写入法定人数,容易受到故障和异常性能的影响。
- 其次,作为 OLTP 应用程序结果的用户操作会导致许多不同类型的写入,这些写入通常以多种方式表示相同的信息 —— 例如,为了防止存储基础设施中的页撕裂而写入双写缓冲区。
3.2 将重做处理卸载到存储中
当传统数据库修改数据页时,会生成一条redo log记录,并调用日志应用器,将该重做日志记录应用到该页的内存中前镜像上,以生成其后镜像。事务提交需要写入日志,但数据页的写入可能会被延迟。
在 Aurora 中,唯一跨网络的写入是重做日志记录。数据库层从不写入任何页,既不用于后台写入,也不用于检查点,也不用于缓存逐出。相反,日志应用程序被推送到存储层,在那里它可以用于在后台或按需生成数据库页。当然,从一开始就从其完整的修改链中生成每个页的成本高得令人望而却步。
因此,我们不断地在后台实例化数据库页,以避免每次都按需从头开始重新生成它们。请注意,从正确性的角度来看,后台实例化完全是可选的:就引擎而言,日志就是数据库,存储系统实例化的任何页面都只是日志应用的缓存。还要注意的是,与检查点不同,只有具有长修改链的页面才需要重新实例化。检查点由整个重做日志链的长度控制。Aurora 页面实例化由给定页面的链的长度控制。
我们的方法极大地减少了网络负载,尽管增加了复制的写入量,但同时提供了性能和持久性。存储服务可以以一种极其简单的并行方式扩展 I/O,而不会影响数据库引擎的写入吞吐量。例如,图 3 展示了一个 Aurora 集群,其中有一个主实例和多个副本实例部署在多个可用区中。在这个模型中,主实例仅向存储服务写入日志记录,并将这些日志记录以及元数据更新流式传输到副本实例。I/O 流根据一个共同的目的地(一个逻辑段,即一个保护组(PG))对完全有序的日志记录进行批处理,并将每个批次传递到所有 6 个副本,在副本中,批次被持久化到磁盘上,并且数据库引擎等待来自 6 个副本中的 4 个的确认,以满足写入法定人数并认为相关的日志记录是持久的或已固化的。副本使用重做日志记录将更改应用于它们的缓冲缓存。

为了测量网络 I/O,我们使用 SysBench只写工作负载对上述两种配置进行了测试,测试数据集为 100GB:一种是在多个可用区中使用同步镜像 MySQL 配置,另一种是使用 RDS Aurora(在多个可用区中有副本)。在这两种情况下,测试针对在 r3.8xlarge EC2 实例上运行的数据库引擎进行了 30 分钟。
Aurora和MySQL网络IO对比
配置 | 事务数量 | 每个事务的IO数量 |
---|---|---|
镜像MySQL | 780000 | 7.4 |
带有副本的Aurora | 27378000 | 0.95 |
我们实验的结果总结在表 1 中。在 30 分钟的时间里,Aurora 能够支持的事务数量是镜像 MySQL 的 35 倍。Aurora 中数据库节点上每个事务的 I/O 次数比镜像 MySQL 少 7.7 倍,尽管 Aurora 的写入量放大了 6 倍,并且这还没有计算 EBS 中的链式复制以及 MySQL 中的跨可用区写入。每个存储节点看到的是未放大的写入,因为它只是六个副本之一,这导致在这个层级需要处理的 I/O 次数减少了 46 倍(7.7*6)。通过向网络写入更少的数据所获得的节省使我们能够积极地复制数据以实现持久性和可用性,并并行发出请求以最小化抖动的影响。
将处理转移到存储服务还可以通过最小化崩溃恢复时间来提高可用性,并消除由诸如检查点、后台数据页写入和备份等后台进程引起的抖动。
让我们来研究一下崩溃恢复。在传统数据库中,崩溃后系统必须从最近的检查点开始,并重放日志以确保所有持久化的重做记录都已被应用。在 Aurora 中,持久化重做记录的应用在存储层连续、异步地进行,并分布在整个集群中。如果数据页不是最新的,那么对该数据页的任何读取请求可能需要应用一些重做记录。因此,崩溃恢复的过程分散在所有正常的前台处理中。在数据库启动时不需要任何额外操作。(发送读请求时再应用redo log,分散恢复处理,传统数据库是集中处理)。
3.3 存储服务设计要点
我们存储服务的一个核心设计原则是最小化前台写入请求的延迟。我们将大部分存储处理转移到后台。考虑到来自存储层的前台请求在峰值和平均值之间的自然可变性,我们有充足的时间在前台路径之外执行这些任务。我们也有机会用 CPU 换取磁盘空间。例如,当存储节点忙于处理前台写入请求时,除非磁盘容量即将耗尽,否则没有必要对旧页面版本进行垃圾回收(GC)。在 Aurora 中,后台处理与前台处理呈负相关。这与传统数据库不同,在传统数据库中,页面的后台写入和检查点与系统的前台负载呈正相关。如果我们在系统上积累了待办事项,我们将限制前台活动以防止长队列的积累。由于在我们的系统中,各个存储节点上的段是以高熵放置的,因此在一个存储节点上的限制很容易通过我们的 4/6 法定人数写入来处理,表现为一个缓慢的节点。
让我们更详细地研究存储节点上的各种活动。如图 4 所示,它涉及以下步骤:
- 1.接收日志记录并添加到内存队列中;
- 2.将记录持久化到磁盘上并确认;
- 3.整理记录并识别日志中的间隙,因为某些批次可能会丢失;
- 4.与对等节点进行通信以填补间隙;
- 5.将日志记录合并到新的数据页中;
- 6.定期将日志和新页面暂存到 S3;
- 7.定期对旧版本进行垃圾回收;
- 8.定期验证页面上的 CRC 码。
请注意,上述每个步骤不仅都是异步的,而且只有步骤(1)和(2)处于可能影响延迟的前台路径中。
图4:
这里图中描述的是主节点和存储节点之间的交互,这里只需要1和2步骤完成,即代表主节点和存储节点交互完成。但是最终返回确认到客户端还是要获取4/6的法定人数的认同的过程。
4.日志稳步向前
在本节中,我们将描述日志是如何从数据库引擎生成的,以便持久状态、运行时状态和副本状态始终保持一致。特别是,我们将描述如何在不使用昂贵的两阶段提交(2PC)协议的情况下高效地实现一致性。首先,我们展示如何在崩溃恢复时避免昂贵的重做处理。接下来,我们解释正常操作以及如何维护运行时状态和副本状态。最后,我们提供恢复过程的详细信息。
4.1 解决方案概述:异步处理
由于我们将数据库建模为一个重做日志流(如第 3 节所述),我们可以利用日志作为一系列有序变更而推进这一事实。实际上,每个日志记录都有一个相关联的日志序列号(LSN),它是由数据库生成的单调递增的值。
这使我们能够通过以异步方式处理问题来简化用于维护状态的共识协议,而不是使用像两阶段提交(2PC)这样冗长且不能容忍故障的协议。从高层次上讲,我们维护一致性和持久性的点,并在收到未完成的存储请求的确认时不断推进这些点。由于任何单个存储节点可能会错过一个或多个日志记录,它们与所属保护组(PG)的其他成员进行通信,寻找间隙并填补漏洞。数据库维护的运行时状态使我们能够使用单段读取而不是法定人数读取,除非在状态丢失并必须重建的恢复期间。
数据库可能有多个未完成的独立事务,这些事务的完成顺序(达到完成且持久的状态)可能与启动顺序不同。假设数据库崩溃或重新启动,对于这些单独的事务中的每一个,确定是否回滚是独立进行的。跟踪部分完成的事务并撤销它们的逻辑保存在数据库引擎中,就像它正在写入简单的磁盘一样。然而,在重新启动时,在数据库被允许访问存储卷之前,存储服务会进行自己的恢复,其重点不是在用户级事务上,而是确保数据库尽管具有分布式性质,但仍能看到存储的统一视图。
存储服务确定它能够保证所有先前日志记录可用性的最高日志序列号(这被称为卷完成日志序列号(VCL))。在存储恢复期间,每个日志序列号大于 VCL 的日志记录都必须被截断。然而,数据库可以通过标记日志记录并将其识别为一致性点日志序列号(CPL)来进一步限制可用于截断的点的子集。因此,我们将卷持久日志序列号(VDL)定义为小于或等于 VCL 的最高 CPL,并截断所有日志序列号大于 VDL 的日志记录。例如,即使我们拥有直到日志序列号 1007 的完整数据,数据库可能已经声明只有 900、1000 和 1100 是 CPL,在这种情况下,我们必须在 1000 处截断。我们完整到 1007,但仅持久到 1000。
这里涉及到很多的术语,这里梳理一下:
术语 | 含义 |
---|---|
LSN(Log Sequence Number) | 日志序列号,数据库系统为每个日志记录生成唯一记录ID。传统的数据库系统采用文件偏移量表示,在Aurora中利用时间戳标记 |
VCL(Volumn Complete LSN) | 存储节点收到的最大连续日志ID |
CPL(consistency Point LSN) | 在数据库层面,事务被分成多个MTRs(mini-transactions), 每个MTR的最后一条log为Consistency Point LSN(CPL) |
VDL(Volumn Durable LSN) | VDL为最大的CPL,其VDL <= VCL,在系统恢复阶段,需要通过多数派确定VDL |
SCL(SegmentComplete LSN) | 已经完成的段的日志号 |
假设DB目前看见三个CPL分别是900、1000、1100,此时VCL为1007(大于1007的log不完整),则VDL为1000,大于1000的log都会被截断。
因此,完整性和持久性是不同的,一致性点日志序列号(CPL)可以被视为划定了某种有限形式的存储系统事务,这些事务必须按顺序被接受。如果客户端不需要这种区分,它可以简单地将每个日志记录标记为 CPL。实际上,数据库和存储的交互如下:
- 每个数据库级别的事务被分解为多个有序的小型事务(MTR),并且必须以原子方式执行。
- 每个小型事务由多个连续的日志记录组成(根据需要可以有很多)。
- 小型事务中的最后一个日志记录是一个 CPL。
在恢复时,数据库与存储服务通信以确定每个保护组(PG)的持久点,并使用该点来确定卷持久日志序列号(VDL),然后发出命令截断 VDL 以上的日志记录。
4.2 正常操作
现在我们描述数据库引擎的 "正常操作",并依次聚焦于写入、读取、提交和副本。
4.2.1 写入
在 Aurora 中,数据库持续与存储服务交互并维护状态以建立法定人数、推进存储卷的持久化,并将事务注册为已提交。
例如,在正常 / 向前路径中,当数据库接收到确认以建立每批日志记录的写入法定人数时,它会推进当前的卷持久日志序列号(VDL)。在任何给定时刻,数据库中可能有大量并发事务处于活动状态,每个事务都在生成自己的重做日志记录。
数据库为每个日志记录分配一个唯一的有序日志序列号(LSN),但要满足一个约束条件,即分配的任何 LSN 的值都不能大于当前卷持久日志序列号(VDL)与一个称为 LSN 分配限制(LAL)(当前设置为 1000 万)之和。这个限制确保数据库不会领先存储系统太多,并引入背压,如果存储或网络跟不上,可以限制传入的写入操作。这里简单来讲就是说不让存储层落后日志层太多。
请注意,每个保护组(PG)的每个段仅看到卷中的一部分日志记录,这些日志记录会影响驻留在该段上的页。每个日志记录都包含一个反向链接,用于标识该 PG 的前一个日志记录。这些反向链接可用于跟踪已到达每个段的日志记录的完整点,以建立一个段完成日志序列号(SCL),该序列号标识了一个最大的 LSN,在此 LSN 以下,该 PG 的所有日志记录都已被接收。存储节点在相互通信时会使用 SCL,以便找到并交换它们缺失的日志记录。
4.2.2 提交
在 Aurora 中,事务提交是异步完成的。当客户端提交一个事务时,处理提交请求的线程将该事务搁置一旁,通过将其 "提交日志序列号(commit LSN)" 记录在一个单独的等待提交的事务列表中,然后继续执行其他工作。与预写日志(WAL)协议等效的操作是基于完成提交,当且仅当最新的卷持久日志序列号(VDL)大于或等于事务的提交日志序列号。随着 VDL 的推进,数据库识别出等待提交的符合条件的事务,并使用一个专用线程向等待的客户端发送提交确认。工作线程不会为提交而暂停,它们只是提取其他待处理的请求并继续处理。
4.2.3 读
在 Aurora 中,与大多数数据库一样,页是从缓冲缓存中提供的,并且只有在相关页不在缓存中时才会导致存储 I/O 请求。
如果缓冲区缓存已满,系统会找到一个受害者页面从缓存中驱逐。在传统系统中,如果被驱逐的受害者是一个"脏页面",那么在替换之前,它会被刷新到磁盘上。这样做是为了确保后续对该页面的获取始终能够得到最新的数据。
虽然 Aurora 数据库在驱逐页面时(或在其他地方)并不会将页面写入磁盘,但它同样强制执行一个类似的保证:缓冲区缓存中的页面必须始终是最新版本。
这个保证是通过仅在页面的"页面日志序列号(page LSN)"(标识与页面最新更改相关的日志记录)大于或等于虚拟日志序列号(VDL)时驱逐该页面来实现的。该协议确保了:
- (a) 页面中的所有更改已在日志中持久化;
- (b) 在缓存缺失时,只需请求当前 VDL 时的页面版本即可获取其最新的持久版本。
正常情况下,数据库读操作并不需要采用多数投票的方式,当从磁盘读取数据时,会分配一个读位点(read point),这个点代表产生时刻的 VDL,同时数据库在存储节点维护 SCL,系统根据读位点和 SCL 来确定从哪个存储节点进行读取,只有当故障恢复或者必要的时候,才会根据多数投票的方式来确定系统的 VDL。
4.2.4 Aurora复制
在 Aurora 中,一个写入器和最多 15 个读取副本都可以挂载单个共享存储卷。因此,就消耗的存储或磁盘写入操作而言,读取副本不会增加额外成本。为了最大限度地减少延迟,由写入器生成并发送到存储节点的日志流也会发送到所有读取副本。在读取器中,数据库通过依次考虑每个日志记录来使用此日志流。如果日志记录引用了读取器缓冲缓存中的一个页面,它会使用日志应用程序将指定的重做操作应用于缓存中的页面。否则,它会简单地丢弃该日志记录。请注意,从写入器的角度来看,副本异步地使用日志记录,写入器独立于副本确认用户提交。副本在应用日志记录时遵循以下两条重要规则:(a) 唯一将被应用的日志记录是那些其日志序列号(LSN)小于或等于虚拟日志位置(VDL)的记录;(b) 作为单个微事务一部分的日志记录在副本的缓存中以原子方式应用,以确保副本看到所有数据库对象的一致视图。实际上,每个副本通常会比写入器落后一小段时间间隔(20 毫秒或更短)。
4.3 恢复
大多数传统数据库使用诸如 ARIES 这样的恢复协议,该协议依赖于预写日志(WAL)的存在,预写日志可以表示所有已提交事务的精确内容。这些系统还会定期对数据库进行检查点操作,通过将脏页刷新到磁盘并将检查点记录写入日志,以一种粗粒度的方式建立耐久性点。在重新启动时,任何给定的页面要么缺少一些已提交的数据,要么包含未提交的数据。因此,在崩溃恢复时,系统会处理自上次检查点以来的重做日志记录,使用日志应用程序将每个日志记录应用到相关的数据库页面。这个过程使数据库页面在故障点达到一致状态,之后可以通过执行相关的撤销日志记录来回滚崩溃期间的正在进行的事务。崩溃恢复可能是一项昂贵的操作。减少检查点间隔会有所帮助,但会以干扰前台事务为代价。而在 Aurora 中不需要进行这样的权衡。
传统数据库的一个极大简化原则是,在正向处理路径和恢复过程中使用相同的重做日志应用程序,在数据库离线时,它同步地在前台运行。在 Aurora 中我们也依赖同样的原则,不同之处在于重做日志应用程序与数据库解耦,并在存储节点上并行且始终在后台运行。一旦数据库启动,它就会与存储服务协作进行卷恢复,因此,即使 Aurora 数据库在每秒处理超过 10 万个写入语句时崩溃,它也能非常快速地恢复(通常在 10 秒以内)。
数据库在崩溃后确实需要重新建立其运行时状态。在这种情况下,对于每个保护组(PG),它联系一个读取法定数量的段,这足以保证发现任何可能已达到写入法定数量的数据。一旦数据库为每个保护组建立了读取法定数量,它就可以通过生成一个截断范围来重新计算高于其的数据将被截断的虚拟日志位置(VDL),该截断范围会取消新 VDL 之后的每个日志记录,直至并包括一个结束日志序列号(LSN),数据库可以证明这个结束 LSN 至少与可能曾经见过的最高的未完成日志记录一样高。数据库推断出这个上限是因为它分配日志序列号,并限制在 VDL 之上分配可以进行到多远(前面描述的 1000 万限制)。截断范围用纪元编号进行版本控制,并持久地写入存储服务,以便在恢复被中断并重新启动的情况下,不会对截断的持久性产生混淆。
数据库仍然需要执行撤销恢复,以回滚崩溃时正在进行的事务的操作。然而,在系统从撤销段构建这些正在进行的事务列表之后,撤销恢复可以在数据库在线时进行。
5.将所有内容整合在一起
在本节中,我们将描述 Aurora 的组成部分,如图 5 中的鸟瞰图所示。

8.最近工作
在本节中,我们将讨论其他贡献以及它们与 Aurora 所采用的方法之间的关系。
存储与计算的解耦。尽管传统系统通常被构建为单一的守护进程,但最近有关于将数据库内核分解为不同组件的研究工作。例如,《申命记》(Deuteronomy)就是这样一个系统,它将提供并发控制和恢复的事务组件(TC)与提供基于无锁日志结构缓存和存储管理器 LLAMA的访问方法的数据组件(DC)分离开来。《交响曲》(Sinfonia) 和《海德尔》(Hyder) 是在可扩展服务之上抽象事务访问方法的系统,数据库系统可以使用这些抽象来实现。Yesquel [36] 系统实现了一个多版本分布式平衡树,并将并发控制与查询处理器分离开来。Aurora 在比《申命记》、《海德尔》、《交响曲》和《Yesquel》更低的级别上解耦存储。在 Aurora 中,查询处理、事务、并发、缓冲缓存和访问方法与日志记录、存储和恢复相分离,后者被实现为可扩展服务。