暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases(译)

原创 吴松 云和恩墨 2022-11-09
1068

摘要

Aurora 是 AWS 中提供 OLTP 服务的关系型数据库服务,本文主要介绍 Aurora 的架构及设计上的考虑。我们认为现在数据处理高吞吐的主要痛点,已经从计算和存储转移到网络上。

Aurora 为关系型数据库引入一种新的架构来解决这个痛点,其中一个主要工作是将 redo 处理下推到专为 Aurora 打造的可横向扩展的多租户存储服务。

本文讲述 Aurora 的方案怎样减少网络传输,同时还能够做到快速崩溃恢复、在集群中故障切换而不丢失数据,以及高容错、可自恢复的存储

然后,介绍一下 Aurora 如何在不使用复杂和代价高昂的复制协议的情况下,利用高效的异步方案在多个存储节点间达到持久状态的一致

最后,在使用 Aurora 作为生产服务18个月后,分享一下客户反馈的现代云应用对数据库的期望,以及一些经验和教训。


关键词:分布式系统;日志处理;一致性模型;副本;恢复;性能;OLTP


1. INTRODUCTION

越来越多的 IT 任务迁移到云上,很大的一个原因是云上可以按需提供容量,并且可以按照容量的运营费用来支付。关系型数据库提供的 OLTP 服务有普遍的需求,为本地数据库提供与云上同等甚至更好的服务,在向云上迁移的过渡时期也非常重要。

现代分布式云服务中,弹性和可扩展性越来越多地通过将计算和存储分离,以及存储节点多副本来实现。这样的架构在处理节点异常、增加副本、故障转移,及数据库扩容等方面带来优势。

在这种架构下,传统数据库面临的 I/O 瓶颈发生了变化。由于 I/O 可以分散到多租户环境的多个节点和磁盘上,磁盘和节点不再是热点。相反,瓶颈转移到数据库层和存储层之间的网络上。除了每秒数据包(PPS)和带宽的瓶颈外,还会有流量放大的问题,因为高性能数据库是并行向存储发起写请求。存储节点、磁盘、网络都会对响应时间产生极大的影响。

虽然大多数数据库操作可以并行执行,但有一些情况下可能需要同步,这会导致 Stall 和上下文切换。例如,由于数据库缓存没有命中,导致需要从磁盘读取就是其中一种情况。在这种情况下读线程无法继续执行,直到从磁盘数据读取完成。缓存不命中还会导致缓存淘汰及刷脏页等动作。通过后台任务,如后台 checkpoint 和刷脏页一定程度上可以减小影响,但也会导致 stall 、上下文切换及资源争抢。

事务提交是另一种影响因素,一个事务未提交可能导致其他事务执行阻塞。使用多阶段提交协议,如 2PC 在云上的分布式环境存在一些挑战。这些协议不能容忍故障,但在大规模的分布式集群中硬件和软件故障可能是常见的情况。这些协议的时延可能很高,因为大规模分布式系统通常分布在多个数据中心。

本文介绍 Amazon Aurora 通过在云上分布式环境使用更积极的 redo log 策略解决上述问题的方案。Aurora 使用一种新的面向服务的架构,以及多租户横向扩展的存储服务,架构如图 1 所示。存储服务抽象了一个虚拟化的分段 redo log,且与数据库实例是松耦合的。虽然每个数据库实例仍然包含传统内核的主要模块(请求处理、事务、锁、buffer cache、access methods 和 undo 管理),但其中一些功能(redo logging、持久化存储、崩溃恢复、备份/恢复)等已经卸载到存储层。



相比于传统的架构,新的架构有3个显著的优势。

首先,存储层设计为跨多个数据中心的,独立容错和自恢复的服务,这样数据库服务不会受到网络或者存储层性能差异及故障的影响。持续性故障可以建模为一个持续的可用性事件,而可用性事件可以建模为一个持续的性能变化,好的设计可以统一处理这些情况。

其次,通过只写 redo log 到存储层,可以将网络的 IOPS 降一个量级。一旦消除了网络这个瓶颈,可以更积极地优化其他性能相关的点。

