译者:Aceking,极数云舟技术合伙人,数据库内核研发专家,负责企业级云原生数据库ArkDB等核心数据库产品,华中科技大学计算机系数据库方向研究生毕业,原达梦数据库产品研发工程师,负责达梦数据库内核开发,长期致力于数据库原理和源码开发,精通C/C++,公司内部流传有他的名言:以后再也不敢说精通C++了。

摘要

Amazon Aurora是亚马逊网络服务(AWS)的一个组成部分,对外提供一个OLTP负载类型的关系数据库服务。本文描述这种架构设计考量。我们确信,高吞吐量的数据处理的主要约束已经从计算、存储转移到网络中来了。Aurora给出一个新颖的架构来解决这个约束,其最显著的特点是把重做日志(redo)处理下放到专门为Aurora设计的存储服务中。文章会介绍如何既能减少网络流量,又能快速崩溃恢复(crash recovery), 还能实现复制失效不损失数据,以及存储上的容错,自愈。然后介绍Aurora在多存储节点中的保持持久状态的一致性的高效异步方案,避免了代价高昂繁复的恢复协议。最后,我们分享了18个多月运营Aurora产品的经验,这些经验是从现代云应用的客户们对数据库层的期望中总结的。

关键字

数据库; 分布式系统; 日志处理; 仲裁模型; 复制; 恢复; 性能; OLTP

01 导论

越来越多的IT负载转移到公有云中,这种全行业的转变的重大原因包括:公有云服务商能够弹性按需提供容量,以及只需支付运营费用而不用支付资产费用的模式。许多IT负载需要一个OLTP的关系型数据库。因此,提供一种等效甚至超越的预置数据库用以支持这种转变显得至关重要。

越来越多现代分布式云服务,通过解耦计算与存储,以及跨越多节点复制的方式实现了弹性与可伸缩性。这样做,可以让我们进行某些操作,例如替换失误或者不可达的主机,添加复制节点,写入节点与复制节点的故障转移,拓展或者收缩数据库实例的大小等等。在这种环境下,传统的数据库系统所面临的IO瓶颈发生改变。因为IO可以分散到多租户集群中的多个节点和多个磁盘中,导致单个磁盘和节点不在是热点。相反,瓶颈转移到发起这些IO请求的数据库层与执行这些IO请求的存储层之间的网络中。除了每秒包数(packets per second , PPS)以及带宽这种基本瓶颈外,一个高性能的数据库会向发起存储机群并行的写入,从而放大了网络流量。某个异常存储节点的性能,磁盘或者网络通路,都会严重影响响应时间。

虽然数据库大部分的操作可以互相重叠执行,但是有一些情况需要同步操作,这会导致停顿和上下文切换(可能是线程上下文切换,也可以是内核态与用户态切换--译者注)。情况之一,数据库由于缓冲缓存(buffer cache)未命中,导致进行磁盘读,读线程无法继续只能等待磁盘读取完成。将脏页强制赶出并且刷盘,用以给新页腾出空间。也会导致缓存(cache)不命中。虽然后台处理,例如检查点,脏页写入线程能减少这种强制行为,但是仍然会导致停顿,上下文切换以及资源争用。

事务提交是另一个干扰源。一个正在提交的事务的停顿,会阻止另一个事务的进行。处理多节点同步协议的提交,如两阶段提交(2PC)对云级规模(cloud-scale)的分布式系统来说更是一个挑战。这些协议不能容忍失败。但是高规模(high-scale)总是充斥着软硬件失效的“背景噪声”。同时还有高延迟,因为高规模系统总是跨多个数据中心分布的。

本文中,我们介绍Amazon Aurora,一种新型的数据库服务,通过大胆激进的使用高分布性的云环境中的日志来解决上述问题。我们给出了一种使用多租户可伸缩的存储面向服务的架构(图1),这种存储服务与数据库实例集群松耦合,并且从中提取虚拟的分段的重做日志(redo log).虽然每个数据库实例仍然有大部分的传统的数据库内核(查询处理,事务,锁,缓冲缓存,访问方法,回滚管理), 但是一些功能(重做日志,持久存储,崩溃恢复,备份恢复)已经剥离,放到存储服务中。
Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

我们的架构与传统方法相比,有三大明显优势:首先,把存储建为一个容错、自愈、跨多个数据中心的独立服务,我们可以保护数据库,使得网络层或存储层的性能差异,暂时或永久的失效,对数据库不再有影响。我们观察到持久存储的失效可以作为一个长时-可用性事件(longlasting availability event)来建模, 而可用性事件可以作为一个长时性能变动来建模,长时性能变动已经有一个设计良好的系统统一处理。 其次,通过存储写日志的方式,可以把网络的IOPS减少一个量级。一旦我们移除了这个瓶颈,就可以大胆的在大量的其他争用点上做优化,就能获取比我们基于的MySQL基线源码更明显的吞吐量的提升。第三,我们把一些最重要最复杂的功能(如:备份、日志恢复)从数据库引擎中一次性代价高昂的操作转变为分摊到大型分布式机群的连续异步的操作。从而获得近乎瞬时的崩溃恢复,无需检查点,同时,备份的代价也不高昂,不会影响前台处理。

本文介绍了我们的三点贡献

如何分析云级规模下的持久性,如何设计具有对相关失效具有弹性的仲裁系统。(段2)

如何使用灵巧的分离存储,使得到存储的负载低于传统的1/4。(段3)

如何在存储分布式系统中消除多阶段同步,崩溃恢复,检查点。(段4)

我们把三个创意组合在一起,设计出Auraro总体架构(段5),随后(段 6)检视我们的性能结果,(段7)展示我们的运营经验,(段8)概述相关的工作,(段9)给出结束语。

02 可伸缩的持久性

