Aurora


摘要(Abstract)

Aurora是亚马逊网络服务(AWS)的一部分,为OLTP业务提供关系型数据库服务。本文介绍了Aurora的系统架构以及背后设计上的考虑。我们认为,高吞吐量数据处理的核心问题已经从计算和存储移到了网络IO。为了解决这个问题,Aurora提出了一种新的关系型数据库架构,将REDO日志的处理下沉到一个专门为Aurora定制的多客户可扩展的存储服务(multi-tenant scale-out storage service)上。我们介绍了这种架构的一些优点,包括减少了网络流量,可以实现快速的故障恢复,无损的故障切换到备机,提供容错并且自愈的存储服务。接着,我们介绍了Aurora如何使用一种高效的异步方法,在大量的存储节点上实现可持久化状态的一致性,避免使用昂贵且沟通复杂的恢复协议。最后,基于在生产环境运维Aurora 18个月的经验,我们分享了从客户上学习到一些心得:客户期望现代云服务中的数据库层是怎样的。

OLTP是Online Transaction Processing的缩写,指的是在线事务处理。它是一种计算机处理方式,用于支持企业日常业务操作,如订单处理、库存管理、客户服务等。OLTP系统通常需要快速响应用户请求,并且需要保证数据的一致性和可靠性。

Multi-tenant是指一种软件架构,其中单个实例的应用程序同时为多个客户(或租户)提供服务。每个客户都可以访问该应用程序的共享实例,但是他们的数据和配置是相互隔离的,以保护各个客户之间的数据安全性和隐私。

1、引言(Introduction)

IT业务现在正加速向公有云迁移。这个产业级别的转变背后一个重要原因是,公有云能提供弹性的按需容量,(IT企业将这部分费用)作为经营性支出支付,而不用采用资本投入的模式。大量的IT业务需要支持OLTP的数据库,而提供与自建数据库等同甚至更高级的数据库服务,对支持这个长期转变的过程是至关重要的。

在现代的分布式云服务中,弹性和可扩展性可以通过将计算和存储解耦,并在多个节点上提供存储的副本来实现。这样的结构可以让我们更容易的实现一些操作,比如替换掉异常或者不可达的主机,添加副本,主机故障后切换到副本,增加或者降低一个数据库实例的容量。在这种环境下,传统数据库所面临的IO瓶颈已经发生了变化。由于IO操作已经分布到一个多客户平台上的多个数据节点的多个数据盘上,单个数据盘或者节点不再是热点。取而代之的是,系统的瓶颈移动到发起这些IO操作的数据库层,以及真正执行这些IO的存储层之间。除了基本的PPS和带宽的瓶颈外,这里还存在着流量的放大效应,因为一个高性能的数据库必须并行的将数据写入到存储层。性能最低的节点、数据盘、网络路径决定着整体的响应时间。

尽管数据库中的很多操作存在着交叉,还是有许多场景同步操作是必须的。这就导致了暂停和上下文切换。其中一个场景是,一次由于数据库缓存池未命中引起的磁盘读,这个时候读取线程在磁盘读完成之前是不能继续执行的。一次缓存未命中,可能带来额外的惩罚:将一个脏页剔除并写入到数据盘,腾出位置给新的页。另外,一些后台处理,如建立checkpoint或者刷脏页的操作,可以减少这种惩罚出现的几率,但是也会导致暂停、上下文切换以及资源竞争。

事务的提交是另外一种(性能的)干扰项,一个提交的阻塞会导致后面的事务提交的无法处理。用多阶段同步提交协议,如2PC(2-phase commit),处理提交是一项极具挑战性的工作。在高度扩展的分布系统中,系统中存在着持续的软硬故障,这些协议在这种场景下的支持不够好,并且有较大的处理时延,因为分布式系统中的节点可能分布在多个数据中心。

在本文中,我们介绍Amazon Aurora,一种通过将REDO日志分散在高度分布云服务环境中,来解决上述问题的新型数据库服务。Aurora使用了创新的面向服务的系统架构,使用多客户可扩展的存储服务层,来抽象虚拟化的分段REDO日志,并松散的与数据库实例层连接在一起。尽管每个数据库实例仍然包含一个传统数据库内核的大部分组件(查询处理器,事务,锁,buffer cache,访问方式以及UNDO日志的管理),一些功能(如REDO日志记录,持久化存储,故障恢复,备份以及恢复数据)都下沉交给存储层来做。

相对于传统的数据库,Aurora的系统架构有三个重要的优势。首先,通过将存储构建为一个跨数据中心容错且自修复的服务,我们可以保护数据库免遭系统性能抖动以及网络或者存储层的短期或者长期的故障的影响。我们注意到,一个可持久化的故障可以认为是系统长时间不可用的事件,而系统的不可用时间又可以建模为长时间的系统性能抖动。一个设计良好的系统可以无差别地处理这些问题。其次,通过只将REDO日志写入存储层,我们可以将网络的IOPS降低一个数量级。一旦我们移除了这个瓶颈,我们可以更进一步地优化其他的竞争点,从而可以在原有的MySQL源码基础上有重大的提升。第三点,我们将一些很复杂且关键的功能(备份,REDO恢复),从原来是数据库引擎中的一次性的昂贵的操作,转变为均摊在一个大型分布式存储层上连续异步的操作。这样,我们可以实现近乎即时的不需要checkpoint的故障恢复,以及廉价的不影响前台处理的备份操作。

在本文中,我们首先介绍三个主要贡献:

1、如何在云规模上实现可持久性,如何设计一个多数派系统以应对关联故障(第二节)

2、如何将传统数据库最下面的一部分下沉到存储层来实现智能的存储(第三节)

3、如何在分布式存储中移除多阶段的同步,如何故障恢复以及建立checkpoint(第四节)