第三,将一些最复杂和重要的功能(备份和 redo 恢复),从数据库引擎中一个代价高昂的任务,拆分为分布式系统中连续的异步操作。这让崩溃恢复近乎实时而且无需 checkpoint,同时备份操作变成不会影响前台的低代价操作。

本文中主要介绍 3 个贡献:

  1. 如何设计大规模云服务的持久性,以及如何设计弹性的故障恢复系统。(第2节)
  2. 如何将传统数据库四分之一的任务卸载到智能存储层。(第3节)
  3. 如何消除分布式存储系统中的多阶段同步、崩溃恢复及 checkpoint。(第4节)
然后介绍如何将这 3 点结合在一起,设计 Aurora 的整体架构(第5节)。第6节介绍性能测试的数据,在第7节分享经验教训。最后在第8节回顾下相关的工作,第9节做一个总结。


2. DURABILITY AT SCALE

如果数据系统没有执行其他的操作,那么它可以读到写下去的数据。但并不是所有的系统都可以,在这一节介绍一致性模型背后的原理,以及为什么要对存储分段,以如何将两者结合,不仅提供了持久性、可用性并减少抖动,同时还解决了管理大规模存储集群的操作问题。

2.1 Replication and Correlated Failures

数据库实例的生命周期与存储的生命周期相关性并不大,如果实例失败了,客户可以直接 shut down 实例。同样,客户可以根据负载选择多启动几个实例。这些原因有助于将计算和存储分离。

但即便计算和存储分离了,存储节点和磁盘也可能故障。因此,必须使用副本的方式应对可能的故障。大规模的云环境,节点、磁盘、网络故障是很常见的。某一种类型的故障持续时间和影响范围都不同。例如,节点可能出现暂时的网络故障,重启时出现暂时不可用,或者因为磁盘、节点、机架故障、主干交换机,甚至数据中心故障出现永久性故障。

在副本集中一种处理故障的方式是通过共识协议在集群内达成一致。如果副本集 V 个节点参与读或写操作,那么读需要获得 Vr 个投票,写需要获得 Vw 个投票。为了达到一致,共识协议需要遵循两个原则。首先,每次读操作必须知道最新的写入,公式为 Vr + Vw > V。这个规则保证参与读的节点与参与写的节点有交集,并且参与读的节点至少有一个参与了最新的写。其次,每次写都必须知道最新的写以避免发生写冲突,公式为 Vw > V/2 。

一种常见的实现是副本集中节点数 V = 3,Vw = 2,Vr = 2。

但我们认为简单达到 2/3 是不够的。为了理解为什么不够,首先介绍一下 AWS 中 AZ(Available Zone) 的概念。一个 AZ 是 Region 的一个子集,它通过低时延的链路连接到 Region 内的其他 AZ,但对于大多数故障(电源、网络、软件部署、洪水等) AZ 间是隔离的。跨 AZ 分布数据副本,可以确保常见的故障只会影响一个数据副本。这意味着可以简单地将三个副本每一个都放到不同的 AZ,这样除了可以应对小的故障外,还可以应对有些大的事件。

但是在大规模存储集群中,任意时刻都可能出现磁盘或节点的故障。这些故障可能在 AZ 内的节点间传播,但是 AZ C 中的故障可能会影响同时出现故障的 AZ A 或 AZ B 达成共识。在这种情况下,以达到 2/3 投票读模型为例,由于出现了 2 个副本的故障,因此不能确定剩余的一个副本数据是否是最新的。换句话说,虽然每个 AZ 内的故障是独立的,一个 AZ 的故障是由 AZ 内的磁盘或者节点故障导致。共识协议需要能够容忍在一个 AZ 发生故障时,同时出现其他的故障的情况。

Aurora 中的设计,可以容忍出现(a)整个 AZ 故障,同时出现其他 AZ 中的一个节点(AZ+1)故障而不会丢失数据,以及 (b)整个 AZ 故障而不会影响数据写入。我们通过在 3 个 AZ 中使用 6 种方式复制数据,使得每个 AZ 内都有数据的 2 个副本来达到这一点。在这个共识协议中,参与的节点数是 6 即 V = 6,写需要获得 4/6 投票即 Vw = 4,而读需要获得 3/6 投票即 Vr = 3 。在这个模型中,可以容忍(a)整个AZ 故障,同时额外出现其他 AZ 内的一个节点故障,仍然不影响读操作。(b)任意的两个节点故障,包括一个 AZ 内两个节点全部故障,仍然可以执行写操作。确保在达到读共识的情况下可以通过添加额外的副本来达到满足写共识。