如果数据库不做其他的事情,必须满足如下要求:一旦写入,必须可读。不是所有系统可以做到这一点。在本段中我们讨论了aurora仲裁模型背后的原理,为什么我们要将存储分段,如何将持久性与可获得性结合在一起,减少抖动,并且帮助解决大规模存储机群的运营问题。

2.1 复制与相关失效

实例的生命周期与存储的生命周期并没有太大的相关。实例失效,用户会将其关闭,他们会根据负载情况调整其大小。这些特点有助于我们将计算层与存储层解耦。

一旦你这样做了,这些存储节点和磁盘仍然会失效,因此需要某种形式的复制实现对失效的弹性。在规模大的云环境下,存在持续的下层背景噪声,如节点失效,磁盘、网络路径失效。每一种失效有不同的持续时间和破坏半径。比如,某个节点暂时无法网络到达,重启导致的临时停机时间,磁盘,节点,机架,网关的叶节点或者主干节点, 甚至整个数据中心的永久失效。

在存在复制的系统中,实现容错使用一种[]基于仲裁的投票协议。如果一个复制数据项有V个副本,每个副本可以赋予一个投票,一次读操作或者写操作分别需要Vr个投票的读仲裁,或者Vw个投票的写仲裁。那么要达到一致性,仲裁必须两个规则,首先,每次读操作必须能获得最新的修改。公式为:

Vr+Vw > V,

这个规则,保证读取的节点集与写节点集有交集,读仲裁至少能读到一个最新版本的数据副本。其次,每次写必须占副本多数,用以避免写冲突。即公式:

Vw > V/2,

一个能容忍复制数据项(V=3)一个节点损失的通用的方法是,写需要2/3的投票仲裁(Vw=2),读也需要2/3投票仲裁(Vr=2)。

我们确信2/3的投票仲裁数是不充分的,为了解释为什么,先了解AWS的一个概念,可用区(AZ,availability zone)是区域(region)的子集。一个可用区与其他可用区可以低延迟的连接,但是可以隔离其他可用区的故障,例如,电源,网络,软件部署,洪水等等。跨AZ的分布复制数据可以保证典型的大规模故障模式只影响一个复制副本,这意味着,可以简单地放三个副本到在不同的可用区,就可以支持大范围的事件容错,但是少量的个别失效事件除外。

然而,在一个大的存储机群,故障的背景噪声意味着,任意给一个时刻,都会有一个节点或者磁盘的子集可能坏掉并修复。这些故障可能在可用区A、B、C中独立分布。然而,可用区C由于火灾、洪水、屋顶倒塌等等,与此同时,可用区A或者B也有故障(故障背景噪声),就打破了任意一个副本的仲裁过程。在这一点上,2/3票数的读取仲裁模型,已经失去了两个副本,因此无法确定第三个副本是否是最新的。换一句话说,虽然,每个可用区的个体复制节点故障互不相关,但是一个可用区的失效,则与可用区所有的节点和磁盘都相关。仲裁系统要能容忍故障背景噪声类型的故障与整个可用区的故障同时发生的情况。

在Aurora中,我们选用一种可以容两种错误的设计:(a)在损失整个可用区外加一个节点(AZ+1)情况下不损失数据。

(b)损失整个可用区不影响写数据的能力。

在三个可用区的情况下,我们把一个数据项6路写入到3个可用区,每个可用区有2个副本。使用6个投票模型(V=6),一个写仲裁要4/6票数(Vw=4),一个读仲裁需要3/6票数(Vr=3),在这种模型下,像(a)那样丢失整个可用区外加一个节点(总共3个节点失效),不会损失读取可用性,像(b)那样,损失2个节点,乃至整个可用区(2个节点),仍然保持写的可用性。保证读仲裁,使得我们可以通过添加额外的复制副本来重建写仲裁。

2.2 分段的存储

现在我们考虑AZ+1情况是否能提供足够的持久性问题。在这个模型中要提供足够的持久性,就必须保证两次独立故障发生的概率(平均故障时间,Mean Time to Failure, MTTF)足够低于修复这些故障的时间(Mean Time to Repair, MTTR)。如果两次故障的概率够高,我们可能看到可用区失效,打破了冲裁。减少独立失效的MTTF,哪怕是一点点,都是很困难的。相反,我们更关注减少MTTR,缩短两次故障的脆弱的时间窗口。我们是这样做的:将数据库卷切分成小的固定大小的段,目前是10G大小。每份数据项以6路复制到保护组(protection Group,PG)中,每个保护组由6个10GB的段组成,分散在3个可用区中。每个可用区持有2个段。一个存储卷是一组相连的PG集合,在物理上的实现是,采用亚马逊弹性计算云(ES2)提供的虚拟机附加SSD,组成的大型存储节点机群。组成存储卷PG随着卷的增长而分配。目前我们支持的卷没有开启复制的情况下可增长达64TB。

段是我们进行背景噪声失效和修复的独立单元。我们把监视和自动修复故障作为我们服务的一部分。在10Gbps的网络连接情况下,一个10GB的段可以在10秒钟修复。只有在同一个10秒窗口中,出现两个段的失效,外加一个可用区的失效,并且该可用区不包含这两个段,我们才可以看到仲裁失效。以我们观察的失效率来看,几乎不可能,即使在为我们客户管理的数据库数量上,也是如此。

2.3 弹性的运营优势

一旦有人设计出一种能对长时失效自然可复原(弹性)的系统,自然也对短时失效也能做到可复原。一个能处理可用区长时失效的系统,当然也能处理像电源事故这样的短时停顿,或者是因糟糕的软件部署导致的回滚。能处理多秒的仲裁成员的可用性的损失的系统,也能处理短时的网络的阻塞,单个存储节点的过载。