我们接着在第五节展示如何将这三个想法结合起来设计Aurora的整体架构,紧接的第六节是我们的性能测试结果。第七节讲的是我们在这个过程中学习到的心得。最后,我们在第八节简要地介绍了相关的工作。第九节是结束语。

2、大规模系统中的可持久性(DURABILITY AT SCALE)

数据库专门设计来满足一种协议,一旦数据写入,就可以被读出来。不过,不是所有的系统都是这样的。我们在这一节介绍我们的多数派模型以及对数据分段背后的理念,将这两者结合起来,如何既能实现可持久性、可用性、减少抖动,又能帮助我们解决大规模存储层的运维问题。

2.1、复制以及关联故障(Replication and Correlated Failures)

实例的生命周期与存储的生命周期不是强耦合的。实例可以挂掉,用户也可以将他们停掉,也可以根据负载升级或者降级实例。基于这些原因,将存储层和计算层分开是有实际意义的。

一旦分离了计算和存储,存储节点本身和数据盘也会故障挂掉。因而,他们必须以某种形式复制来应对故障。在大规模的云环境中,长期存在着低频的节点、数据盘、网络路径故障的背景噪音。每一个故障可能具有不同的持续时间和影响范围。举个例子,可能某一个节点会存在短暂的网络不可用的情况,由于重启引起的短暂的停服,或者也存在着一个数据盘、节点、机架、网络交换设备的叶子或者主干,甚至整个数据中心的永久性故障。

在一个复制系统的里面应对故障的一个方案是,使用基于多数派投票的协议。如果V个副本每个都有一个投票权,那么一个读或者写操作必须分别获得读多数派Vr票,以及写多数派Vw票。为了保证一致性,这些多数派必须满足两个规则。首先,为了读到最新的数据,必须满足Vr+Vw > V。这个规则保证最近的一次写多数派和读多数派至少包含相同的一个节点,从而保证读到最新的数据。其次,为了避免写冲突,感知到最新的写入操作,写操作的涉及的副本数必须满足Vw > V/2。

通常的为了避免一个节点故障的方式是将数据复制三份,设置V为3,读多数派为Vr=2,写多数派为Vw=2。

但是我们认为设计2/3为多数派是不够的。为了理解这是为什么,我们必须先理解AWS中可用区的概念。一个可用区是一个地域的子集,与该区域的其他可用区通过低延时的链路连接。可用区之间对很多故障是隔离的,包括供电、网络、软件、洪灾等。将数据副本存放在不同的可用区中,可以保证通常的故障模式只会影响到一个副本。这也就意味着,用户只要将三个副本存放在不同的可用区中,就可以应对大规模的事件和小范围内个别的故障。

我们将Aurora设计为能容忍(a)挂掉整个可用区(AZ)以及一个额外的节点(AZ+1)而不影响读取数据,(b)挂掉一整个可用区而不影响写入数据。我们通过将数据复制为6个副本,存放在3个可用区中,每个可用区2个。我们将大多数派模型中的V值设为6,这样写多数派为Vw=4,读多数派为Vr=3。通过这个模型,我们在挂掉一个可用区加一个节点仍然提供读服务,挂掉一个可用区仍然提供写服务。确保读多数派,能使我们添加一个副本就可以重建写多数派。

2.2、分段存储(Segmented Storage)

我们考虑一下AZ+1的方案是否能提供足够的可持久性。为了在这个模型中保持足够的可持久性,必须保证两个不相关故障成对出现的概率(平均故障间隔),要比平均修复时间小得多。如果成对故障出现的概率非常高,可能会导致一个AZ故障,从而形成不了多数派。过了某个点之后,很难去进一步降低独立事件的平均故障时间。因而,我们将重点放在通过降低平均修复时间来降低成对故障的影响。我们采用的具体做法是,将数据库的总容量划分为固定大小的数据段,大小为10G。每个数据段有6个副本,组成一个Protect Group(PG),分布在3个AZ中,每个AZ 2个。一个Aurora数据卷通过一组PGs连接而成,物理上由一组挂载本地SSD的EC2主机作为一个存储节点,每个存储节点有多个存储单元。通过分配更多的PG,可以线性的扩展数据卷的容量,Aurora支持的最大数据卷(一个副本)容量为64T。

数据段是系统中最小的故障和恢复单元,自动的监控和修复故障是整个服务的一个部分。之所以选择10G,是因为在万兆网络条件下,恢复一个数据段只需要10秒钟。在这种情况,如果要打破多数派,那么必须同时出现两个数据段同时故障加上一个AZ故障,同时AZ故障不包含之前两个数据段故障的独立事件。通过我们对故障率的观察,这种情况出现的概率足够低,即使是在我们现在为客户服务的数据库量级上。

2.3、弹性的运维优势(Operational Advantages of Resilience)

一旦我们设计了一个能对长时间故障保持韧性的系统,那么这个系统就能轻松处理短时间的故障了。一个存储系统如果能应对一个AZ的长时间故障,也能应对由于停电或者软件故障引起的短时间服务不可用。同理,如果能应对一个多数派中的成员数秒钟的失联,当然也能处理短时间的网络拥塞或者存储节点的高负载。

由于Aurora系统对故障有着高度的忍耐性,我们可以通过这一点来处理导致数据段不可用的运维操作。举个例子,热点管理可以变得很直观。我们可以直接将一个热点数据盘或者节点标记为故障,通过将数据迁移到冷存储节点上来迅速地修复多数派。而操作系统和安全漏洞修复对于存储节点来说,就是一个短时间的不可用事件。甚至,存储层的软件升级也可以类似的处理。我 每次处理一个AZ,同时保证同一个PG内没有两个副本所在的节点同时被处理。基于这些,我们在存储服务上可以使用敏捷方法和快速部署。

3、日志即数据库(THE LOG IS THE DATABASE)