2.2 Segmented Storage

考虑一下上面一个 AZ 故障,同时出现一个其他 AZ 内节点故障的场景是否可以保证持久性。在这个模型下要保证足够的持久性,必须确保发生2个不相关的故障(平均故障时间 MTTF)的概率,相对于修复其中一个的时间(平均修复时间MTTR)足够低。如果出现 2 个不相关故障的概率很高,可能出现无法达成共识的情况。要降低独立故障的 MTTF 很困难,我们选择专注于减少 MTTR 以减小发生双重故障的窗口。为此,我们将数据库卷划分为固定大小的小段,目前这个大小是 10GB。每个段都使用 6 种方式复制到保护组(Protection Groups, PGs)中,每个保护组包含 6 个 10GB 的段,跨 3 个 AZ  分布,每个 AZ 内有两个段。存储卷是一组串联的保护组,使用大量的存储节点实现,这些节点使用 Amazon Elastic Compute Cloud(EC2)配置为带 SSD 的虚拟机。组成卷的保护组,根据卷的增长进行分配,目前在没有多副本的情况下可以支持增长到 64TB。

段现在是处理故障和修复的基本单位,在服务中自动监控和修复。在 10Gbps 的网络链路上,10 GB 大小的段可以在 10s 内被修复。只有在这 10s 的窗口内出现 2 个这样的故障,同时出现另外一个 AZ 的故障,共识协议才会无法运行。根据实际观察到的故障率,即便是我们为客户管理的数据库数量非常大,出现这样的情况的概率是非常低的。

2.3 Operational Advantage of Resilience

当设计了对长期故障有弹性的系统后,它也可以处理短期的故障。能处理 AZ 长期不可用的系统,也可以处理电源故障,或由于软件部署错误导致需要回滚引起的短时间不可用。能处理节点短时间不可用的系统,也可以应对存储节点上的网络拥塞或负载过大导致的短时间不可用。

由于系统具有高容错性,可以将其用于故障的维护操作。例如,发热管理就很简单。可以将热磁盘或节点上的某个段标记为故障,通过弹性机制把它迁移到其他的冷节点。操作系统及安全相关的补丁升级可以通过标记存储节点上的短暂不可用事件来完成。甚至存储设备的软件升级也可以使用这种方式进行管理。每次升级一个 AZ 并确保一个保护组中不会有超过一个成员在升级。这样存储服务可以快速敏捷部署。

3. THE LOG IS THE DATABASE

本节阐述为什么基于第2节描述的段式副本存储的传统数据库,会在网络 I/O 及同步等待方面带来难以承受的性能问题。然后阐述将日志处理卸载到存储层的方案,并通过实验演示该方案如何显著减少网络 I/O。最后,介绍一下我们在存储服务中使用的各种技术,以最大限度减少同步等待和不必要的写入。

3.1 The Burden of Amplified Writes

我们的方案具有很高的弹性,但这种方案使得传统的数据库(如 MySQL)性能受到很大的影响,因为 MySQL 为每个应用程序处理很多不同的 I/O 写入。复制会导致 I/O 放大,从而带来 PPS(每秒数据包)的压力,I/O 带来的同步等待会引起 stall 增加时延。虽然链复制及其替代方案可以降低网络成本,但它们仍然受到同步导致的 stall 及时延增加的影响。

先回顾一下传统数据库的写流程。像 MySQL 这样的系统将数据页写入数据库对象(如 heap files、b-trees 等),同时也记录 redo log 到 WAL。redo log 项记录了 page 修改前后镜像的差异。可以对修改前的镜像应用 redo log 产生修改后的镜像。

实际上还需要写入其他数据。例如,考虑 MySQL 使用同步镜像,实现跨数据中心的高可用性,实例配置为 active-standby 模式,如图-2 所示。AZ1 中有一个 active MySQL 实例,通过网络连接到存储,存储使用 Amazon EBS(Elastic Block Storage)。 AZ2 中有一个 standby MySQL 实例,同样也使用 EBS。主 EBS 卷上的写通过软件镜像同步到备 EBS 的卷上。