由于我们系统有高容错性,可以利用这点,通过引起段不可用,做一些维护工作。比如 热管理就直截了当,我们直接把把热点磁盘或节点的一个段标记为坏段,然后仲裁系统通过在机群中迁移到较冷的节点来修复。操作系统或者安全打补丁在打补丁时,也是一个短时不可用事件。甚至,软件升级也可以通过这个方式做。在某个时刻,我们升级一个AZ,但是确保PG中不超过一个成员(段或者节点)也在进行补丁。因此,我们的系统可以用敏捷的方法论在我们的存储服务中进行快速部署。

03 日志即数据库

本段解释为什么传统的数据库系统在采用段2所述的分段的复制存储系统,仍难以承受网络IO和同步停顿导致的性能负担。我们解释了将日志处理剥离到存储服务的方法,以及实验证实这种方法如何显著的降低网络IO,最后讲述了最小化同步停顿和不必要写操作的各种技术。

3.1 写放大的负担

我们的存储卷的分段存储,以及6路写入4/6仲裁票数的模型,具有高弹性。但是不幸的是,这种模型会让传统的数据库如MYSQL无法承受这种性能负担,因为应用往存储做一次写入,会产生存储中许多不同的真实IO。高量的IO又被复制操作放大,产生沉重的PPS(每秒包数,packets per second)负担。同时,IO也会导致同步点流水线停滞,以及延迟放大。虽然链复制[8]以及其他替代方案能减少网络代价,但是仍然有同步停顿以及额外的延迟。让我们看看传统的数据库写操作是怎么进行的把。像Mysql这样的系统,一边要向已知对象(例如,heap文件,B-数等等)写入页,一边要像先写日志系统(write-ahead log, WAL)写入日志。日志要记录页的前像到后像的修改的差异。应用日志,可以让页由前像变为后像。
而实际上,还有其他的数据也要写入。举个例子,考虑一下如图2所示以主从方式实现的同步镜像mysql配置, 用以实现跨数据中心的高可用性。AZ1是主mysql实例,使用亚马逊弹性块存储(EBS),AZ2是从Mysql实例,也用EBS存储,往主EBS的写入,均要通过软件镜像的方法同步到从EBS卷中。

Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

图2所示,引擎需要写入的数据类型:重做日志,binlog日志,修改过数据页,double-write写,还有元数据FRM文件。图中给了如下的实际IO顺序:

步骤1步骤2,像EBS发起写操作,并且会像本地可用区的EBS镜像写入,以及操作完成的响应消息。

步骤3,使用块级镜像软件将写入转入到从节点。

步骤4和5从节点将传入的写入操作执行到从节点的EBS和它的EBS镜像中。

上述MySQL的镜像模型不可取,不仅仅因为它如何写入的,也因为它写进了什么数据。首先,1,3,5步骤是顺序同步的过程,由于有许多顺序写入,产生了额外的延迟。抖动也被放大,哪怕是在异步写的时候,因为它必须等待最慢的操作,让系统任凭异常节点的摆布。按照分布式视角来看,这个模型可以视作4/4的写仲裁,在失效或者异常节点的异常性能面前,显得十分脆弱。其次,OLTP应用产生的用户操作,可以产生许多不同类型的写入,而这些数据写入方式虽然都不同,但是却表示同样的信息。例如,往双写缓冲的写入,主要是为了防止存储层页的损坏,(但是内容与页的普通写入是一样的)

3.2 日志处理分离到存储

传统数据库修改一个页,就会产生日志记录。调用日志应用器,应用日志到内存中该页数据的前像,得到该页的后像。事务提交前日志必须已经写入,而数据页则可推后写入。在Aurora中,通过网络的写入只有重做日志。数据库层不写入数据页,也没有后台写入,没有检查点,没有Cache替换。相反,日志应用器已经放置到存储层,存储层在后台生成,或者按需生成数据库的数据页。当然,从头开始应用所有修改的,代价让人难以承受,因此,我们在后台持续生成数据页以避免每次抓取时在重新生成这些页。从正确性的角度来看,后台生成是完全可选的:就引擎而言,日志即数据库 ,存储所有具体化出来的页,都可以视作日志应用的缓存。与检查点不同,只有那些很长修改链的页才会要重新具化 ,检查点由整个日志链长度控制,而Aurora由给定页的日志链长度来控制。

尽管复制会导致的写放大,我们的方法明显能减少网络负载,而且保证性能与持久性。在令人尴尬的并行写入,存储能超载IO而不影响数据库引擎的写吞吐能力。举个例子,图3所示,是一个一主多从Aurora集群,部署在多个可用区中。在这个模型中,主库往存储写入日志,并且把这些日志与元数据更新发送给从节点。基于公共存储目标(一个逻辑段,比如,PG)的一批批日志都是完全有序的。把每个批次以6路传输给复制节点,存储引擎等待4个或4个多的回应,用以满足写仲裁条件,来判断日志在持久上或硬化上是否有问题。复制节点应用这些日志记录来更新自身的缓存缓冲。

Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

为了测量网络IO的状况,我们在以上所述的两种mysql配置下,进行了100GB的数据集只写负载的sysbench[9]测试:一种是跨多个可用区的mysql镜像配置,另一个是Aurora的RDS,在r3.8large EC2的实例中运行了30分钟。

实验结果如表1所示。在30分钟的时间里,在事务上,Aurora比mysql镜像多35倍,虽然Aurora的写放大了6倍,但是每个事务写IO仍然比mysql镜像小7.7倍。这里并没有记录EBS的链式复制与mysql跨可用区的写。每个存储节点,只是6个复制节点中的一个,因此看不到写放大。这里的IO数量减少了46倍。写入了更少的数据,省下的网络能力,可以激进地通过复制实现持久性与可用性,并在发起并行请求时,最小化抖动的影响。
Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