在这一节,我们阐释了为什么传统的数据库使用分段冗余的存储系统,会引起不能承受的网络IO和同步阻塞等性能负担。接着,介绍Aurora采用的将日志处理交给存储服务层来做的方案,并且用实验数据说明了该方案能显著地降低网络IOs。最后,介绍了Aurora存储服务中使用的一些技巧,用于将同步阻塞和不必要的写操作最小化。

3.1、成倍放大的写负担(The Burden of Amplified Writes)

我们的模型中将数据整体容量分段,并将分段复制为6个副本形成4/6写多数派,给整个系统带来了韧性。不过,这个模型会让传统的数据库如MySQL对单次应用层的写入产生过多的真实IO操作,使得整个系统的性能无法接受。高IO被复制操作成倍的放大,产生的高包量PPS让系统负担很重。同时,这些IO操作也产生一些同步点,导致数据管道阻塞、延时被放大。虽然链式复制及其变种可以减少网络开销,但是仍然受困于同步阻塞以及延时放大。

我们来审视一下写操作如何在传统的数据库中执行的。数据库系统如MySQL将数据页写到数据对象中(如堆文件、B树等),同时将REDO日志写入Write-Ahead日志WAL。每一条REDO日志包含着一个数据页的前镜像和后镜像的差异。将REDO日志应用到前镜像上可以得到数据页的后镜像。

在实际中,一些其他的数据也必须被写入。比如,考虑一对同步镜像的MySQL实例,通过部署在不同的数据中心形成主从结构来获取高可用性。在AZ1中有一个MySQL实例,通过EBS挂载带网络的存储。在AZ2中有一个从机,同样通过EBS挂载带网络的存储。写入到主EBS的数据会通过软件镜像同步到一个从EBS上。

图2展示了数据库引擎需要写入的不同类型的数据,包括REDO日志,为支持任意时间回档归档到S3上的二进制日志,被修改的数据页,为了防止页损坏而双写的数据,还有元数据FRM文件。图中同样描述了IO流的顺序。在步骤1和2中,会写入数据到主EBS上,同时同步到在同一个AZ中的从EBS上,当两个都写完了才回复确认。接着,在步骤3中,写入数据会使用块级别的软件镜像同步到MySQL从机上。最后,在步骤4和5中,数据会被写到MySQL从机上挂载的一对主从EBS上。

上面描述的MySQL镜像模型在现实中是不可取的,不仅是因为数据是如何写入的,同时也因为有哪些数据被写入。首先,步骤1、3、5是顺序且同步的。延时会因为同步写而累积。抖动会被放大,主要是因为即使是异步写,也必须等待最慢的一次操作,系统的性能由最坏的操作结果决定。从分布式系统的角度看,这个模型可以看作一个4/4写多数派模型,在故障和最坏操作的性能限制条件下很脆弱。其实,由OLTP应用产生的用户操作可能导致多种不同的类型的写入,而实际上代表的是同样的信息—比如,为了防止存储基础设施中的页损坏而设计的双写操作。

3.2、REDO日志处理下沉到存储(Offloading Redo Processing to Storage)

当一个传统数据库修改一个数据页,会产生一个REDO日志记录,并调用Log Applicator将其应用在内存中的页的前镜像上产生页的后镜像。事务的提交要求首先必须写入日志,数据页的刷盘可能会滞后。

在Aurora中,需要通过网络传输的写数据只有REDO日志。数据库层不会因为后台操作或者建立检查点而写入其他数据。取而代之的是,Log Applicator被下推到了存储层,用来在后台或者按需产生数据页。诚然,从头开始按每页修改的完整路径来生成每个数据页是相当昂贵的操作。因而,我们在后台不断地使用REDO日志来生成数据页,来避免每次都按需从头生成。注意到,后台的数据生成从正确性的角度来看完全是可选的:因为从存储引擎的角度出来,日志就是数据库,所有生成的数据页不过是日志的缓存。同时,不像建立检查点,只有有一连串修改记录的数据页需要重新生成。建立检查点,与完整REDO日志链的有关,而Aurora的数据页生成只与这个页的日志链有关。

我们的方案即使是在由于复制引起的放大写的条件下,不仅减少了网络负载,而且还提供了可观的性能和可持久性。存储服务可以以并行独立任务的方式来扩展IO,并且不影响数据库引擎的吞吐量。举个例子,图3展示了一个Aurora集群,包括一个主实例和多个副本,部署在多个不同的可用区中。在这个模型中,主实例将REDO日志写入存储层,并将日志以及元数据的更新一起发送给副本实例。IO流根据目的地来将日志顺序打成batch,并将每个batch传给数据的6个副本并持久化到数据盘上。数据库引擎只要收到6个中的4个回复就形成了一个写多数派,此时可认为这些日志文件被持久化了。每个数据副本使用这些REDO日志将数据页的变更应用在他们的buffer cache中。

为了测试网络IO,我们用SysBench跑了一个写压力测试,100G的数据量写入两个不同配置的数据库:一个是之前介绍的部署在不同可用的区的MySQL同步镜像,另外一个是Aurora(副本在不同的可用区)。对两个数据库实例,在r3.8xlarge EC2实例上运行测试30分钟。

我们的测试结果归纳在表1中。在30分钟的测试过程中,Aurora可以负载比MySQL镜像多35倍的事务。每个事务所需的IO次比MySQL镜像少7.7倍。我们通过将更少的数据通过网络写,使得我们可以更积极地复制数据获得持久性和可用性,可以并发的请求来最小化性能的抖动。

将日志处理放在存储层可以通过一系列手段来提升可用性,包括减少故障恢复时间,消除由于后台操作如建立检查点、数据页写入以及备份等引起的性能抖动。

我们来对比一下故障恢复。在传统的数据库中,系统必须从最近的一个检查点开始恢复,重放日志确保所有REDO日志都被应用。在Aurora中,可持久化REDO日志不断地、异步的应用在存储层,分布在各个数据节点上。如果数据页还没被生成,一个读请求可能会应用一些REDO日志来生成数据页。这样一来,故障恢复的过程被分散在所有的正常的前台操作中。在数据库启动的时候不需要做任何事情。