图 2 中数据库实例需要写多种不同的数据:redo log、归档到 Amazon S3 以支持 PITR 的 binary log、被修改的数据页、double-write 引入的数据页的第二次临时写以防止数据页损坏、以及元数据(FRM)文件。

图中还标识了 I/O 流的顺序。Step 1 和 Step 2 中写到 EBS,以及 AZ 内的另一个副本,当 Step1 和 Step 2 都执行完成返回确认。在 Step 3 使用块级同步软件镜像同步到 standby 实例,然后 Step 4 和 Step 5 写到备 EBS 和关联的副本。

上面使用的模型是不可取的,不仅是数据的写入方式,同时还有写哪些数据的问题。首先 Step 1、Step 3 和 Step 5 是顺序和同步的,延迟会增加,因为许多写操作是有顺序的。抖动会被放大,因为即便是异步写,也需要等最慢的操作,系统会受出现异常的影响。从分布式系统的角度看,这个模型可以被看作 4/4 的写共识,并且容易受到故障和性能异常的影响。其次, OLTP 应用中用户操作会导致许多不同类型的写,这些写中通常会使用多种不同的方式表示相同数据--例如,double write buffer 就是为了防止页面损坏产生了冗余数据。

3.2 Offloading Redo Processing to Storage

传统数据库修改数据页,产生 redo log 记录,然后对内存中的数据应用 redo log 产生修改后的数据。事务提交时写入日志,但数据页的修改可能会有延迟。

Aurora 中唯一跨网络的写操作只有 redo log。数据库层不写任何页面,没有后台写,没有 checkpoint ,也没有缓存淘汰导致的刷脏页。取而代之的是,日志回放被下推到存储层,可以在后台回放或者按需回放。当然,从修改链的头开始生成每个页面代价太高了。因此,需要不停地在后台基于日志生成数据页,以避免每次都需要访问时从头开始生成。这里需要注意的是,在后台生成页面从正确性的角度而言不是必须的,因为对引擎而言日志就是数据库。存储层生成的页面可以看成是日志的缓存。还有一点需要注意,和 checkpoint 不同,只有当某个页面的修改链很长时才需要根据日志物化页面。checkpoint 由整个 redo log 链的长度控制,而 Aurora 中页面的生成由具体的页面的修改链长度控制。

尽管复制导致了写放大,但我们的方案显著降低了网络负载,并且兼顾了性能和持久性。存储服务可以并行地横向扩展,而不影响数据库引擎的写吞吐。例如,图 3 中显示了一个 Aurora 集群,其中有一个 primary 实例和多个副本实例部署在多个 AZ 中。在此模型下,primary 只写日志到存储服务,并将这些日志以及元数据变更传到副本实例。I/O 流基于共同的目的地(一个逻辑段,即一个PG),对有序的日志记录进行批处理,并将每个批处理发给 6 个副本,批处理被持久化到磁盘上,数据库等待 6 个副本中 4 个确认后认为副本可以满足写共识,日志已经是持久化的。副本将 redo log 应用到本地缓存。 


为了度量网络 I/O,在 100 GB 的数据集上跑了 SysBench 的 write-only 测试。分别在跨多 AZ 的同步镜像 MySQL 实例,和 RDS Aurora (跨多 AZ 副本) 上执行测试。测试的两个环境,在 r3.8xlarge EC2 上运行数据库 30 分钟。

测试的结果在表 1 中,在 30 分钟的测试时间内,Aurora 能够运行比 MySQL 多 35 倍的事务。尽管 Aurora 将写放大了 6 倍,在不计算 EBS 中的链式复制和 MySQL 中的跨 AZ 写的情况下,Aurora 中数据库节点上每个事务的 I/O 数量比 MySQL 少了 7.7 倍。对每个存储节点而言,写没有放大,因为它们只是 6 个副本中的 1 个,这导致存储层需要处理的 I/O 减少了 46 倍。


通过减少网络写数据的量,节省的资源可以让我们能够更积极地复制数据实现持久性和可用性,并且可以并行处理请求以最大程度减少抖动。