把日志处理放到存储服务中,也可以最小化崩溃恢复的时间,来提高可用性,并且消除检查点、后台写、备份等后台处理所带来的抖动。我们再看崩溃恢复。传统数据库在崩溃之后,必须从最近的检查点开始,将日志重演并要报保证所有的保存的日志都被应用。在Aurora中,持续的日志应用发生在存储中,并且它是持续的,异步的,分散在机群中的。任何一次读页请求,如果该页不是当前版本,则要应用一些重做日志。因此,崩溃恢复过程已经分散到所有的正常的前台处理中,不需要在数据库启动的时候执行。

3.3 存储服务的设计点

我们的存储服务设计的核心原则是最小化前台写请求的延迟。我们把大部分的存储处理移到了后台。鉴于存储层前台的请求峰值与平均值之间的自然变动,我们有足够的时间在前台请求之外进行这些处理。而且我们也有机会用cpu换磁盘。比如说,当存储节点忙于前端请求处理,就没有必要进行旧页的垃圾回收(GC),除非该磁盘快满了。Aurora中,后台处理与前台处理负相关。与传统的数据库不一样,传统数据库的后台写页、检查点处理与系统的的前台的请求量正相关。如果系统中有积压请求,我们会抑制前端请求,避免形成长队列。因为系统中的段以高熵的形式(高混乱程度)分布在不同的存储节点中,限制一个存储节点在4/6仲裁系统中自然的作为慢节点处理。

现在以更多的细节来观察存储节点各种活动。如图4所示,包含了如下若干步骤:

1、接收日志记录并且放入内存队列中。

2、保存到磁盘并且返回应答。

3、组织日志记录,并且识别日志中的空白,因为有些批次的日志会丢失。

4、与对等节点交流(gossip)填住空白。

5、合入日志到新的数据页。

6、周期地将新数据页和日志备份到S3。

7、周期的回收旧版本。

8、校验页中的CRC编码。

注意,以上各个步骤不全是异步的,1 和 2 在前台路径中有潜在的延迟影响。

Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

04 日志前行

本段中,我们介绍数据库引擎生成的日志如何在持久状态,运行状态,复制状态始终保持一致。特别是,如何不需要2PC协议高效地实现一致性。首先,我们展示了如何在崩溃恢复中避免使用高昂的重做处理。我们解释正常操作中如何维护运行状态与复制状态。最后,我们披露崩溃恢复的细节。

4.1 解决方案草图:异步处理

因为我们把数据库建模为一个日志流,事实上日志作为有序的修改序列,这点我们也要利用。实际上,每个日志都有与之关联的日志序列号(Log Sequence Number, LSN), LSN是由数据库产生的单调递增的值。

这就可以使我们简化共识协议,我们采用异步方式协议而不是2PC协议。2PC协议交互繁复并且不容错。在高层次上,我们维护一致性与持久化的点,在接收还未完成的存储请求的响应时推进这些点,并且是持续的推进。由于单独的存储节点可能会丢失一个或者多个日志记录,它们可以与同PG的其他成员进行交流,寻找空白并填补空洞。运行状态是由数据库维护的,我们可以直接从存储段读取而不需要读仲裁。但在崩溃恢复时候,运行状态已丢失需要重建的时候除外,这时候仍需要读仲裁。

数据库可能有很多独立的未完成的事务,这些事务可以与发起时顺序完全不同的顺序完成(达到持久化的结束状态)。假设数据库奔溃或重启,这些独立事务是否回滚的决策是分开的。则跟踪并撤销部分完成事务的逻辑保留在数据库引擎中,就像写入简单磁盘一样。在重启过程中,在允许数据库访问存储卷之前,存储进行自身的崩溃恢复,而不关心用户层的事务。当然,要保证数据库看到的存储系统是单一的视图,尽管实际上存储是分布式的。

存储服务决定一个最高的LSN,保证在在这个LSN之前的日志记录都是可以读取的。(VCL,volume Complete LSN)。在恢复过程中,对应LSN任何高于VCL的重做日志都要丢弃。但是,数据库还可以进一步约束条件,取其子集,用以标记哪些日志可以丢弃。这个子集就是CPL集(Consistency Point LSNs)。因此,我们还可以定义一个VDL(volume durable LSN)为小与VCL的最大CPL。举个例子,虽然数据完成到LSN为1007,但是数据库标记的CPL集为900,1000,1100。在这种情况下,我们丢弃的位置为1000(1000以后的都要丢弃),也就是我们完成到1007(VCL),但是持久到1000(VDL)。

因此,完成性与持久性是不同的。CPL可以看作存储事务必须有序接受的某种限制形式。如果客户端没有对此加以区别,我们把每个日志记录标记为一个CPL。实际上,数据库与存储以如下方式交互:

每个数据库层面的事务,都被打断为多个迷你事务(mini-transactions,MTRs),迷你事务是有序的,而且是原子的方式执行(就是不可分割地执行)。

一个迷你事务由多条连续的日志记录组成(要多少有多少)。

迷你事务的最后一条日志记录就是一个CPL

在崩溃恢复过程中,数据库告诉存储层为每个PG建立一个持久点,用来建立一个VDL,然后发起丢弃高于VDL日志的命令。

4.2 正常操作

我们现在描述数据库的“正常操作”, 依次关注写、读、提交与复制。

4.2.1 写