3.3、存储服务的设计点(Storage Service Design Points)

存储服务的一个核心设计点是尽可能减少前台写操作的延时。我们将大部分的存储处理操作移到了后台。考虑到存储层从峰值到平均请求的巨大差异,我们有足够的时间在前台操作路径之外处理这些任务。我们也可以使用计算来换存储。举个例子,如果存储节点在忙着处理前台写请求的时候,没有必要运行GC来回收老的数据页版本,除非是数据盘快满了。在Aurora中,后台处理和前台处理是负相关的。这与传统的数据库不同,传统数据库后台的脏页刷盘和建立检查点与前台的负载是正相关的。在这样的系统中,如果后台积累了许多未处理的任务,那么必须扼制前台正常的处理流程才能防止后台任务越积累越多。由于在Aurora中数据段被分散在不同的存储节点上,一个存储节点卡死可以轻易被4/6写多数派处理,卡死的存储节点会被看作一个慢节点(不影响整体的流程)。

我们来进一步看看存储节点的处理流程。如图4所示,它包括以下的步骤:(1)收到日志记录并将其加入内存的队列,(2)持久化记录并确认写入,(3)整理日志记录并确认日志中有哪些缺失,因为有些包可能丢了,(4)与其他数据节点交互填补空缺,(5)用日志记录生成新的数据页,(6)不断的将数据页和REDO日志持久化到S3,(7)周期性的回收旧的版本,(8)最后周期性的对数据页进行CRC校验。

注意上面的步骤都是异步的,只有步骤(1)和(2)是在前台操作的路径中,可能会影响延时。

4、日志驱动(THE LOG MARCHES FORWARD)

在这一节中,我们介绍了数据库引擎是如何产生日志的,这样可持久化状态、运行时状态、以及复制状态永远是一致的。重点讲述了如何不通过复杂的2PC协议来高效地实现一致性。首先,我们展示如何在故障恢复的时候,避免使用昂贵的REDO日志重放。其次,我们介绍了一些常规操作,以及我们能如何保持运行时和复制状态。最后,我们介绍故障恢复过程的细节。

4.1、方案概览:异步处理(Solution sketch: Asynchronous Processing)

由于我们将数据库建模为日志流,这样日志的不断滚动的过程可以看作一连串顺序的变更。在实现上,每一条日志记录都有一个由数据库产生单调递增的日志编号LSN。

这让我们可以使用异步的思路简化用来维护状态的一致性协议,而不是使用2PC这种沟通复杂且对错误容忍度低的协议。从上层来看,我们维护一致性和可持久性的状态点,并随着我们收到发出去请求的确认消息,不断地推进这些点。由于任何单个的存储节点都有可能丢失一个或者多个日志记录,这些节点与节点相互之间交流,找到并填补丢失的信息。数据库维护的运行时状态可以让我们用单个数据段的读来代替大多数读,除非是在故障恢复的时候,状态丢失了必须通过重建。

数据库可能同时发起了多个独立的事务,这些事务完成的顺序与发起的顺序是不一致的。假如这时数据库崩溃了重启,每个事务决定是否需要回滚是相互独立的。跟踪未完成的时候并回滚的逻辑还是在数据库引擎中完成,就如同它在写单个盘一样。不过,在数据库重启的时候,在它被允许访问存储之前,存储服务需要进行自己的故障恢复流程,然而重点不在用户级的事务上,而是确保数据库能看到存储的一致性视图,尽管存储本身是分布式的。

存储服务首先确定VCL(Volume Complete LSN),能确保之前日志记录都可用最大的LSN。在存储恢复的过程中,大于VCL的日志就都必须被截断。数据库可以通过找到CPL(Consistency Point LSN),并使用这些点进行进一步的日志截断。这样我们可以定义VDL(Volume Durable LSN)为所有副本中最大的CPL,CPL必须小于或者等VCL,所有大于VDL的日志记录都可以被截断丢掉。举个例子,即使我们有到LSN 1007的完整数据,数据库发现只有900、1000和1100是CPL点,那么,我们必须在1000处截断。我们有到1007的完整数据,不过我们只有到1000的可持久性。

VCL表示当前已提交到存储服务的最高LSN。CPL表示在某个时间点上,所有数据都已经被写入到存储服务,并且可以被认为是一致的LSN。VDL表示小于或等于VCL的最高CPL,并且用于确定哪些日志记录可以被截断以确保数据持久性。

因而,完整性和可持久性是不同的。一个CPL可以看作描述带某种形式限制的存储系统事务,这些事务本身必须按序确认。如果客户端认为这些区分没用,它可以将每个日志记录看作一个CPL。在实现中,数据库和存储必须如下交互:

  • 每个数据库层的事务会被划分为多个mini事务,这些事务是有序的,并且被原子的执行
  • 每个mini事务由多个连续的日志记录组成
  • mini事务的最后一个日志记录就是一个CPL

在故障恢复的时候,数据库告诉存储服务建立每个PG的可持久化点,并使用这些来确认VDL,然后发送命令截断所有大于VDL的日志记录。

4.2、常规操作(Normal Operation)

我们现在介绍数据库的常规操作,重点依次介绍写,读,事务提交,副本。

4.2.1、写(Writes)

在Aurora中,数据库不断的与存储服务交互,维护状态来保持大多数派,持久化日志记录,并将事务标记为已提交。比如,在正常/前台路径中,如果数据库收到写大多数派的写确认回复,它会将VDL往前推进。在任意一个时间点,数据库中都会存在着大量并发的事务,每个事务产生自己的REDO日志。数据库为每个记录分配一个唯一有序的LSN,这些LSN不能大于VDL加上LAL(LSN Allocation Limit)(目前被设为10m)。这个限制保证数据库不会领先存储服务太多,以至于导致后台处理的压力过大(如网络或者存储跟不上)阻塞写请求。