将处理下移到存储层也可以减少崩溃恢复时间,并消除由后台进程(如 checkpoint、后台刷页面、备份)等引起的抖动,从而提升了可用性。

以崩溃恢复为例,在传统数据库中,崩溃后必须从最近的 checkpoint 开始重放日志,以确保所有持久化的数据都被应用。在 Aurora 中,持久化的 redo log 记录在存储层分布在整个集群中,被持续地、异步地应用。

如果页面不是最新的,对数据的任何读请求都可以应用 redo log 记录执行回放。因此,崩溃恢复的处理分散在正常的前台请求中,数据库启动时不需要执行任何东西。

3.3 Storage Service Design Points

存储服务的核心设计原则是最小化前台请求的时延。我们将大部分存储处理移到后台,存储层处理的前台请求的平均峰值会有自然的波动,有足够时间在前台 IO 路径之外后台处理这些任务。还可以使用 CPU 资源换取磁盘资源。例如,当存储节点忙于处理前台请求时,不需要运行 GC 来回收老版本,除非磁盘资源快达到存储容量了。Aurora 中后台处理与前台处理之间的关联不强。这和传统的关系型数据库不同,传统关系型数据库后台的刷页面及 checkpoint 和前台的负载由很强的关联。如果系统出现任务积压,我们会限制前台任务,以防止积压队列过长。由于段是以高熵分布在存储节点中,对于 4/6 的共识允许很轻松地对一个节点限流,这个限流的节点看上去就像一个处理较慢的节点。

下面详细介绍一下存储节点上的处理流程。如图 4 所示,主要包括以下几步:(1)收取日志记录,加入到内存队列,(2)将日志持久化到磁盘(4/6),返回确认,(3)组织日志记录,识别日志中的缺失部分,因为可能有些日志记录丢失了,(4)与其他节点通信,获取日志缺失的部分,(5)将日志记录回放成新的数据页,(6)定期将日志和新页面存到 S3,(7)定期执行 GC 回收老版本,(8)定期执行页面上的 CRC 校验。


以上步骤都是异步的,只有(1)和 (2)两步在前台路径上,可能对时延有影响。

4. THE LOG MARCHES FORWARD

本节,讲述数据库引擎如何生成日志,以使得持久化状态、运行时状态及副本状态始终保持一致。同时还会介绍如何在没有使用代价高昂的 2PC 协议的情况下有效实现一致性。

首先,我们介绍如何避免在发生崩溃时进行代价高昂的 redo 处理。然后,介绍读写等操作的流程以及如何维护运行时和副本状态。最后,介绍恢复过程的细节。

4.1 Solution sketch: Asynchronous Processing

我们将数据库建模为重做日志流(redo log stream)如第 3 节所描述,我们可以利用日志是修改记录的有序序列这一事实。实现中,每个日志记录都有关联的日志序列号(LSN),由数据库生成并且单调递增。

这使得我们可以使用异步方式而不是 2PC 这样的协议来简化共识协议。在上层,我们维护一致性和持久化的点,并在收到存储层回复确认后推进这些点。由于单个存储节点可能缺失一些日志记录,节点间会通信查找填补缺失的项。数据库维护的运行时状态,允许对某个段读取不使用共识协议,除非出现状态丢失需要重建。

数据库中可能有多个相互隔离的未完成的事务,这些事务可以按不同的顺序达到完成和持久化状态。假设数据库崩溃或重启,每个未完成的事务单独确认是否回滚。

跟踪哪些事务没有执行完成,然后执行 undo 是数据库引擎的事情,就像简单地执行写磁盘一样。但是,在重启时,允许数据库访问存储卷之前,存储服务会执行自己的恢复流程,它关注的不是用户级的事务,而是确保数据库看到统一的存储视图,尽管存储本身是分布式的。

存储层确定可以保证之前的日志记录可用的最高 LSN (称为 VCL 或 Volume Complete LSN)。在存储恢复期间,必须截断 LSN 大于 VCL 的日志记录。但是数据库可以通过标记日志记录为 CPL (一致性点 LSN,Consistency Point LSN)来限制允许截断的日志点。我们将 VDL (Volume Durable LSN)定义为小于等于 VCL 的最大的 CPL,截断 LSN 大于 VDL 的所有日志记录。