在Aurora中,数据库不停地与存储服务交互,并且维护状态以建立仲裁,推进卷持久性(volume durablity),并且在提交时注册事务。举个例子,在正常/前转路径中,当数据库接收到响应消息为每批日志建立写仲裁,则推进当前VDL。在任意给定时刻,数据库可能有大量的并发事务活动。每个事务均产生日志,数据库又为每条日志分配唯一的有序的LSN,但是分配受限于一条原则,LSN不能大于当前VDL与一个常量值的和。这个常量称为LSN分配限值(LAL, LSN Allocation limit)(目前设置为一千万)。这个限值保证数据库的LSN不超出存储系统太远,在存储或者网络跟不上时,提供一个反压,用以调节来入的写请求。

注意,每个PG的每个段仅仅能看到存储卷日志记录的子集,该子集的日志仅仅影响驻留于该段的页。每个日志记录包含一个指向PG中前一个日志记录的反向链接。这些反向链接可以跟踪达到每个段的日志记录的完成点,用以建立段完成LSN(Segment Complete LSN, SCL), SCL是表示一个最大LSN,在该LSN之下所有的日志记录均已经被该PG接收到。存储节点使用SCL相互交流,用来查找并交换获得自己确实的那部分日志。

4.2.2 提交

在Aurora中,事务提交是异步完成的。当客户端提交一个事务,处理事务提交的线程记录一个“提交LSN”(commit lsn)到一个单独的等待提交的事务列表中,然后将事务搁置去处理其他任务。这相当于WAL协议是基于事务提交的完成,也即当且仅当最新的VDL大于或等于事务的提交LSN的时候。随着VDL的推进,数据库在正在等待提交事务识别符合条件的,由专有线程向正在等待的客户端发送提交响应消息。工作线程不会因为提交事务而暂停,它们仅是把其他的挂起的请求拉起继续处理。

4.2.3 读

在Aurora中,和大部分的数据库一样,页由buf和cache提供。只有页是否在Cache中还存疑的时候,才会导致存储IO请求。

如果buf cache已满,系统找出受害页,将其赶出cache。在传统数据库中,如果受害页是“脏页“,替换前要刷盘。这就保证后续的页的提取,总是最新数据。然而Aurora在驱逐页的时候并没有将页写出,但它强制执行一个保证:在buff或cache中的页始终是最新的版本。实现这一保证的措施是,仅仅将“页LSN(Page LSN)“(与页相关最新的日志LSN)大于或者等于VDL。这个协议保证了:(a)页的所有修改在日志中都已硬化, (b)在cache没命中的时候,获得最近的持久化版本的页,请求当前VDL版本页就足够了。

数据库在正常情况下不需要使用读仲裁来建立一致性。当从磁盘读取一个页,数据库建立一个读取点(read-point)

表示读取发起时候的VDL。数据库可以选择一个相对于读取点数据完整存储节点,从而取得到最新版本的数据。存储节点返回的数据页一定与数据库的迷你事务(mtr)的预期语义一致。因为,数据库管理着往存储节点日志的馈送,跟踪这个过程(比如,每个端的SCL),因此它知道那些段满足读取要求(那些SCL大与读取点的段)。然后直接向有足额数据的段发起读请求。

考虑到数据库知道那些读操作没有完成,可以在任何时间在每个PG的基础上计算出最小的读取点LSN(Minimum Read Point LSN). 如果有存储节点交流写的可读副本,用来建立所有节点每个PG的最小读取点,那么这个值就叫做保护组最小读取点LSN(Protection Group Min Read Point LSN, PGMRPL). PGMRPL用来标识“低位水线”,低于这个“低位水线”的PG日志记录都是不必要的。换句话说,每个存储段必须保证没有低于PGMRPL的读取点的页读请求。每个存储节点可以从数据库了解到PGMRPL,因此,可以收集旧日志,物化这些页到磁盘中,再安全回收日志垃圾。

实际上,在数据库执行的并发控制协议,其数据库页和回滚段组织方式,与使用本地存储的传统数据库的组织方式并无二致。

4.2.4 复制节点(replicas)

在Aurora中,在同一个存储卷中,一次单独的写最多有15个读复制。因此,读复制在消耗存储方面,并没有增加额外的代价,也没有增加额外的磁盘写。为了最小化延迟,写生成的日志流除了发送给存储节点,也发送所有的读复制节点。在读节点中,数据库依次检查日志流的每一天日志记录。如果日志引用的页在读节点的缓冲缓存中,则日志应用器应用指定的日志记录到缓存中的页。否则,简单丢弃日志记录。注意从写节点的视角来看,复制节点是异步消费日志流的,而写节点独立于复制节点响应用户的提交。复制节点在应用日志的时候必须遵从如下两个重要规则:(a)只有LSN小于或等于VDL的日志记录才能应用。(b)只有属于单个迷你事务(MTR)的日志记录才能被应用(mtr不完整的日志记录不能应用),用以保证复制节点看到的是所有数据库对象的一致视图。实际上,每个复制节点仅仅在写节点后面延迟一个很短的时间(20ms甚至更少)。

4.3 恢复

大部分数据库(如ARIES)采用的恢复协议,取决于是否采用了能表示提交事务所有的精确的内容的先写日志(write ahead log,WAL)。这些系统周期的做数据库检查点,通过刷脏页到磁盘,并在日志中写入检查点记录,粗粒度地建立持久点。在重启的时候,任何指定的页都可能丢失数据,丢失的数据可能是已经提交了的,也可能包含未提交的。因此,在崩溃恢复的过程中,系统从最近的检查点开始处理日志,使用日志应用器将这些日志应用日志到相关数据库页中。通过执行相应的回滚日志记录,可以回滚崩溃时正在运行的事务,这个过程给数据库带来了故障点时候的一致性状态。崩溃恢复是一个代价高昂的操作,减少检查点间隔可以减少代价,但是这会带来影响前端事务的代价。传统数据库需要在两者做权衡,而Aurora则不需要这样的权衡。