注意到每个PG中的每个数据段只会看到整体的一部分日志记录。每个日志记录含有一个反向的指针指向这个PG中的前一个日志记录。这些反向指针可以用来追踪每个数据段的完整性点,来确认SCL(Segment Complete LSN),SCL是PG收到的连续日志的最大LSN值。SCL被数据库节点用来与其他节点交流,找到缺失的日志记录并添补它们。

4.2.2、提交(Commits)

在Aurora中,事务的提交是异步完成的。当客户端提交一个事务,处理这个提交请求的线程将事务放在一边,并将COMMIT LSN记录在一个单独的事务队列中等待被确认提交,然后就去做其他事情了。这等同于实现了WAL协议:确认一个事务提交完成了,当且仅当最新的VDL大于或者等于这个事务的COMMIT LSN。当VDL不断的增加,数据库找到哪些事务等待被确认,用一个单独的线程给等待的客户端返回事务完成的确认。Worker线程不会等待事务提交完成,它们会继续处理等待着的请求。

WAL协议指的是Write-Ahead Logging Protocol,是一种常见的数据库恢复协议。在WAL协议中,所有的数据修改操作都必须先写入一个称为WAL的日志文件中,然后再将其写入数据库本身。这样可以确保在发生故障时,可以使用WAL日志文件来恢复数据库状态。

4.2.3、读(Reads)

在Aurora中,与大多数数据库一样,数据页是从buffer cache中读取,只有在被请求的页不在cache中时,才会发起一次存储IO请求。

如果buffer cache满了,系统会找到一个页并将其踢出缓存。在传统的数据库中,如果这个被踢出的页是脏页,它在被替换之前会被刷新到数据盘中。这是为了保证接下来读取的数据页永远是最新的数据。不过Aurora在踢出页的时候不会写磁盘,它提供了一个类似的保证:buffer cache中的数据页永远是最新的数据。这个保证通过踢出page LSN(数据页上应用的最新的日志记录的LSN)大于或者等于VDL的数据页来实现。这个协议确保:(a)所有对数据页的变更都已经持久化在日志中了,(b)如果缓存失效,可以通过获取最新页来构造当前VDL所对应的页面。

数据库在通常情况下都不要通过多数派读来获得一致性。当从盘里面读一个页的时候,数据库建一个读取点,代表请求发生时的VDL。数据库可以选择一个对这个读取点是完整的存储节点,这样读取的数据肯定是最新的版本。从存储节点返回的数据页必须与数据库中mini事务的语义一致。由于数据库直接将日志记录发送给存储节点,并跟进日志处理的进程(也就是,每个数据段的SCL),通常它知道哪些数据段是可以满足一个读请求的(SCL大于读取点的数据段),因而可以直接将请求发送给有足够数据的数据段。

考虑到数据库记录了所有的当前读操作,因而可以计算出在任意时间点每个PG的最小读取点LSN。如果有读副本,写副本会与它们沟通获取所有存储节点上每个PG的最小读取点LSN。这个值称作PG最小读取点LSN(PGMRPL),代表低水位点。在此以下的PG的所有的日志记录都是不必要的。换句话说,存储节点中数据段确认不会再有读取请求的读请求点小于PGMRPL。每个存储节点都能通过数据库获取到这个值,并且能合并老的日志记录并继续生产新的数据页,然后放心的将这些日志记录回收掉。

跟传统的MySQL数据库一样,实际的并发控制协议在数据库引擎中执行,就像数据页和UNDO段在本地存储一般。

4.2.4、副本(Replicas)

在Aurora中,一个写副本和多至15个读副本可以挂载同一个共享的存储空间。因而,读副本不会增加任何的存储和写开销。为了减少延时,写副本产生的日志流发送到存储节点的同时,也会发送到所有的读副本。在读副本中,数据库会依次消耗每一个日志记录。如果日志记录指向的是一个buffer cache中存在的页,它就用log applicator应用日志的变更到数据页上。否则的话,它就直接丢掉这条日志。注意,从写副本的角度来看,读副本是异步的消耗这些日志,而写副本确认用户事务的完成是与读副本无关的。读副本在应用这些日志的时候遵守两条重要的规则:(a)只有SDL小于或者等于VDL的日志记录会被应用,(b)一个mini事务中的日志记录会原子的被应用,确保副本可以看到所有数据库对象的一致性视图。在实际中,每个读副本滞后写副本一小段时间(20ms或以内)。

4.3、故障恢复(Recovery)

大多数传统的数据库使用类似ARIES的恢复协议,这些协议依赖可以代表所有已提交事务的WAL。这些系统也会粗粒度的为数据库周期性的,通过刷新脏页和将检查点写入日志,来建立检查点。在重启的时候,一个数据页可能丢失一些已经提交的数据,或者包含未提交的数据。因而,在故障恢复的时候,系统重放自上一个检查点其的所有REDO日志到相关的数据页。这个过程将数据库的页重新置为在故障发生那个时间点的一致性状态,之后通过执行相关的UNDO日志可以将正在执行的事务回滚。故障恢复是一个代价昂贵的操作。降低建立检查点的时间间隔会有所帮助,不过是以干扰前台事务为代价的。在Aurora中不需要做这样的折中。

传统数据库的一个简化规则是,在前台处理和故障恢复同步使用的REDO日志applicator,也会在数据库离线在后台服务中使用。在Aurora中,我们也依赖于同样的规则,只不过这里REDO applicator是与数据库解耦的,一直并行的在后台运行在存储节点上。当数据库启动的时候,它会与存储服务协助进行数据恢复,因而Aurora数据库可以恢复非常快(通常在10s以内),即使在崩溃的时候正在执行100K TPS的写入。