例如,假设现在有到 LSN 为 1007 的所有日志记录,但是数据库声明只有 900、1000 和 1100 是 CPL,在这种情况下必须截断 LSN 1000 以后的记录。虽然 LSN 到 1007 是已经完成的,但是只有到 1000 的数据库认为是持久化了的。

完整性和持久性是不同的,CPL 可以视为必须按顺序接收的,某种受限的存储系统事务。如果数据库层不需要这种区分,可以简单地设置每条日志记录为 CPL。在实现中,数据库和存储的交互如下:

  1. 每个数据库层的事务被拆分为多个小事务(mini-transactions,MTRS),这些小事务是有序的且必须原子执行。MySQL中把对页面的一次原子访问的过程称之为一个Mini-Transaction,这里的原子操作,指的是要么全部成功,要么全部失败,不存在中间状态。
  2. 每个小事务由多条连续的日志记录组成(根据需要决定日志的条数)。
  3. 小事务最后一条日志记录是一个 CPL。
在恢复流程中,数据库和存储层交互确定每个 PG(保护组) 的持久化点,然后用它确定 VDL,然后发出命令截断 VDL 之后的日志记录。

4.2 Normal Operation

 现在介绍数据库引擎的“普通操作”,主要是写、读、事务提交和复制。

4.2.1 Writes

Aurora 中数据库持续和存储层交互,维护共识机制,实现卷持久化,并将事务注册为已提交。例如,在正常的路径中,当数据库收到共识组的写确认,它需要推进当前的 VDL。任意时刻,可能有大量的并发事务,这些事务各自生成自己的 redo log 记录。数据库为每个日志生成唯一有序的 LSN,同时保证没有分配 LSN 的值大于当前 VDL 和 LAL (LSN Allocation Limit)的和,当前 LAL 的值设置为 1000w。这个限制保证数据库层相对存储层不会跑的太快,同时还有反压机制,如果网络层和存储层无法跟上数据库层,则会限制写入。
需要注意的是每个 PG 中的段只能看到日志记录在卷上的一个子集。每个日志记录都有一个反向链接,用于指向该 PG 中前一条日志记录。这些反向链接可以用于跟踪到达的每个段中的日志记录的完整性,然后生成 SCL (Segment Complete LSN), SCL 是每个 PG 已经接收的日志记录中没有空洞的最大的 LSN 。节点间通信时通过 SCL 来查找和获得缺失的日志记录。

4.2.2 Commits

Aurora 中事务提交是完全异步的。客户端提交事务时,处理提交请求的线程将事务的 "commit LSN" 记录到单独的待提交的事务列表中,然后继续执行其他工作。与 WAL 协议等价的是,当且仅当最新的 VDL 大于等于事务的 commit LSN 时,才能完成提交。随着 VDL 推进,数据库识别到正在等待提交的事务,并通过专用线程向客户端发送提交确认。工作线程不会因为提交而等待,它们拉取其他的待处理请求并继续处理。

4.2.3 Reads

和大多数数据库一样,Aurora 中页面在内存中有缓存,只有当缓存中没有相关页面时才会产生对存储层的 I/O 。如果缓存满了,系统会找到一个页面从缓存中淘汰出去。在传统数据库中,被淘汰的页面是一个脏页,在被替换出去之前会刷盘。这是为了确保对该页面的后续访问能够获取最新的数据。但是 Aurora 中缓存淘汰不会写出页面,但也有类似的保证机制:缓存中的页面始终是最新版本。只有当页面的 "page LSN" (标识与页面最新的修改相关的日志记录)大于等于 VDL 时才会淘汰页面,可以保证缓存中的 page 都是最新的版本。这个协议确保:(a)页面内的所有修改已经在日志中记录,(b)缓存未命中时,请求当前 VDL 的页面版本就可以获取最新的持久化的版本。
正常情况下数据库不需要使用读仲裁。从存盘读取页面时,数据库会建立一个读取点(read-point),表示发出请求时的 VDL。然后,数据库可以选择一个相对于读取点来说是完整(complete)的存储节点,因为数据库知道从这个存储节点可以获取到最新的版本。存储节点返回的页面必须与数据库中的小事务(MTR)的预期语义一致。由于数据库直接管理向存储节点写日志,并且跟踪进度(即每个段的 SCL),因此数据库通常知道哪个段可以满足读取要求(SCL 大于读取点的段),所以可以直接向有足够数据的段发读请求。