传统数据库有一个重要的简化原则是,使用同一个日志应用器,推进数据库状态与进行同步的日志恢复。并且此时从前端看来,数据库库是脱机的。Aurora设计也遵循同样的原则。但是日志应用器已经从数据库中解耦出来,直接在存储服务中在总是在后台并行运行。一旦数据库开始执行卷恢复,它会与存储服务合作来进行。结果是,Aurora数据库恢复速度非常快(通常在10秒以下),哪怕是每秒运行100,000条语句时候崩溃后情况下做恢复。

Aurora数据库不需要在崩溃之后重建运行状态。在崩溃的情况下,它会联系每个PG,只要数据满足了写仲裁的条件,段的读仲裁完全可以保证能发现这些数据。一旦数据库为每个PG建立了读仲裁,则可以重新计算VDL,生成高于该VDL的丢弃范围(truncation range),保证这个范围后的日志都要丢弃,丢弃范围是这是数据库能可以证明的最大可能保证未完成的日志(不是完整MTR的日志)可见的结束LSN。然后,数据库可以推断出LSN分配的上限,即LSN的上限能超过VDL多少(前面描述的一千万)。丢弃范围使用时间数字(epoch number)作为版本,写在存储服务中,无论崩溃恢复还是重启,都不会弄混丢弃日志的持久化。

数据库仍然需要做undo恢复,用来回滚崩溃时正在进行的事务。但是,回滚事务可以在数据库联机的时候进行,在此之前,数据库已经通过回滚段的信息,建立了未完成事务列表。

05 放在一起

本段中,我们描述如图5所示Aurora的各个模块。

Aurora库是从社区版的mysql/innodb数据库fork出来的分支,与之不同的主要在读写磁盘数据这块。在社区版的Innodb中,数据的写操作修改buffer中的页,相应的日志也按LSN顺序写入WAL的缓存中。在事务提交的时候,WAL协议只要求事务的日志记录写入到磁盘。最终,这个被修改的缓存页使用双写技术写入到磁盘中,使用双写的目的是为了避免不完整的页写(partial page writes)。这些写操作在后台执行,或者从cache驱逐页出去时候执行,或者在检查点时候执行。除IO子系统外,innodb还包含事务子系统,锁管理器,B+树的实现,以及“迷你事务”(MTR)。innodb的将一组不可分割执行(executed atomically)的操作称为一个迷你事务(例如,分裂/合并B+树页)。

Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

在Aurora的Innodb变种中,mtr中原先那些不可分割执行日志,按照批次组织,分片并写入到其所属的PG中,将mtr最后一个日志记录标记为一致点(consistency point), Aurora在写方面支持与社区mysql一样的隔离等级(ANSI标准级别,Snapshot 隔离级,一致性读)。Aurora读复制节点可以从写节点获取事务开始和提交的持续信息,利用这些信息支持本地只读事务的snapshot隔离级。注意,并发控制是完全有数据库实现的,对存储服务没有任何影响。存储服务为下层数据提供了展现了一个统一视图,与社区版本innodb往本地存储写数据的方式,在逻辑上完全等效。

Aurora使用亚马逊关系数据库服务(RDS)作为控制平台。RDS包含一个数据库实例上的代理,监控集群的健康情况,用以决定是否进行故障切换,或者是否进行实例替换。而实例可以是集群中的一个,集群则由一个写节点,0个或者多个读复制节点组成。集群中所有的示例,均在同一个地理区域内(例如,us-east-1,us-west-2等)。但是放置在不同的可用区内。连接同一个区域的存储机群。为了安全起见,数据库、应用、存储之间的连接都被隔离。实际上,每个数据库实例都可以通过三个亚马逊虚拟私有云(VPC)进行通信:用户VPC,用户应用程序可以通过它与数据库实例进行交互。RDS VPC,通过它数据库节点之间,数据库与控制平台之间进行交互。存储VPC,用以进行数据库与存储之间的交互。

存储服务部署在EC2虚拟机集群中。这些虚拟机在一个区域内至少跨三个可用区,共同负责多用户提供存储卷、以及这些卷的读写、备份、恢复。存储节点负责操作本地SSD,与对等节点或数据库实例进行交互,以及备份恢复服务。备份恢复服务持续的将数据的改变备份到S3,并且依据需求从S3恢复。存储控制平台使用亚马逊DynamoDB服务存储集群的永久数据、存储卷配置、卷的元数据、备份到S3的数据的描述信息。为了协调长时间的操作,如数据库卷恢复操作,修复(重新复制)存储节点失效等,存储控制平台使用亚马逊简单工作流服务(Amazon Simple Workflow Service)。维持高可用性,需要在影响用户之前,主动地、自动地、尽早地检测出真正的或潜在的问题。存储操作各个关键的方面都要通过metric收集服务进行持续监控,metric会在关键性能、可用性参数值指示出现异常进行报警。

06 性能结果

在本段中,我们分享了自2015五月Aurora达到“GA”(基本可用)之后的运营经验。我们给出工业标准的基准测试结果,以及从我们客户获得的性能结果。

标准基准测试结果

这里展示了在不同的试验下,Aurora与mysql的性能对比结果。实验使用的是工业标准的基准测试,如sysbench和TPC-C的变种。我们运行的mysql实例,附加的存储是有30K IPOS的EBS存储卷,除非另有说明,这些mysql运行在有32 vCPU和244G内存的r3.8xlarge EC2实例中,配备了Intel Xeon E5-2670 v2 (Ivy Bridge) 处理器,r3.8xlarge的缓存缓冲设置到170GB。

07 学到的经验

我们可以看到客户运行的大量的各种应用,客户有小型的互联网公司,也有运行大量Aurora集群的复杂组织。然而许多应用的都是标准的用例,我们关注于这些云服务应用中共同的场景和期望,以引领我们走向新方向。