数据库在故障重启的时候仍然需要重建运行时状态。在这种情况下,数据库连接每一个PG,数据段的读多数派如果能确认发现其他数据,也可以形成一个写多数派。一旦数据库为每一个PG建立了读多数派,它可以计算出REDO可以截断的范围,这个范围是新的VDL到当前数据库已经分配的最大的LSN。数据库能推导出这个上限值,是因为它分配LSN,并且限制了最大的LSN为VDL+LAL(之前已经介绍过的,值为10m)。这些截断范围是用时间戳来标记的并且写到存储服务中,因而在截断的时候并没有任何歧义,即使恢复过程被打断或者重启。

数据库仍然需要执行UNDO恢复来回滚在故障时间点正在进行的事务。不过,UNDO恢复可以在系统启动后通过UNDO段来获取正在进行的事务之后再进行。

5、整体来看(PUTTING IT ALL TOGETHER)

在这一节中,我们从整体来描述构成Aurora的组件,如图5所示。

数据库引擎是社区版MySQL/InnoDB的分支,主要区别是InnoDB如何从数据盘读取或者写入数据。在社区版InnoDB中,一个写操作的执行包括:数据页在buffer中被修改,REDO日志按LSN有序写入到WAL的buffer中。在事务提交的时候,WAL协议只要求事务的REDO日志写入到数据盘。真正被修改的数据页最终会写入数据盘,这里使用了双写技术来避免数据写盘不完整。这些数据页的写入可能发生在后台,可能由于cache的踢出,也有可能由于建立检查点。除了IO子系统之外,InnoDB还有事务子系统,通过B+树和mini事务实现的锁管理器。Mini事务是只在InnoDB中使用的结构,描述的是一组必须原子执行的操作(比如,分裂或者合并B+树的页)。

在Aurora版本的InnoDB中,每个Mini事务中的REDO日志会按所属的PG分组打包,然后批量写入存储服务中。每个Mini事务的最后一个日志记录被标记为一个一致性点。Aurora写副本支持社区版MySQL相同的隔离级别。Aurora的读副本会不断的从写副本中获取事务开始和提交的信息,并使用这些信息来支持本地只读事务的快照隔离级别。注意到并发控制完全在数据库引擎中实现,不会影响存储服务。存储服务为数据提供一个一致性的视图,在逻辑上等价于社区版InnoDB写数据到本地存储。

Aurora使用Amazon RDS来作为它的控制面板。RDS在数据实例上部署Agent来监控集群的健康状况,是否需要做故障切换,或者实例是否应该被替换掉。每个数据库集群包括一个写副本,0个或者多个读副本。集群中所有的实例都在一个地理上的区域(Region)中,通常会位于不同的可用区,连接到相同区域里面的存储服务。为安全性考虑,我们隔离了数据库,应用以及存储之间的通信。在实际中,每个数据库实例可以与三个Amazon虚拟网络VPC通信:用户应用与数据库引擎交互的用户VPC,数据库引擎与RDS控制面板交互的RDS VPC,数据库与存储服务交互的存储VPC。

存储服务部署在一个EC2虚拟机集群上,集群最少会跨同一个Region的三个可用区AZ,共同为多个用户提供存储,读取或者写入数据,备份或者恢复用户数据。存储节点操作本地的SSD盘,与数据库实例、其他存储节点、备份/恢复服务交互,持续地将数据备份到S3或者从S3恢复数据。存储服务的控制面板用Amazon DynamoDB作为持久存储,存放数据库容量配置、元数据以及备份到S3上的数据的详细信息。为了支持长时间的操作,比如由故障导致的数据库恢复或者复制操作,存储服务的控制面板使用Amazon Simple Workflow Service SWF。为了保证高质量的可用性,需要在用户发现之前积极的、自动的监控和探测真实的和潜在的问题。所有存储服务的关键操作都被持续的监控起来,如果发现性能或者可用性方面的潜在问题会及时告警。

6、性能测试结果(PERFORMANCE RESULTS)

在这一节中,我们分享自2015年7月Aurora GA之后在生产环境运营的经验。首先介绍标准的工业基准测试的结果,接着是一些来自客户的性能测试结果。

6.1、标准基准测试的结果(Results with Standard Benchmarks)

我们使用标准的基准测试工具SysBench和TPC-C类似测试工具来进行测试,对比了Aurora和MySQL在不同场景下的性能表现。我们在带有20K IOPS EBS的EC2实例上进行测试,除非特殊说明,这些实例的规格为32 vCPU,244G内存,Intel Xeon E5-2670 v2(Ivy bridge)处理器。实例上的buffer cache设为170G。

6.1.1、随实例规格扩展(Scaling with instance sizes)

在这个测试中,我们发现Aurora的吞吐量可以随着实例规格线性增长,在最高实例规格上吞吐量是MySQL5.6或者MySQL5.7的5倍。而Aurora目前是基于MySQL5.6的代码库的。我们在EC2 r3系列实例(large,xlarge,2xlarge,4xlarge,8xlarge)上运行1GB数据量大小(250张表)的只读或者只写的基准测试。R3系列的每个实例的vCPU和内存数量是下一个比它大的规格的一半。

在这个测试中,我们发现Aurora的吞吐量可以随着实例规格线性增长,在最高实例规格上吞吐量是MySQL5.6或者MySQL5.7的5倍。而Aurora目前是基于MySQL5.6的代码库的。我们在EC2 r3系列实例(large,xlarge,2xlarge,4xlarge,8xlarge)上运行1GB数据量大小(250张表)的只读或者只写的基准测试。R3系列的每个实例的vCPU和内存数量是下一个比它大的规格的一半。

6.1.2、不同数据集大小下的吞吐量(Throughput with varying data sizes)

在这个测试中,我们发现Aurora的吞吐量远大于MySQL,即使使用更大的数据集且包括cache之外的数据。表2展示使用SysBench的纯写入测试,使用100GB大小数据集Aurora可以比MySQL快67倍。即使是使用1TB包含Cache外数据的测试集,Aurora也比MySQL快34倍。