鉴于数据库知道所有未完成的读操作,它可以随时基于每个 PG 计算最小的读取点的 LSN 。如果存在只读实例,写实例可以与之通信建立跨所有实例的每个 PG 的最小读取点的 LSN。这个值称为 PG 的最小读取点(Protection Group Min Read Point,PGMRPL),表示低于这个值的所有日志记录都是不必要的。换句话说,段上不会有页面的读取请求的读取点小于 PGMRPL。每个存储节点都知道数据库的 PGMRPL,因此可以通过合并老的日志记录来更新磁盘上的页面,然后安全地回收老版本的日志。实际的并发控制协议在数据库引擎执行,就像数据库页面和 undo 段是在本地存储一样,这与传统的 MySQL 很相似。

4.2.4 Replicas

Aurora 中一个写实例和最多 15 个读实例可以挂载到同一个共享的存储卷。读实例不会在写存储或磁盘方面有额外的开销。为了尽量减少时延,写实例生成的日志流发送到存储节点时也会发送到所有的读实例。读实例上,数据库通过依次确认每条日志记录来消费日志流。如果日志记录关联读实例缓存中的某个页面,则执行应用日志的逻辑来重做缓存中的页面,否则会丢弃日志记录。从写实例的角度而言,读实例回放日志是异步的,写实例响应用户提交与读实例上的操作无关。

副本在回放日志时需要遵循以下两个重要的规则:(a)只有 LSN 小于等于 VDL 的日志记录被应用,(b)属于某个小事务的所有日志记录以原子的方式应用到副本的缓存中,以确保副本看到一致的数据库对象。

实践中,每个读副本相对于写实例而言有短暂的延迟(20ms 甚至更短)。

4.3 Recovery

大多数传统数据库使用诸如 ARIES 之类的恢复协议,该协议依赖于可以表示所有已提交事务的精确内容的预写日志 (WAL) 的存在。这些系统还定期执行 checkpoint,通过将脏页刷新到磁盘并将 checkpoint 记录写入日志来以粗粒度方式建立持久性点。重启时,任何给定的页面都可能会丢失一些已提交的数据或包含未提交的数据。因此,在崩溃恢复时,系统从上次的 checkpoint 位置应用 redo log 到对应的页面上。这个过程使数据库页面在故障点处于一致状态,之后可以通过执行相关的 undo log 记录来回滚崩溃期间进行中的事务。崩溃恢复可能是一项代价很高的操作。减少 checkpoint 的时间间隔会有所帮助,但会干扰前台事务。而 Aurora 不需要这样的权衡。

传统数据库的一个很大的简化原则是,相同的 redo log 既可以用于正常的副本间数据同步,也可以用于异常时执行恢复。Aurora 中,也使用相同的设计,只是 redo log 执行器与数据库分离了,它运行在存储节点上,而且始终是后台运行。一旦数据库启动,redo log 执行器与存储服务协作执行卷恢复,因此 Aurora 中即使每秒处理超过 10w 条写入时发生崩溃,也可以非常快速地恢复(通常在 10s 以内)。

数据库在崩溃后确实需要重新构建运行时状态。在这种情况下,数据库与每个 PG 通信,获得达到读共识的段的集合,这些段足够保证能够读到那些已经达成写共识的数据。一旦数据库为每个 PG 建立了读共识,它可以通过生成一个截断范围来重新计算 VDL,超过这个 VDL 的数据会被截断,新的 VDL 之后直到且包括数据库可能见过的最高的未完成记录的 LSN 之间的数据全部截断。数据库推断出这个上限,因为 LSN 是由数据库分配的,并且限制分配的 LSN 可以在 VDL 之后多远(前面描述过,目前的值限制为 1000W)。截断范围使用 epoch 编号进行版本控制,并持久化到存储服务,以便在恢复中断和重启时不会混淆截断范围的持久性。