7.1 多租户与数据库整合

我们许多客户运营软件即服务(SaaS)的业务,有专有云业务客户,也有将企业部署的遗留系统转向SaaS的客户。我们发现这些客户往往依赖于一种不能轻易修改应用。因此,他们通常把他们自己的不同用户通过一个租户一个库或模式的方式整合到一个单实例中。这种风格可以减少成本:他们避免为每个用户购买一个专有实例,因为他们的用户不可能同时活跃。举个例子,有些SaaS的客户说他们有50,000多用户。

这种模式与我们众所周知的多租户应用如Saleforce.com不同,saleFore.com采用的多租户模式是,所有用户都在同一模式统一的表中,在行级记录中标记租户。结果是,我们看到进行数据库整合的用户拥有巨量的表。生产实例中拥有超过150,000个表的小数据库非常的普遍。这就给管理如字典缓存等元数据的组件带来了压力。这些用户需要(a)维持高水平的吞吐量以及并发的多用户连接,(b)用多少数据就购买提供多少的模式,很难进一步预测使用多少存储空间,(c)减少抖动,最小化单个用户负载尖峰对其他租户的影响。Aurora支持这些属性,对这些SaaS的应用适配得很好。

7.2 高并发自动伸缩负载

互联网负载需要处理因为突发事件导致的尖峰流量。我们的主要客户之一,在一次高度受欢迎的全国性电视节目中出现特殊状况,经历了远高于正常吞吐量峰值的尖峰,但是并没有给数据库带来压力。要支持这样的尖峰,数据库处理大量并发用户连接显得十分的重要。这在Aurora是可行的,因为下层存储系统伸缩性如此之好。我们有些用户运行时都达到了每秒8000个连接。

7.3 模式升级

现代web应用框架,如Ruby on Rails,都整合了ORM(object-relational mapping, 对象-关系映射)。导致对于应用开发人员来说,数据库模式改变非常容易,但是对DBA来说,如何升级数据库模式就成为一种挑战。在Rail应用中,我们有关于DBA的第一手资料:“数据库迁移“,对于DBA来说是"一周做大把的迁移“,或者是采用一些回避策略,用以保证将来迁移的不那么痛苦。而Mysql提供了自由的模式升级语义,大部分的修改的实现方式是全表复制。频繁的DDL操作是一个务实的现实,我们实现了一种高效的在线DDL,使用(a)基于每页的数据库版本化模式,使用模式历史信息,按需对单独页进行解码。(b)使用写时修改(modify-on-write)原语,实现对单页的最新模式懒更新。

7.4 可用性与软件升级

我们的客户对云原生数据库如何解决运行机群和对服务器的补丁升级的矛盾,有着强烈的期望。客户持有某种应用,而这些应用有主要使用Aurora作为OLTP服务支持。对这些客户来说,任何中断都是痛苦的。所以,我们许多客户对数据库软件的更新的容忍度很低,哪怕是相当于6周30秒的停机时间。因此,我们最近发布了零停机补丁(zero downtime patch)特性。这个特性允许给用户补丁但是不影响运行的数据库连接。

如图5所示,ZDP通过寻找没有活动事务的瞬间,在这瞬间将应用假脱机到本地临时存储,给数据库引擎打补丁然后更新应用状态。在这个过程中,用户会话仍然是活动的,并不知道数据库引擎已经发生了改变。
Amazon Aurora:高吞吐量的云原生关系数据库的设计考量

08 相关工作

在本段中,我们讨论其他人的贡献,这些贡献与Aurora采用的方法相关。

存储与引擎解耦虽然传统的数据都被设计成单内核的守护进程。但是仍有相关的工作,将数据库内核解耦成不同的组成部分,例如,Deuteronomy就是这样的系统,将系统分为事务组件(Tranascation Component, TC)和数据组件(Data Componet ,DC).事务组件提供并发控制功能已经从DC中做崩溃恢复的功能,DC提供了一个LLAMA之上的访问方法。LLAMA是一个无闩锁的基于日志结构的缓存和存储管理系统。Sinfonia 和 Hyder则从可拓展的服务抽出了事务性访问方法,实现了可以抽象使用这些方法的数据库系统。Yesquel系统实现了一个多版本分布式平衡树,并且把将并发控制从查询处理中独立出来。Aurora解耦存储比Deuteronomy、Hyder、Sinfonia更为底层。在Aurora中,查询处理,事务,并发,缓存缓冲以及访问方法都和日志存储解耦,并且崩溃恢复作为一个可拓展的服务实现的。

分布式系统 在分区面前,正确性与可用性的权衡早已为人所知,在网络分区去情况下,一个副本的可串行化是不可能的,最近Brewer的CAP理论已经证明了在高可用系统在网络分区的情况下,不可能提供“强”一致性。这些结果以及我们在云级规模的经验,激发了我们的一致性目标,即使在AZ失效引起分区的情况下也要一致性。

Brailis等人研究了高可用事务系统(Highly Available Transactions, HATs)的问题。他们已经证明分区或者高网络延迟,并不会导致不可用性。可串行化、snapshot隔离级、可重复读隔离级与HAT不兼容。然而其他的隔离级可以达到高可用性。Aurora提供了所有的隔离级,它给了一个简单的假设,即任何时候都只有一个写节点产生日志,日志更新的LSN,是在单个有序域中分配的。

Google的Spanner提供了读和写的外部一致性,并且提供了跨数据在同一个时间戳上全球一致性读。这些特性使得spanner可以一致性备份,一致性分布式查询处理以及原子模式更新,这些特性都是全球规模的,哪怕是正在运行的事务的时候,也支持。正如Bailis解释的一样,Spanner是为google的重-读(read-heavy)负载高度定制的。采用两阶段提交,读/写事务采用两阶段锁。