6.1.3、随用户连接数扩展(Scaling with user connections)

在这个测试中,我们发现Aurora的吞吐量可以随着客户端的连接数量而扩展。表3展示了运行SysBench OLTP基准测试的writes/sec结果,测试中连接数从50到500再到5000。Aurora可以从40K writes/sec扩展到110K writes/sec,MySQL的吞吐量在500个连接左右时达到峰值,然后随着连接数扩展到5000而急速下降。

6.1.4、随副本数扩展(Scaling with Replicas)

在这个测试中,我们发现Aurora读副本的延时比MySQL低很多,即使Aurora处在更高的负载情况下。表4展示了,随着负载从1K writes/sec到10K writes/sec,Aurora读副本的延时从2.62ms增长到5.38ms。相反,MySQL读副本的延时从1s增长到300s。在10K负载情况下,Aurora的副本延时比MySQL低几个数量级。副本延时通过一个被提交的事务在副本上可见所需要的时间来度量的。

6.1.5、热点行争用时的吞吐量(Throughput with hot row contention)

“hot row contention”表示热点行争用,即多个事务同时尝试修改同一行数据时发生的竞争和冲突。

在这个测试中,我们发现相对于MySQL,Aurora在像TPC-C基准测试中有hot row contention的负载下也能表现的很好。我们在Aurora、MySQL5.6、MySQL5.7上运行Percona TPC-C类似工具,运行实例规格为r3.8xlarge挂载IOPS为30K的EBS。表5展示了Aurora可以保持相对MySQL5.7 的2.3倍到16.3倍的吞吐量,负载从10GB数量、500个连接,到100GB数据、5000个连接。

6.2、客户端真实负载的测试结果(Results with Real Customer Workloads)

在这一小节中,我们分享一些客户在生产环境从MySQL迁移到Aurora的测试结果。

6.2.1、应用程序在Aurora的响应时间(Application response time with Aurora)

一个互联网游戏公司将生产环境的服务从MySQL迁移到r3.4xlarge实例的Aurora上。在迁移之前,网络事务的平均响应时间为15ms。与之对应的,迁移之后的平均响应时间为5.5ms,差不多有了3倍的提升,如图8所示。

6.2.2、Aurora中执行语句的延时(Statement Latencies with Aurora)

一个教育公司主要业务是帮助学校管理学生的笔记本电脑,将他们的服务从MySQL迁移到了Aurora。Select和单条记录insert语句在迁移前的中位点和95分位点如图9和图10所示。

在迁移之前,95分位延时在40ms到80ms之间,比中位点1ms差得远了。本文之前介绍了,应用程序会遇到这种少数性能极差的情况。在迁移之后,95分位的延时显著降低,接近中位点延时。

、

6.2.3、多个副本下的复制延时(Replica Lag with Multiple Replicas)

如Pinterest的Weiner所指出的,MySQL副本经常远落后于他们的写副本,会引起非常奇怪的bug。对于上面提到的那个教育公司,副本延时有时可能飙升到12分钟而影响到应用程序的正确性,所以这些副本只能作为一个备机。与之相对的,在迁移到Aurora之后,4个副本集的复制延时从未超过20ms,如图11所示。复制延时的显著改善让这家公司转移了一大部分应用程序的负载到只读副本上,既节约了成本又提高了可用性。

7、心得(LESSONS LEARNED)

我们遇到过运行各种各样的应用的客户,从小的互联网公司到大型的组织机构。他们很多的使用场景都是标准的,这里我们重点放在在云服务中比较常见的场景和期望,而这些导致我们走向了新的方向。

7.1、多客户和数据库聚合(Multi-tenancy and database consolidation)

很多我们的客户都经营SaaS服务,自己使用或者为他们自己的客户提供SaaS模型的服务。我们发现这些客户依赖的应用程序很难被改变。因而,他们通常将自己的很多客户集中在某一个实例上,使用库或者表来作为租赁的基本单位。这种模式可以节约成本:由于他们自己的客户不太可能同时使用,这样可以避免为每个客户申请一个单独的实例。举个例子,我们有些客户称他们自己有超过50K的客户。

这个模型与Salesforce.com著名的多客户应用场景有很大的不同,他们将数据打包到一个统一的表中,按行来租赁。因而,我们发现很多客户有很多张表。生产环境中数据库表超过150K是非常常见的。这给一些管理元数据的组件,如字典cache,带来很大压力。更重要的是,这些客户需要(a)保持高吞吐量和连接数,(b)存储容量按使用扩展和收费,因为很难提前预知需要多大的存储,(c)减少抖动,这样一个客户的峰值对其他客户的影响很小。Aurora支持所有的这些特性,而且很适合SaaS应用。

7.2、高并发自动扩展的负载(Highly concurrent auto-scaling workloads)

互联网的负载通常需要应对突发事件引起的网络流的尖峰。我们有个重要客户在一个很火的全国电视节目时,遇到过一次远超过平时负载吞吐量高峰的流量,不过没有对数据库构成压力。为了支持这样的突发流量,数据库需要同时能处理很多并发的请求。Aurora在这种场景下也能处理的很好,因为它的底层存储系统扩展性极好。我们有很多客户每秒钟的连接数超过8000次。

7.3、Schema演进(Schema evolution)

现代Web应用程序框架如Ruby on Rails深入集成了ORM工具。因而,应用程序可以很方便的改变数据库的schema,然而却让DBA们很难把握schema会如何演进。在Rails应用程序中,这些称之为DB迁移,我们听到一线的DBA称他们一周可能会有几十次DB迁移,或者会提前准备好策略来让未来的变更会比较容易。这些问题在MySQL中被放大,因为MySQL提供自由的schema变更语义,使用整表拷贝的方式来实现大多数变更。既然频繁的DDL是一个现实问题,我们在Aurora中实现了高效的在线DDL,(a)为每一个数据页关联一个schema版本,通过schema的变更历史来解码单个数据页,(b)用modify-on-write的方式按需将单个数据页更新到最新的schema。