数据库仍然需要为发生崩溃时进行中的事务执行 undo 恢复。但是,数据库可以在系统从 undo 段中构建出崩溃时执行中的事务列表后,在线执行 undo 恢复。

5. PUTTING IT ALL TOGETHER

本节介绍 Aurora 的模块图,如图 5 所示。


数据库引擎是社区 MySQL/InnoDB 的一个分支,主要的区别在 InnoDB 如何读取和写入数据到磁盘。社区的 InnoDB,写操作导致缓存中的页面被修改,关联的 redo log 记录按照 LSN 的顺序写入 WAL 缓冲区。事务提交时,WAL 协议只要求 redo log 记录被持久化到磁盘。实际修改的缓存页面最终也通过双写(double-write)技术写入磁盘,以避免页面的部分写。这些页面的写入发生在后台,或者是发生缓存淘汰时,或者是 checkpoint 时。除了 IO 子系统,InnoDB 还包括事务子系统、锁管理、B+-tree 实现和“迷你事务”(MTR)等相关内容。MTR 是一种仅在 InnoDB 内部使用的结构,它对必须以原子方式执行的操作集合进行建模(例如,B+-tree 的分裂/合并)。

Aurora 的 InnoDB 中,每个 MTR 中表示必须被原子执行的 redo log 记录被组织成批次,这些批次被日志记录所属的 PG 集合共享,这些批次被写到存储服务中。每个 MTR 的最后一条日志被标记为一个一致性的点。Aurora 写支持与社区 MySQL 完全一样的隔离级别(标准的 ANSI 级别和快照隔离或一致性读)。Aurora 中的只读副本从写实例获取事务开启和提交的信息,来支持本地事务的快照隔离,这些事务当然是只读的。注意,并发控制完全在数据库引擎实现,不会影响存储服务。存储服务提供底层数据的统一视图,在逻辑上和通过社区 InnoDB 写入到本地存储的结果相同。

Aurora 将 Amazon Relational Database Service (RDS) 用于其控制面。RDS 在数据库实例上包含一个称为主机管理器 (HM) 的代理,它监控集群的运行状况并确定它是否需要故障转移,或者是否需要切换实例。
每个数据库实例是由单个写实例和零个或多个只读副本组成的集群的一部分。集群的实例位于单个地理区域,(例如,us-east-1、us-west-1 等),通常放置在不同的 AZ 中,并连接到同一区域中的存储集群。
为了安全起见,我们隔离了数据库、应用程序和存储之间的通信。在实践中,每个数据库实例可以在三个 Amazon Virtual Private Cloud (VPC) 网络上进行通信:客户应用程序与数据库实例交互的 Customer VPC、数据库实例和控制面交互的 RDS VPC 以及数据库与存储服务交互的 Storage VPC

存储服务部署在一个 EC2 虚拟机集群上,虚拟机在每个区域的至少 3 个 AZ 上进行配置,共同负责配置客户存储卷、向这些卷读取和写入数据,以及从这些卷中备份和恢复数据。存储节点操作本地 SSD 并与数据库引擎实例、其他对等存储节点以及备份/恢复服务进行交互,其中备份/恢复服务将更改的数据持续备份到 S3 并根据需要从 S3 恢复数据。存储控制面使用 Amazon DynamoDB 数据库服务来持久存储集群和存储卷配置、卷元数据以及备份到 S3 的数据的详细描述信息。为了管理需要长时间执行的操作,例如存储节点故障后的数据库卷恢复操作或修复(重新复制)操作,存储控制平面使用 Amazon Simple Workflow Service。保持高可用性需要在最终用户受到影响之前主动、自动和及早地检测已经发生和潜在问题。使用指标收集服务持续监控存储操作的所有关键点,如果关键性能或可用性指标存在问题,则会发出警报。

6. PERFORMANCE RESULTS

本节介绍使用 Aurora 作为生产环境的一些经验。首先介绍业界的一些标准 benchmark 的执行结果,然后展示客户的一些性能数据。

6.1 Results with Standard Benchmarks

在此展示 Aurora 和 MySQL 执行业内的一些标准 benchmark 的结果对比,如 SysBench 和 TPC-C。

最后修改时间:2023-04-16 10:30:36
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论