并发控制弱一致性与弱隔离级模型在分布式数据库中已为人所知。由此产生了乐观复制技术以及最终一致性系统在中心化的系统中,采用的其他的方法,有基于锁的悲观模式(),采用如多版本并发控制乐观模式,如Hekaton,如VoltDB采用分片方法,Deuteronomy和Hyper采用时间戳排序的方法。而Aurora给数据库系统提供了一个持久存在的抽象的本地磁盘,允许引擎自己处理隔离级与并发控制。

日志结构存储1992年LFS就引入了日志结构存储系统。最近Deuteronomy和LLMA的相关工作,以及Bw-tree以多种方式在存储引擎栈中采用日志结构技术,和Aurora一样,通过写入差量而不是写整页减少写放大。Aurora与Deuteronomy都实现了纯重做日志系统,并且跟踪最高持久化的LSN,用以确认提交。

崩溃恢复 传统数据库依赖于ARIES的恢复协议,现在有些数据库为了性能而采用了其他途径。例如,Hekaton与VoltDB采用某种形式的更新日志来重建崩溃后的内存状态。像Sinfonia系统采用如进程对以及复制状态机的技术来避免崩溃恢复。Graefe描述一种使用每页日志记录链的系统,可以按需逐页重做。可以提高崩溃恢复的速度。Aurora与Deuteronomy不需要重做崩溃恢复的过程。这是因为Deuteronomy会将事务延迟,以便只有提交的事务的修改才会写入到持久存储中。因而,与Aurora不同,Deuteronomy能处理的事务大小必然受到限制。

09 结论

我们设计出了高吞吐量的OLTP数据库Aurora,在云级规模环境下,并没有损失其可用性与持久性。主要思想是,没有采用传统数据库的单核架构,而是从计算节点中把存储解耦出来。实际上,我们只从数据库内核中移走了低于1/4的部分到独立的弹性伸缩的分布式服务中,用以管理日志与存储。由于所有的IO写都要通过网络,现在我们的基本的约束就在于网络。所以我们关注于能缓解网络压力并能提高吞吐量的技术。我们使用仲裁模型来处理大规模的云环境下的相关的复杂失效。并且避免了特殊节点带来的性能惩罚。使用日志处理减少总的IO负担。采用异步共识方法消除多阶段提交同步协议的繁复交互与高昂的代价,以及离线的崩溃恢复,和分布式存储的检查点。我们的方法导致了简化系统架构,能降低复杂性,易于伸缩能为未来的发展打下基础。

文章个人解读

图1中所示的Data Plane中,数据库引擎那个框包住了Caching,而存储服务那个框,也包住了Caching。耐人寻味。可以猜测,Cache是共享内存,数据库引擎和存储服务都可以访问。而且Cache不光是数据库引擎的一部分,而且也是存储服务的一部分。全文大部分篇幅讲如何优化写请求带来的网络负担,而只字未提读请求是否带来网络负担。因此,可以大胆猜测,一次写操作,不光是把日志应用到磁盘存储中,同时也把已经在cache的页也应用了,因此大部分的读请求不需要真实的存储IO,除非该页在cache中没有命中。从段6的Aurora测试可以看到,Aurora实例拥有的缓存相当大,一般的应用软件的数据库数据都能放在内存中。从图3也可以看到,AZ1主实例,也会向AZ2,AZ3数据库从实例发送日志与元数据,既然已经将日志处理从数据库引擎解耦出来了,发送的日志由谁应用呢?个人认为,AZ2与AZ3接收到日志,仍然是有存储服务应用的,只不过Cache是存储服务与数据库引擎共享的。

而且有这个猜测,我们进一步解读零停机补丁的实现方式。ZDP如何保持用户连接实现假脱机?个人认为,应用到数据库之间有proxy。使用proxy来保持应用程序的连接,并重新与更新后数据库实例建立连接,而且要做到用户会话无感觉,Cache是共享内存是必要的。因为Cache保存了大部分数据库运行状态,重启后的数据库实例仍然继续进行用户会话的查询。这个和使用共享内存的postgre有点像,你随意杀死postgre某些进程,并不影响用户会话的查询。

此外,个人认为,Aurora的计算节点,有可能也是存储节点。虽然逻辑上是存储与计算分离的,但是也可以享受不分离带来的好处,比如,存储服务可以直接往Cache应用日志,使得数据库引擎在大部分读请求不需要真实的IO。

Aurora有没有checkpoint?

Aurora的PGMRPL可以认为是检查点。LSN小于这个点的数据页已经刷盘,而大于这个点的页可以刷盘,也可以不刷盘,LSN小于这个点的日志都可以丢弃。PGMRPL在逻辑上与存储引擎的检查点是等效的。可以认为是PG上的检查点。

Aurora与polardb

Aurora与Polardb都是存储与计算分离、一些多读、共享分布式存储的架构。很显然,polardb在写放大上面没有做优化。从节点需要同步主库的脏页,共享的存储,私有的脏页,是个很难解决的矛盾。因此polardb的读节点会影响写节点。而Aurora可以认为没有脏页。日志即数据库,可以把日志应用器看作更慢的存储,存储服务与缓存都可以认为是日志应用器的Cache。

从节点与主节点之间有延迟,而aurora存储的数据页有多版本,文中明确指出存储有旧页回收处理。从节点依据读取点LSN读取到指定版本的页,文中段4.2.4指出,写节点到读复制节点之间的延迟小于20ms,因此,不可回收的旧版本页应该不会太多。

临时表

每个读节点虽然只有查询操作,但是查询操作会生成临时表用以保存查询的中间结果。生成临时表数据是不会产生日志的。但是这里仍有写IO,个人认为,Aurora有可能直接写在本地存储,这样不会产生网络上的负担。