DDL:数据库定义语言Data Definition Language

7.4、可用性和软件更新(Availability and Software Upgrades)

客户对云上的数据库的一些迫切的期待与我们如何运营系统和如何给服务器升级可能是相互矛盾的。由于我们的客户主要用Aurora来作为一个OLTP服务支撑线上应用程序,任何的干扰都可能导致严重的后果。因而,很多客户对我们更新数据库软件的容忍度是非常低的,即使在六周内只计划30s的服务暂停时间也不行。我们近期发布了一个Zero Downtime Patch ZDP特性,使得我们可以在保证已有的数据库连接不受干扰的情况下,更新服务器。

ZDP指的是Zero-Downtime Patching,即零停机补丁。它是一种用于在不中断服务的情况下对数据库引擎进行升级和修补的技术。

如图12所示,ZDF的原理是,首先找到一个没有活动连接的实例,将实例的应用程序状态导出到持久化存储中,给引擎升级,然后导入应用程序状态。在这个过程中,用户的session不受影响,对引擎的升级是无感知的。

8、相关工作(RELATED WORK)

在本节中,我们介绍其他人的贡献以及它们如何Aurora中采用的方案关联的。

存储计算分离。

尽管传统的数据库系统都会被构造成一个庞然大物,近期有一些数据库方面的工作将内核解耦为不同的组件。举个例子,Deuteronomy10就是这样的系统,它分离了提供并发控制的事务组件,和提供恢复功能构建在LLAMA34的数据组件,其中LLAMA是一个无锁、日志结构的缓存和存储管理器。Sinfonia39和Hyder40这些系统将事务的方法抽象成一个可扩展的服务,数据库系统的实现可以使用这些抽象。Yesquel36实现了一个多版本的分布式平衡树,将并发控制和查询处理器分开。Aurora比Deuteronomy、Sinfonia、Hyder和Yesquel在更低的层次将存储解耦出来。在Aurora中,查询处理器、事务、并发控制、buffer cache和访问方式是与日志、存储、故障恢复解耦的,后者被实现为可扩展的服务。

分布式系统。

在CAP理论中,人们早已知道正确性和可用性的权衡,以及面对分区的条件下,one-copy序列化是不可能的。最近,Brewer的CAP理论得到结论:一个高度可用的数据库系统在网络隔离的情况下不可能提供强一致性。这些结论以及我们对云级别规模的复杂且相互关联故障的经验,促使我们定下即使在一个AZ不可用的条件下仍然保持一致性的设计目标。

Bailies等人研究了高可用事务HATs,HAT既不会受网络分区导致的不可用的影响,又不会导致高的网络延时。他们的工作说明了Serializability,Snapshot Isolation, Repeatable Isolation不是HTA兼容的。Aurora提供所有这些隔离级别,基于一个简化的前提:在任意一个时间点,只有一个写副本在生成日志,这些日志的LSN在同一个有序空间里分配。

Google的Spanner提供外部一致的读和写,全局一致的指定时间点的读。这些特性可以让Spanner提供全局的一致的备份,全局的一致的分布式查询处理,全局的原子的schema更新,即使是在有事务正在执行的情况下。就像Bailis所描述的,Spanner是为Google读负载高的场景定制的,在读和写的时候依赖于两阶段提交和两阶段锁。

并发控制。

弱一致性以及隔离模型在分布式数据库中是广为人知的,也导致了乐观复制技术和最终一致性系统的出现。集中式系统的一些方案包括,经典的悲观锁方式,如Hekaton中的MVCC的乐观锁方式,如VoltDB中的分片模式,Hyper中的时间戳序列模式,还有Deuteronomy。Aurora的存储服务为数据库引擎提供了一个本地磁盘的抽象,让引擎来决定隔离级别和并发模式。

日志结构的存储。

日志结构的存储在1992年首先出现在LFS中。最近的Deuteronomy以及LLAMA中的相关工作,还有Bw-Tree在存储引擎栈中以多种形式使用了日志结构的技术,像Aurora一样,它们通过只写数据页的变更来减少写放大。Deuteronomy和Aurora实现的都是纯粹的REDO日志方式,并跟踪事务确认回复的最大的LSN。

故障恢复。

传统的数据库都依赖于类似ARIES5的恢复协议来实现故障恢复,近期很多系统为性能的考虑选择了其他的路径。举个例子,Hekaton和VoltDB使用某种更新日志来重建它们的内存状态。类似Sinfonia的系统,使用process pairs和状态机复制技术来避免故障恢复。Graefe介绍了使用每页的日志记录链来加快按需的page-by-page的REDO以加快恢复速度。跟Aurora一样,Deuteronomy不需要REDO恢复,这是因为Deuteronomy只会将已经提交的更新写入存储。因而,不像Aurora,Deuteronomy里的事务数量是受限制的。

9、结论(Conclusion)

我们在云环境下将Aurora设计为一个高吞吐量的OLTP数据库,不牺牲可用性和可持久性。主要的思想是避免传统数据库庞大复杂的结构,将存储和计算解耦。具体来说,我们将数据库内核最下面一小部分移到一个独立可扩展分布式的负责日志记录和存储数据的存储服务中。由于这时所有的IO都通过网络,我们最根本的限制变成了网络。因而,我们将重点放在缓解网络开销、增加吞吐量的技术上。我们依赖的技术有:多数派模型,可以处理在大规模云服务环境下复杂关联的故障,避免最差性能点的惩罚,通过日志处理来减少整体的IO负担,异步的一致性来避免沟通复杂且代价昂贵的多阶段同步协议,离线故障恢复,在分布式存储中建立检查点。我们的方案能得出一个简化的复杂度降低的系统,可以很方便的扩展,并为以后的演进奠定基础。


文章作者: wck
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wck !
评论
  目录