赞
踩
摘要:亚马逊的 Aurora 是一种关系数据库服务,用于作为亚马逊网络服务(AWS)的一部分提供的 OLTP 工作负载。在本文中,我们描述了 Aurora 的架构和导致该架构的设计考虑。我们认为,高吞吐量数据处理的核心限制已经从计算和存储转移到网络。Aurora 为关系数据库带来了一种新的体系结构来解决这一限制,最显著的是通过将重做处理(redo processing)推送到为 Aurora 专门构建的多租户横向扩展(multi-tenant scale-out)存储服务。我们描述了这样做不仅可以减少网络流量,还可以实现快速故障恢复、故障转移到副本而不丢失数据,以及容错、自修复存储。然后,我们描述了 Aurora 如何使用高效的异步方案在多个存储节点之间就持久状态达成共识,从而避免昂贵的恢复协议。最后,在将 Aurora 作为生产服务运营了18个月之后,我们分享了从客户那里学到的关于现代云应用对其数据库层的期望的经验。
关键词:数据库;分布式系统;日志处理;法定人数模型;复制;恢复;性能;OLTP
信息技术工作负载越来越多地转向公共云提供商。这种全行业范围的转型的重要原因包括能够在灵活的按需基础上提供容量,并使用运营费用而不是资本费用模型来支付容量。许多信息技术工作负载需要一个关系型 OLTP 数据库;为内部数据库提供同等或更高的功能对于支持这种长期过渡至关重要。
在现代分布式云服务中,通过将计算从存储中分离出来,并通过在多个节点上复制存储,弹性和可扩展性日益增强。这样做可以让我们处理一些操作,例如替换行为不良或无法访问的主机、添加副本、从写入程序故障切换到副本、向上或向下扩展数据库实例的大小等。
传统数据库系统面临的输入/输出瓶颈在这种环境下发生了变化。由于输入/输出可以分布在多租户集群中的许多节点和许多磁盘上,因此单个磁盘和节点不再是热点。相反,瓶颈转移到请求输入/输出的数据库层和执行这些输入/输出的存储层之间的网络上。除了每秒数据包(PPS)和带宽的基本瓶颈之外,还有流量的放大,因为高性能数据库会并行向存储机群发出写操作。异常存储节点、磁盘或网络路径的性能会影响响应时间。
虽然数据库中的大多数操作可能会相互重叠,但有几种情况需要同步操作。这会导致停顿和上下文切换。一种这样的情况是由于数据库缓冲区缓存中的缺失而导致的磁盘读取。读取线程在读取完成之前无法继续。缓存未命中还可能导致置换和刷新脏缓存页面以容纳新页面的额外代价。后台处理(如检查点和脏页写入)可以减少这种损失的发生,但也会导致停顿、上下文切换和资源争用。
事务提交是另一个干扰来源;提交一个事务时的停顿会阻止其他事务的进行。在云级分布式系统中,使用多阶段同步协议(如两阶段提交,2PC)处理提交是一项挑战。这些协议不能容忍故障,并且大规模分布式系统有连续的硬故障和软故障的 "背景噪声"。它们也是高延迟的,因为大规模系统分布在多个数据中心。
在本文中,我们描述了亚马逊的 Aurora ,这是一种新的数据库服务,通过在高度分布式的云环境中更积极地利用重做日志来解决上述问题。我们使用一种新颖的面向服务的体系结构(见图1),该体系结构带有一个多租户横向扩展存储服务,该服务抽象出一个虚拟化的分段重做日志,并松散地耦合到一组数据库实例。虽然每个实例仍然包括传统内核的大部分组件(查询处理器、事务、锁定、缓冲区缓存、访问方法和撤消管理),但是几个功能(重做日志记录、持久存储、崩溃恢复和备份/恢复)被下放(off-loaded)到存储服务中。
与传统方法相比,我们的体系结构有三大优势。首先,通过将存储构建为跨多个数据中心的独立容错和自我修复服务,我们可以保护数据库免受网络或存储层的性能差异以及暂时或永久故障的影响。我们观察到,持久性的失败可以被建模为长期的可用性事件,可用性事件可以被建模为长期的性能变化——一个设计良好的系统可以统一处理这些问题。其次,通过仅将重做日志记录写入存储,我们能够将网络 IOPS 减少一个数量级。一旦我们消除了这个瓶颈,我们就能够积极地优化许多其他的争用点,在我们开始的 MySQL 代码基础上获得了显著的吞吐量改进。第三,我们将一些最复杂和最关键的功能(备份和重做恢复)从数据库引擎中的一次性昂贵操作转移到在大型分布式机群中摊销的连续异步操作。这产生了没有检查点的近乎即时的崩溃恢复,以及不干扰前台处理的廉价备份。在本文中,我们描述了三个贡献:
(1) 如何推断云规模的持久性,以及如何设计能够应对相关故障的法定系统。(第 2 节)。
(2) 如何通过将传统数据库的较低部分转移到这一层来利用智能存储。(第 3 节)。
(3) 如何消除分布式存储中的多阶段同步、崩溃恢复和检查点(第 4 节)。
然后,我们在第 5 节展示了如何将这三个想法结合起来设计 Aurora 的整体架构,然后在第 6 节回顾了我们的性能结果,并在第 7 节总结了我们的经验教训。最后,我们在第 8 节简要介绍了相关工作,并在第 9 节给出了结论。
如果数据库系统不做其他事情,它必须满足数据一旦被写入就可以被读取的约定。不是所有系统都这样。在本节中,我们将讨论我们的法定模型背后的基本原理、我们对存储进行细分的原因,以及这两者如何结合起来,不仅提供耐用性、可用性和减少抖动,还帮助我们解决大规模管理存储集群的运营问题。
实例生存期与存储生存期不太相关(Instance lifetime does not correlate well with storage lifetime)。实例失败。客户关闭了它们。他们根据负载上下调整它们的大小。出于这些原因,它有助于将存储层与计算层分离。一旦这样做,这些存储节点和磁盘也不能避免出现故障。因此,必须以某种形式复制它们,以提供故障恢复能力。在大规模云环境中,存在节点、磁盘和网络路径故障。每个故障可能有不同的持续时间和不同的影响范围。例如,一个节点可能会暂时缺乏网络可用性,重新启动时会暂时停机,或者磁盘、节点、机架、叶或主干网络交换机甚至数据中心会永久出现故障。
在复制系统中容忍故障的一种方法是使用基于法定人数的投票协议(quorum-based voting protocol)。如果复制数据项的每一个 V 副本都被分配了一个投票,则读或写操作必须分别获得 Vr 投票的读法定人数或 Vw 投票的写法定人数。为了实现一致性,法定人数必须遵守两个规则。首先,每次读取必须知道最近的写入,公式为 Vr + Vw > V。此规则确保用于读取的节点集与用于写入的节点集相交,并且读取仲裁包含至少一个具有最新版本的位置。其次,每次写操作必须知道最近的写操作,以避免写冲突操作,公式为 Vw > V/2。容忍单个节点丢失的常见方法是将数据复制到(V = 3)个节点,并依赖 2/3 (Vw = 2)的写入法定人数和 2/3 (Vr = 2)的读取法定人数。
我们认为 2/3 的法定人数是不够的。为了说明为什么,让我们首先解释 AWS 中可用性区域(Availability Zone,AZ)的概念。接入点是一个区域的子集,它通过低延迟链路连接到该区域的其他接入点,但针对大多数故障(包括电源、网络、软件部署、泛洪等)进行隔离。跨 AZs 分布数据副本可确保典型的大规模故障模式仅影响一个数据副本。这意味着人们可以简单地将三个副本中的每一个放在不同的 AZ 中,并且除了较小的单个故障之外,还可以容忍大规模事件。
然而,在大型存储设备群中,故障意味着,在任何给定的时刻,磁盘或节点的某个子集可能已经发生故障,并且正在被修复。这些故障可能独立地分布在 AZ A、B 和 C 中的每个节点上。但是,由于火灾、屋顶故障、洪水等原因,AZ C 的故障将破坏 AZ A 或 AZ B 中同时发生故障的任何副本的法定人数。此时,在 2/3 读取法定人数模型中,我们将丢失两个副本,并且无法确定第三个副本是否是最新的。换句话说,虽然每个 AZ 中副本的单个故障是不相关的,但 AZ 的故障是该 AZ 中所有磁盘和节点的相关故障。Quorums 需要容忍 AZ 故障以及同时发生的故障。
在 Aurora,我们选择了一个设计点,该设计点允许 (a) 丢失一个完整的 AZ 和一个额外的节点(AZ+1)而不丢失数据,以及 (b) 丢失一个完整的 AZ 而不影响写入数据的能力。我们通过在 3 个 AZ 中以 6 次复制每个数据项来实现这一点,每个 AZ 中每个数据项有 2 个副本。我们使用的法定人数模型是 6 票(V = 6),写法定人数是 4/6 (Vw = 4),读法定人数是 3/6 (Vr = 3)。利用这样的模型,我们可以 (a) 丢失单个 AZ 和一个额外的节点(3 个节点的故障)而不丢失读可用性,以及 (b) 丢失任何两个节点,包括单个 AZ 故障并保持写可用性。确保读取仲裁使我们能够通过添加额外的复制副本来重建写入仲裁。
我们来考虑一下 AZ+1 是否提供了足够的耐久性这个问题。为了在该模型中提供足够的耐用性,必须确保在修复其中一个故障(Mean Time to Repair,MTTR)所需的时间内,不相关故障的双故障概率(Mean Time to Failure,MTTF)足够低。如果双重故障的概率足够高,我们可能会在 AZ 故障上看到这些,从而打破法定人数。超越一个点,很难降低 MTTF 独立失败的概率。相反,我们专注于减少 MTTR,以缩小双重失误的脆弱性。为此,我们将数据库卷划分为固定大小的小段,目前大小为 10GB。这些数据以 6 次复制到保护组(Protection Groups,PGs)中,因此每个保护组由 6 个 10GB 的数据段组成,跨三个保护组组织,每个保护组中有两个数据段。存储卷(storage volume)是一组串联的 PGs,使用大量存储节点进行物理实施,这些存储节点使用亚马逊弹性计算云(EC2)作为虚拟主机进行配置,并连接固态硬盘。构成卷的 PGs 是随着卷的增长而分配的。我们目前支持在未复制的基础上增长到 64TB 的卷。
分段(Segments)现在是我们独立的故障和修复单元。作为我们服务的一部分,我们监控并自动修复故障。在 10Gbps 网络链路上,10GB 的段可以在 10 秒内修复。我们需要在相同的 10 秒窗口中看到两个这样的故障,再加上一个不包含这两个独立故障中的任何一个的 AZ 故障,以失去法定人数。以我们观察到的失败率来看,这是完全不可能的,即使对于我们为客户管理的大量数据库来说也是如此。
一旦一个人设计了一个对长期失败有天然弹性的系统,它自然也会对短期失败有弹性。能够处理 AZ 长期丢失的存储系统也可以处理因电源事件或需要回滚的坏软件部署而导致的短暂停机。一个可以处理仲裁成员的多秒可用性损失的节点可以处理存储节点上短暂的网络拥塞或负载(One that can handle a multisecond loss of availability of a member of a quorum can hand)。
由于我们的系统对故障有很高的容忍度,因此我们可以利用这一点来进行维护操作,以免造成数据段不可用。例如,热数据管理(heat management)很简单。我们可以将热磁盘或节点上的某个段标记为坏段,通过迁移到机群中其他较冷的节点,可以快速修复仲裁。操作系统和安全修补(OS and security patching)是正在修补的存储节点的短暂不可用事件。甚至我们存储设备的软件升级也是这样管理的。我们一次执行一个 AZ,并确保一个 PG 中不超过一个成员同时被修补。这使我们能够在存储服务中使用灵活的方法和快速部署。
在本节中,我们将解释为什么在第 2 节中描述的分段复制存储系统上使用传统数据库会在网络 IOs 和同步停顿方面带来难以承受的性能负担。然后,我们解释了我们将日志处理下放到存储服务的方法,并通过实验演示了我们的方法如何显著减少网络 IOs。最后,我们描述了我们在存储服务中使用的各种技术,以最大限度地减少同步停顿和不必要的写入。
我们对存储卷进行分段并以 4/6 写入法定量以 6 次复制每个分段的模型为我们提供了高弹性。不幸的是,对于 MySQL 这样的传统数据库来说,这种模型导致了难以维持的性能,因为 MySQL 为每个应用程序编写生成许多不同的实际 I/O。高 I/O 卷被复制放大,造成每秒数据包数(packets per second,PPS)沉重的负担。此外,输入/输出操作系统会导致同步点,从而导致流水线停滞和延迟扩大。尽管链复制(chain replication)及其替代方案可以降低网络成本,但它们仍会遭受同步停滞和附加延迟的困扰。
让我们来看看写操作在传统数据库中是如何工作的。像 MySQL 这样的系统将数据页写入它公开的对象(例如堆文件、B 树等)。以及重做日志记录写入预写日志(WAL)。每个重做日志记录都由被修改页面的后映像和前映像之间的差异组成(Each redo log record consists of the difference between the after-image and the before-image of the page that was modified)。日志记录可以应用于页面的前图像,以产生其后图像。
实际上,还必须写入其他数据。例如,考虑一个同步镜像 MySQL (mirrored MySQL)配置,它实现了跨数据中心的高可用性,并在主备配置下运行,如图 2 所示。AZ1 中有一个活跃的 MySQL 实例,在亚马逊弹性块存储(EBS)上有网络存储。AZ2 中还有一个备用 MySQL 实例,EBS 上也有网络存储。对主 EBS 卷的写入使用软件镜像与备用 EBS 卷同步(The writes made to the primary EBS volume are synchronized with the standby EBS volume using software mirroring)。
图 2 显示了引擎需要写入的各种类型的数据:重做日志、归档到亚马逊简单存储服务(S3)以支持即时恢复的二进制(表单)日志、修改的数据页面、数据页的第二次临时写入(双写)以防止页面损坏(the modified data pages, a second temporary write of the data page (double-write) to prevent torn pages)以及最后的元数据(FRM)文件。该图还显示了实际输入输出流的顺序,如下所示。在步骤 1 和 2 中,写入被发送到 EBS,EBS 又将其发送到 AZ 本地镜像,当两者都完成时,接收到确认。接下来,在步骤 3 中,使用同步数据块级软件镜像将写入转移到备用实例。最后,在步骤 4 和 5 中,写操作被写入备用 EBS 卷和相关镜像。
上面描述的镜像 MySQL 模型是不受欢迎的,这不仅是因为数据是如何写入的,也是因为写入了什么数据。首先,步骤 1、3 和 5 是顺序的和同步的。延迟是附加的,因为许多写入是顺序的。抖动会被放大,因为即使是异步写入,也必须等待最慢的操作,使系统任由异常值摆布。从分布式系统的角度来看,该模型可以被视为具有 4/4 写入仲裁,并且容易受到故障和异常性能的影响。其次,由 OLTP 应用程序导致的用户操作会导致许多不同类型的写入,这些写入通常以多种方式表示相同的信息,例如,为了防止存储基础架构中的页面损坏而写入双写缓冲区(the writes to the double write buffer in order to prevent torn pages in the storage infrastructure)。
当传统数据库修改数据页面时,它会生成重做日志记录,并调用日志应用程序,将重做日志记录应用到页面的内存前映像,以生成其后映像。事务提交要求写入日志,但数据页写入可能会被推迟。
在 Aurora 中,唯一跨越网络的写入是重做日志记录。从未有页面是从数据库层被写入的,不用于后台写入,不用于检查点,也不用于缓存回收。相反,日志应用程序被推送到存储层,在那里它可以用于在后台或按需生成数据库页面。当然,从一开始就从完整的修改链中生成每个页面是非常昂贵的。因此,我们不断在后台物化数据库页面,以避免每次按需从头开始重新生成它们。请注意,从正确性的角度来看,后台物化完全是可选的:就引擎而言,日志是数据库,存储系统物化的任何页面都只是日志应用程序的缓存。还要注意,与检查点不同,只有具有长串修改的页面才需要重新具体化。检查点由整个重做日志链的长度决定。Aurora 页面具体化是由给定页面的链长度决定的。
我们的方法显著降低了网络负载,尽管增加了复制写入,并提供了性能和耐用性。存储服务可以以令人尴尬的并行方式扩展输入/输出,而不会影响数据库引擎的写吞吐量(The storage service can scale out I/Os in an embarrassingly parallel fashion without impacting write throughput of the database engine)。例如,图 3 显示了一个 Aurora 集群,其中一个主实例和多个副本实例跨多个 AZ 部署。在此模型中,主服务器仅将日志记录写入存储服务,并将这些日志记录以及元数据更新流式传输到复制副本实例。输入输出流根据一个公共目标(一个逻辑段,即一个 PG)对日志记录进行完全排序,并将每一批传送到所有 6 个副本,其中该批保存在磁盘上,数据库引擎等待 6 个副本中的 4 个的确认,以满足写仲裁,并认为有问题的日志记录是持久的或加固的。副本使用重做日志记录将更改应用到它们的buffer caches(The replicas use the redo log records to apply changes to their buffer caches)。
为了测量网络输入/输出,我们使用 SysBench 只写工作负载运行了一个测试,其中包含上述两种配置的 100GB 数据集:一个是跨多个 AZ 的同步镜像 MySQL 配置,另一个是 RDS Aurora(replicas across multiple AZs,跨多个 AZ 的副本)。在这两个实例中,测试针对运行在 r3.8xlarge EC2 实例上的数据库引擎运行了 30 分钟。
我们实验的结果总结在表 1 中。在 30 分钟的时间里,Aurora 能够维持比镜像 MySQL多 35 倍的事务。尽管使用 Aurora 将写入放大了 6 倍,并且不包括 EBS 中的链式复制和 MySQL 中的跨 AZ 写入,但 Aurora 中数据库节点上每个事务的 I/O 数量比镜像 MySQL 中的少 7.7 倍。每个存储节点都可以看到未复制的写入,因为它只是六个拷贝中的一个,导致该层需要处理的输入/输出减少了 46 倍。通过向网络写入更少的数据,我们节省了成本,这使我们能够积极复制数据以提高耐用性和可用性,同时,并行发出请求以最大限度地减少抖动的影响。
将处理转移到存储服务还可以通过最大限度地减少崩溃恢复时间来提高可用性,并消除后台进程(如检查点、后台数据页面写入和备份)引起的抖动。
来检查崩溃恢复。在传统数据库中,崩溃后,系统必须从最近的检查点开始,并重放日志,以确保所有持久的重做记录都已应用。在 Aurora 中,持久化重做记录的应用程序在存储层连续、异步地发生,并分布在整个车队中。如果数据页不是最新的,对该页的任何读请求可能需要应用一些重做记录。因此,崩溃恢复的过程分散在所有正常的前台处理中。数据库启动时不需要任何东西。
我们存储服务的核心设计宗旨是最大限度地减少前台写请求的延迟。我们将大部分存储处理转移到后台。鉴于存储层的峰值和平均前台请求之间的自然可变性,我们有足够的时间在前台路径之外执行这些任务(we have ample time to perform these tasks outside the foreground path)。我们也有机会用 CPU 换磁盘。例如,当存储节点忙于处理前台写请求时,没有必要运行旧页面版本的垃圾回收,除非磁盘接近容量。在 Aurora 中,后台处理与前台处理负相关。这不同于传统数据库,在传统数据库中,页面的后台写入和检查点与系统的前台负载呈正相关。如果我们在系统上建立了一个 backlog,我们将抑制前台活动,以防止长队列的建立。由于数据段以高熵分布在系统中的各个存储节点上,一个存储节点上的节流很容易由我们的 4/6 仲裁写入处理,所以表现为一个慢速节点。
让我们更详细地回顾一下存储节点上的各种活动。如图 4 所示,它包括以下步骤:(1) 接收日志记录并添加到内存队列中,(2) 将记录保存在磁盘上并进行确认,(3) 组织记录并识别日志中的空白,因为一些批次可能会丢失,(4) 与对等方利用 gossip 以填补空缺,(5) 将日志记录合并到新的数据页面中,(6) 定期将日志和新页面转移到 S3,(7) 定期垃圾回收旧版本,最后 (8) 定期验证页面上的循环冗余校验代码。
请注意,不仅上述每个步骤是异步的,而且只有步骤 (1) 和 (2) 在前台路径中,这可能会影响延迟。
在本节中,我们将描述如何从数据库引擎生成日志,以便持久状态、运行时状态和副本状态始终保持一致。特别是,我们将描述如何在没有昂贵的 2PC 协议的情况下高效地实现一致性。首先,我们展示了如何在崩溃恢复时避免昂贵的重做处理。接下来,我们解释正常操作以及如何维护运行时间和副本状态。最后,我们提供恢复过程的详细信息。
因为我们将数据库建模为重做日志流(如第 3 节所述),所以我们可以利用日志作为有序的变更序列向前推进的事实。实际上,每个日志记录都有一个关联的日志序列号(Log Sequence Number,LSN),它是由数据库生成的单调递增的值。
这让我们通过以异步方式处理问题来简化维护状态的一致性协议,而不是使用像 2PC 这样喋喋不休、不能容忍故障(chatty and intolerant)的协议。在高层次上,我们保持一致性和持久性,并在收到未完成存储请求的确认时不断提高这些点。由于任何单个存储节点可能会丢失一个或多个日志记录,因此它们会与 PG 的其他成员 gossip,寻找差距并填补漏洞。由数据库维护的运行时状态允许我们使用单个段读取,而不是仲裁读取,除非在状态丢失且必须重建时进行恢复。
数据库可能有多个未完成的独立事务,这些事务可以以不同于启动的顺序完成(达到已完成和持久的状态)。假设数据库崩溃或重新启动,是否回滚的决定对于每个事务都是独立的。跟踪部分完成的事务和撤销它们的这一逻辑是保存在数据库的引擎中,就像它正在向简单磁盘写入一样(The logic for tracking partially completed transactions and undoing them is kept in the database engine, just as if it were writing to simple disks)。但是,在重新启动时,在允许数据库访问存储卷之前,存储服务会进行自己的恢复,恢复的重点不是用户级事务,而是确保数据库看到统一的存储视图,尽管它是分布式的。
存储服务确定最高的 LSN,这样它可以保证所有先前的日志记录可用性(这称为 VCL 或 Volume Complete LSN)。在存储恢复期间,LSN 大于 VCL 的每个日志记录都必须被截断。然而,数据库可以进一步约束点的子集,这可以通过标记日志记录来截断,并将它们标识为 CPLs 或一致性点 LSNs(The database can, however, further constrain a subset of points that are allowable for truncation by tagging log records and identifying them as CPLs or Consistency Point LSNs)。因此,我们将 VDL (卷耐用的 LSN ,Volume Durable LSN)定义为最高的 CPL,它小于或等于 VCL,并截断所有 LSN 大于 VDL 的日志记录。例如,即使我们有 LSN 1007 之前的完整数据,数据库也可能已经声明只有 900、1000 和 1100 是 CPLs,在这种情况下,我们必须在 1000 截断。我们完成到 1007,但只耐用到 1000(We are complete to 1007, but only durable to 1000)。
因此,完整性和持久性是不同的,并且可以认为一个 CPL 描述了某种必须有序接受的存储系统事务的有限形式。如果客户端不需要这种区分,它可以简单地将每个日志记录标记为一个公共对象。实际上,数据库和存储的交互方式如下:
(1) 每个数据库级事务被分解成多个有序的、必须原子执行的小型事务。
(2) 每个小型事务由多个连续的日志记录组成(需要多少就有多少)。
(3) 小型事务中的最终日志记录是一个 CPL。
在恢复时,数据库与存储服务对话以建立每个 PG 的持久点,并使用该持久点来建立 VDL,然后发出命令来截断 VDL 上的日志记录。
我们现在描述数据库引擎的 "正常操作",并依次关注写、读、提交和副本。
在Aurora中,数据库不断与存储服务交互,并保持状态以建立仲裁、提高卷持久性,并将事务注册为已提交。例如,在正常/转发路径中,当数据库收到确认以建立每批日志记录的写入仲裁时,它会推进当前的 VDL。在任何给定时刻,数据库中可能有大量并发事务处于活动状态,每个事务都生成自己的重做日志记录。数据库为每个日志记录分配一个唯一的有序 LSN,但有一个限制条件,即分配给 LSN 的值不能大于当前 VDL 值和一个称为 LSN 分配限制(LSN Allocation Limit,LAL)的常数之和(当前设置为 1000 万)。此限制可确保数据库不会远远领先于存储系统,并在存储或网络跟不上的情况下引入 back-pressure,从而抑制传入的写入。
请注意,每个 PG 的每个段只看到卷中影响驻留在该段上的页面的日志记录的子集。每个日志记录都包含一个反向链接,用于标识该 PG 的前一个日志记录。这些反向链接可用于跟踪到达每个段的日志记录的完整点,以建立段完整 LSN(Segment Complete LSN,SCL),该 LSN 标识接收到 PG 的所有日志记录的最大 LSN。存储节点在相互 gossip 时会使用 SCL,以便找到并交换丢失的日志记录。
在 Aurora 中,事务提交是异步完成的。当客户端提交事务时,处理提交请求的线程通过将其 "提交 LSN" 记录为等待提交的单独事务列表的一部分来搁置事务,并继续执行其他工作。等价的 WAL 协议是基于完成一个提交,当且仅当,最新的 VDL 大于或等于事务的提交 LSN(The equivalent to the WAL protocol is based on completing a commit, if and only if, the latest VDL is greater than or equal to the transaction’s commit LSN)。随着 VDL 进程的推进,数据库识别出符合条件的等待提交的事务,并使用专用线程向等待的客户端发送提交确认。工作线程不会暂停提交,它们只是拉取其他挂起的请求并继续处理。
在 Aurora 中,与大多数数据库一样,页面是从缓冲区缓存(buffer cache)中提供的,只有当缓存中不存在有问题的页面时,才会产生存储输入输出请求。
如果缓冲区缓存已满,系统会找到一个要从缓存中替换的页面。在传统系统中,如果被替换的页面是一个 "脏页",那么在替换它之前会被刷新到磁盘。这是为了确保页面的后续提取的总是最新的数据。虽然 Aurora 数据库在替换页面时(或在任何其他地方)不会写出页面,但它会强制执行类似的保证:缓冲区缓存中的页面必须始终是最新版本。只有当页面的 "页面 LSN"(标识与页面的最新更改相关联的日志记录)大于或等于 VDL 时,才会通过从缓存中逐出页面来实现该保证。该协议确保:(a) 页面中的所有更改都已在日志中得到强化,以及 (b) 缓存未命中时,请求当前 VDL 的页面版本以获得其最新的持久版本就足够了。
在正常情况下,数据库不需要使用读取法定人数来建立共识。从磁盘读取页面时,数据库会建立一个读取点,代表发出请求时的 VDL。然后,数据库可以选择相对于读取点(read-point)完整的存储节点,知道它将因此接收最新版本。存储节点返回的页面必须与数据库中小型事务的预期语义一致(A page that is returned by the storage node must be consistent with the expected semantics of a mini-transaction (MTR) in the database)。由于数据库直接管理向存储节点输送日志记录和跟踪进度(即每个段的 SCL),所以它通常知道哪个段能够满足读取(SCL 大于读取点的段),因此可以直接向具有足够数据的段发出读取请求。
假设数据库知道所有未完成的读取,它可以在任何时候基于每个 PG 计算最小读取点 LSN。如果存在读取副本,写者会使用 gossip 协议,以在所有节点上建立每点最小读取点 LSN(If there are read replicas the writer gossips with them to establish the per-PG Minimum Read Point LSN across all nodes)。该值称为保护组最小读取点 LSN (PGMRPL),代表 "低水位线",低于该值时,保护组的所有日志记录都是不必要的(This value is called the Protection Group Min Read Point LSN (PGMRPL) and represents the "low water mark" below which all the log records of the PG are unnecessary)。换句话说,存储节点段保证不会有读取点低于 PGMRPL 的读取页面请求。每个存储节点都知道数据库中的 PGMRPL,因此可以通过合并旧的日志记录并安全地垃圾回收它们来推进磁盘上的物化页面。
实际的并发控制协议是在数据库引擎中执行的,就像传统的 MySQL 一样,数据库页面和撤消段是在本地存储中组织的。
在 Aurora 中,单个写者和多达 15 个读取副本都可以装载一个共享存储卷。因此,读取副本不会增加存储消耗或磁盘写入操作的额外成本。为了最大限度地减少延迟,由写入程序生成并发送到存储节点的日志流也会发送到所有读取的副本。在读者中,数据库通过依次考虑每个日志记录来消耗这个日志流。如果日志记录引用了读者缓冲区缓存中的页面,它将使用日志应用程序对缓存中的页面应用指定的重做操作。否则,它只会丢弃日志记录。请注意,从写者的角度来看,副本异步地使用日志记录,这表明用户提交独立于副本。复制副本在应用日志记录时遵循以下两个重要规则:(a) 将应用的唯一日志记录是 LSN 小于或等于 VDL 的日志记录,以及 (b) 作为单个小型事务一部分的日志记录被自动应用到复制副本的缓存中,以确保复制副本看到所有数据库对象的一致的视图。实际上,每个副本通常比写者落后一小段时间( 20 毫秒或更短)。
大多数传统数据库使用诸如 ARIES 之类的恢复协议,该协议依赖于可以表示所有提交事务的精确内容的预写日志(write-ahead log,WAL)的存在。这些系统还定期检查数据库,通过将脏页刷新到磁盘并将检查点记录写入日志,以粗粒度的方式建立持久性点。重新启动时,任何给定的页面都可能会丢失一些已提交的数据或包含未提交的数据。因此,在崩溃恢复时,系统通过使用日志应用程序将每个日志记录应用到相关的数据库页面来处理自上一个检查点以来的重做日志记录。这个过程使数据库页面在故障点达到一致的状态,之后可以通过执行相关的撤消日志记录来回滚崩溃期间正在进行的事务。崩溃恢复可能是一项昂贵的操作。减少检查点间隔有所帮助,但代价是干扰前台事务。Aurora 不需要这样的权衡。
传统数据库的一个非常简单的原则是,在正向处理路径和恢复中使用相同的重做日志应用程序,当数据库脱机时,它在前台同步操作。我们在 Aurora 中也依赖于相同的原则,除了重做日志应用程序从数据库中分离出来,并在存储节点上并行运行,并且一直在后台运行。一旦数据库启动,它就会与存储服务协作执行卷恢复,因此,即使 Aurora 数据库在每秒处理超过 100,000 条写语句时崩溃,它也可以非常快地恢复(通常不到 10 秒)。
崩溃后,数据库确实需要重新建立其运行时状态。在这种情况下,它为每个 PG 联系一个段的读取法定人数,该法定人数足以保证发现任何可能已经达到写入法定人数的数据。一旦数据库为每个 PG 建立了一个读定额,它就可以重新计算 VDL,在这个 VDL 之上的数据将被截断,生成一个截断范围,该范围将使新 VDL 之后的每个日志记录失效,直到并包括一个终端 LSN,数据库可以证明它至少是有史以来最高的未完成日志记录的最高值。数据库推断这个上限是因为它分配了 LSNs,并限制了分配在 VDL 以上的距离(前面描述的 1000 万的限制)。截断范围用 epoch 号进行版本控制,并持久地写入存储服务,以便在恢复中断和重新启动的情况下不会混淆截断的持久性。
数据库仍然需要执行撤消恢复,以便在崩溃时解除运行中事务的操作。但是,在系统根据撤消段(undo segments)建立这些正在进行的事务的列表之后,当数据库联机时,撤消恢复可能会发生。
在本节中,我们将描述 Aurora 的构建模块,如图 5 中的鸟瞰图所示。
数据库引擎是 MySQL/InnoDB "社区" 的一个分支,主要在 InnoDB 如何读写磁盘数据方面有所不同。在社区 InnoDB 中,写操作导致数据在缓冲区页面中被修改,相关的重做日志记录按 LSN 顺序写入 WAL 的缓冲区。在事务提交时,WAL 协议只要求事务的重做日志记录持久地写入磁盘。实际修改后的缓冲区页面最终也会通过双写技术写入磁盘,以避免部分页面写入。这些页面写入在后台进行,或者在从缓存中替换的过程中进行,或者在执行检查点时进行。除了输入输出子系统,InnoDB 还包括事务子系统、锁管理器、B+ 树实现和相关的 "小型事务" 概念。中期审查是仅在 InnoDB 内部使用的构造,它对必须以原子方式执行的操作组进行建模(例如,B+树页面的拆分/合并)。
在 Aurora InnoDB 变体中,表示必须在每个 MTR 中自动执行的更改的重做日志记录被组织成由每个日志记录所属的 PGs 分片的批数据,并且这些批数据被写入存储服务。每个中期审查的最终日志记录被标记为一致性点。Aurora 支持与社区 MySQL 在写者中支持的完全相同的隔离级别(标准 ANSI 级别和快照隔离或一致性读取)。Aurora 读取副本在写入程序中获取有关事务启动和提交的连续信息,并使用这些信息来支持本地事务的快照隔离,这些事务当然是只读的。请注意,并发控制完全是在数据库引擎中实现的,不会影响存储服务。存储服务提供了底层数据的统一视图,该视图在逻辑上与你将数据写入社区 InnoDB 中的本地存储所获得的视图相同。
Aurora 利用亚马逊关系数据库服务作为其控制平面。RDS(Relational Database Service)在数据库实例上包括一个称为主机管理器(Host Manager,HM)的代理,它监控集群的运行状况,并确定集群是否需要故障转移,或者是否需要替换实例。每个数据库实例都是一个集群的一部分,该集群由一个写者和零个或多个读者组成。集群的实例位于单个地理区域中(例如,us-east-1、us-west-1等),通常放置在不同的自动仓库中,并连接到同一地区的存储集群。为了安全起见,我们隔离了数据库、应用程序和存储之间的通信。实际上,每个数据库实例都可以在三个亚马逊虚拟私有云(VPC)网络上进行通信:客户应用程序通过其与引擎交互的客户 VPC、数据库引擎和控制平面通过其相互交互的 RDS VPC 以及数据库通过其与存储服务交互的存储 VPC。
存储服务部署在 EC2 虚拟机群集上,这些虚拟机在每个区域至少有 3 个自动存储区进行资源调配,并共同负责资源调配多个客户存储卷,在这些卷之间读写数据,以及在这些卷之间备份和恢复数据。存储节点操作本地固态硬盘,并与数据库引擎实例、其他对等存储节点和备份/恢复服务交互,这些服务可根据需要将更改的数据连续备份到 S3,并从 S3 恢复数据。存储控制平面使用亚马逊动态数据库服务来持久存储集群和存储卷配置、卷元数据以及备份到 S3 的数据的详细描述。对于编排长期运行的操作,例如存储节点故障后的数据库卷恢复操作或修复(重新复制)操作,存储控制平面使用亚马逊简单工作流服务。保持高水平的可用性要求在最终用户受到影响之前,对真实和潜在的问题进行主动的、自动化的早期检测。使用指标收集服务不断监控存储操作的所有关键方面,如果关键性能或可用性指标表明存在问题,就会发出警报。
在本节中,我们将分享我们在将 Aurora 作为生产服务运行方面的经验,该服务于 2015年 7 月推出。我们首先总结运行行业标准基准测试的结果,然后展示我们客户的一些性能结果。
在这里,我们展示了不同实验的结果,这些实验使用行业标准基准(如 SysBench 和 TPC-C 变体)比较了 Aurora 和 MySQL 的性能。我们在连接到具有 30K 预配 IOPS 的 EBS 卷的实例上运行 MySQL。除非另有说明,这些都是 r3.8X 大型 EC2 实例,具有 32 个vCPU和 244GB 内存,采用 Intel Xeon E5-2670 v2(Ivy Bridge)处理器。r3.8XL 上的缓冲缓存设置为 170GB。
在这个实验中,我们报告了 Aurora 中的吞吐量可以随着实例大小线性扩展,最高实例大小可以是 MySQL 5.6 和 MySQL 5.7 的 5 倍。请注意,Aurora 目前基于 MySQL 5.6 代码库。我们在 r3 系列的 5 个 EC2 实例(large、xlarge、2xlarge、4xlarge、8xlarge)上运行了 1GB 数据集(250 个表)的 SysBench 只读和只写基准测试。每个实例的大小正好是紧随其后的较大实例 vCPUs 和内存的一半。
结果如图 7 和图 6 所示,分别用每秒写和读语句来衡量性能。Aurora 的性能在每个更高的实例大小下加倍,对于 r3.8xlarge,达到 121,000 次写入/秒和 600,000 次读取/秒,是 MySQL 5.7 的 5 倍,MySQL 5.7 的最高读取/秒和 125,000 次写入/秒。
在这个实验中,我们报告说,Aurora 的吞吐量明显超过 MySQL,即使有更大的数据大小,包括具有缓存外工作集的工作负载。表 2 显示,对于 SysBench 只写工作负载,Aurora 比 MySQL 快 67 倍,数据库大小为 100GB。即使对于 1TB 的数据库大小和缓存外的工作负载,Aurora 仍然比 MySQL 快 34 倍。
在这个实验中,我们报告了 Aurora 中的吞吐量可以随着客户端连接的数量而扩展。表 3 显示了当连接数从 50 增长到 500 再增长到 5000 时,以每秒写入数计算的 SysBench OLTP 基准测试的运行结果。虽然 Aurora 从每秒 40,000 次写入扩展到每秒 110,000 次写入,但 MySQL 的吞吐量在大约 500 个连接时达到峰值,然后随着连接数量增长到 5000 个而急剧下降。
在这个实验中,我们报告说 Aurora 读取副本的延迟明显低于 MySQL 副本,即使工作负载更高。表 4 显示,随着工作负载在每秒 1000 到 10000 次写入之间变化,Aurora 中的复制延迟从 2.62 毫秒增加到 5.38 毫秒。相比之下,MySQL 中的副本延迟从不到一秒钟增长到 300 秒钟。在每秒 10,000 次写入时,Aurora 的复制延迟比 MySQL 小几个数量级。副本延迟是根据提交的事务在副本中可见所需的时间来衡量的。
6.1.5 热行争用时的吞吐量
在这个实验中,我们报告说,相对于 MySQL,Aurora 在具有热行争用(hot row contention)工作负载上表现非常好,例如基于 TPC-C 基准的工作负载。我们针对亚马逊 Aurora 和 MySQL 5.6 和 5.7 运行了Percona TPC-C变体,在 3.8xlarge 上, MySQL 使用了一个配备了 30K 的 EBS 卷。表 5 显示,Aurora 的吞吐量是 MySQL 5.7 的 2.3 到 16.3 倍,因为工作负载从 500 个连接和 10GB 的数据大小到 5000 个连接和 100GB 的数据大小不等。
在本节中,我们将分享一些客户报告的结果,这些客户将生产工作负载从 MySQL 迁移到 Aurora。
一家互联网游戏公司将其生产服务从 MySQL 迁移到 Aurora 的 3.4x 大型实例上。他们的网络事务在迁移前的平均响应时间为 15 毫秒。相比之下,迁移后的平均响应时间为 5.5 毫秒,提高了 3 倍,如图 8 所示。
一家教育技术公司,其服务帮助学校管理学生笔记本电脑,将他们的生产工作负载从MySQL 迁移到 Aurora。迁移前后(14:00时)选择和每条记录插入操作的中值(P50)和 95% 延迟如图 9 和图 10 所示。在迁移之前,P95的延迟在 40 毫秒到 80 毫秒之间,比 P50 的大约 1 毫秒的延迟差得多。该应用程序遇到了我们在本文前面描述的那种糟糕的异常性能。然而,迁移后,两种操作的 P95 延迟显著改善,接近 P50 延迟。
据 Pinterest 的 Weiner 报道,MySQL 副本通常远远落后于它们的写者,并且可能 "导致奇怪的错误"。对于前面描述的教育技术公司来说,副本延迟通常会增加到 12 分钟,影响应用程序的正确性,因此副本仅作为支持服务。相比之下,迁移到 Aurora 后,4 个副本的最大副本延迟从未超过 20 毫秒,如图 11 所示。Aurora 提供的改进的副本延迟让该公司将应用程序负载的很大一部分转移到副本上,从而节省了成本并提高了可用性。
我们现在已经看到客户运行的各种各样的应用程序,从小型互联网公司一直到运行大量 Aurora 集群的高度复杂的组织。虽然他们的许多用例都是标准的,但我们关注的是云环境中常见的场景和期望,这些场景和期望引领我们走向新的方向。
我们的许多客户都在运营软件即服务(SaaS)业务,或者独家运营,或者与一些剩余的内部客户一起运营,他们正试图转向他们的 SaaS 模式。我们发现,这些客户通常依赖于他们无法轻易更改的应用程序。因此,他们通常通过使用模式/数据库(schema/database)作为租赁单位,将不同的客户整合到一个实例上。这个习惯用法降低了成本:当不可能所有的客户同时活跃时,他们避免为每个客户的专用实例付费。例如,我们的一些 SaaS 客户报告说,他们自己有超过 50,000 名客户。
该模型与众所周知的多租户应用程序(如 Salesforce.com)明显不同,后者使用多租户数据模型,将多个客户的数据打包到单个模式的统一表中,并按行识别租赁。因此,我们看到许多客户的统一数据库包含大量表。小型数据库中超过 150,000 个表的生产实例非常常见。这给管理元数据的组件(如字典缓存)带来了压力。更重要的是,此类客户需要 (a) 维持高水平的吞吐量和许多并发用户连接,(b) 一种模型,在该模型中,数据仅在使用时提供并付费,因为很难预先预测需要多少存储空间,以及 (c) 减少抖动,以便单个租户的峰值对其他租户的影响最小。Aurora 支持这些属性,非常适合 SaaS 的应用。
互联网工作负载通常需要处理基于突发事件的流量峰值。我们的一个主要客户在一个非常受欢迎的全国电视节目中有一个特殊的场景出现,并经历了一个这样的高峰,大大超过了他们的正常峰值吞吐量,而没有给数据库带来压力。为了支持这种峰值,数据库处理许多并发连接是很重要的。这种方法在 Aurora 中是可行的,因为底层存储系统可扩展性很好。我们有几个客户每秒运行超过 8000 个连接。
现代 Web 应用程序框架,如 Ruby on Rails,深入集成了对象关系映射工具。因此,应用程序开发人员很容易对他们的数据库进行许多模式更改,这使得数据库管理员很难管理模式的发展。在 Rails 应用程序中,这些被称为 "数据库迁移",我们已经听到了数据库管理员的第一手描述,他们要么必须处理 "每周几十次迁移",要么制定对冲策略以确保未来的迁移不会带来痛苦。MySQL 提供了自由的模式演化语义,并使用完整的表副本实现了大多数更改,这加剧了这种情况(The situation is exacerbated with MySQL offering liberal schema evolution semantics and implementing most changes using a full table copy)。由于频繁的 DDL 是一个实用的现实,我们已经实现了一个高效的在线 DDL 实现,它 (a) 基于每页版本化模式,并根据需要使用模式历史对单个页面进行解码,以及 (b) 使用写时修改原语(modify-on-write primitive)将单个页面缓慢升级到最新的模式。
我们的客户对云原生数据库有着苛刻的期望,这可能与我们如何运营集群以及我们修补服务器的频率相冲突。由于我们的客户主要将 Aurora 作为支持生产应用程序的 OLTP 服务,任何中断都可能造成创伤。因此,我们的许多客户对我们的数据库软件更新的容忍度非常低,即使这相当于每 6 周左右计划停机 30 秒。因此,我们最近发布了一个新的零停机补丁(Zero-Downtime Patch ,ZDP)功能,允许我们在不影响运行中数据库连接的情况下为客户打补丁。
如图 12 所示,ZDP 的工作方式是寻找一个没有活动事务的瞬间,并在那个瞬间将应用程序状态假脱机到本地临时存储,修补引擎,然后重新加载应用程序状态。在此过程中,用户会话保持活跃,并且忘记了引擎在幕后发生了变化。
在这一节中,我们将讨论其他的贡献,以及它们与 Aurora 所采取的方法之间的关系。
将存储与计算分离。虽然传统系统通常被构建为庞大的守护进程,但是最近有一些关于数据库的工作,这些工作将内核分解成不同的组件。例如,Deuteronomy 就是这样一个系统,它将提供并发控制和恢复的事务组件(Transaction Component,TC)与在 LLAMA 之上提供访问方法的数据组件(Data Component,DC)分开,LLAMA 是一个无闩锁(latch-free)的日志结构缓存和存储管理器。Sinfonia 和 Hyder 是在横向扩展服务上抽象事务访问方法的系统,数据库系统可以使用这些抽象来实现。Yesqell 系统实现了多版本分布式平衡树,并将并发控制与查询处理器分开。Aurora 将存储分离到比 Deuteronomy、Hyder、Sinfonia 和 Yesquel 更低的级别。在 Aurora 中,查询处理、事务、并发、缓冲区缓存和访问方法与作为横向扩展服务实现的日志记录、存储和恢复是分离的。
分布式系统。面对分区时正确性和可用性之间的权衡早已为人所知,其主要结果是在面对网络分区时单拷贝可串行化(one-copy serializability)是不可能的。最近,在有的文章中证明的 Brewer 的 CAP 定理指出,在存在网络分区的情况下,高可用性系统不能提供 "强" 一致性保证。这些结果以及我们在云级别的(cloud-scale)复杂的和相关的故障方面的经验激发了我们的一致性目标,即使存在由 AZ 故障导致的分区。
Bailis 等人研究了提供高可用性事务(Highly Available Transactions,HATs)的问题,这种事务既不会在分区期间不可用,也不会导致高网络延迟。它们表明,可序列化性、快照隔离和可重复读取隔离不兼容 HAT,而大多数其他隔离级别可通过高可用性实现。Aurora 提供了所有这些隔离级别,方法是做出一个简化的假设,即在任何时候都只有一个写者使用从单个有序域分配的 LSN 生成日志更新。
谷歌的 Spanner 提供外部一致的读写,以及在一个时间戳上跨数据库的全局一致的读取。这些特性使 Spanner 能够支持一致的备份、一致的分布式查询处理和原子模式更新,所有这些都是在全球范围内进行的,甚至在正在进行的事务中也是如此。正如 Bailis 所解释的,Spanner 高度专用于谷歌的读重(读多写少)工作负载,并依赖于读/写事务的两阶段提交和两阶段锁定。
并发控制。较弱的一致性(PACELC)和隔离模型在分布式数据库中是众所周知的,并导致了乐观的复制技术以及最终一致性的系统。集中式系统中的其他方法包括基于锁定的经典悲观方案、像 Hekaton 中的多版本并发控制这样的乐观方案、像 HyPer 和 Deuteronomy 中的 VoltDB 和时间戳排序这样的分片方法。 Aurora 的存储服务为数据库引擎提供了持久保存的本地磁盘的抽象,并允许引擎确定隔离和并发控制。
日志结构存储。日志结构存储系统是由 LFS 在 1992 年提出的。最近,Deuteronomy 和相关的 LLAMA 和 Bw-Tree 在存储引擎堆栈中以多种方式使用日志结构技术,并且像 Aurora 一样,通过写入增量而不是整页来减少写入放大。Deuteronomy和 Aurora 都实现了纯粹的重做日志,并跟踪最高稳定的 LSN 来确认提交。
恢复。虽然传统数据库依赖于基于 ARIES 的恢复协议,但最近的一些系统选择了其他性能途径。例如,Hekaton 和 VoltDB 在崩溃后使用某种形式的更新日志重建它们的内存状态。像 Sinfonia 这样的系统通过使用像进程对和状态机复制这样的技术来避免恢复。Graefe 描述了一种具有每页日志记录链的系统,该系统支持按需逐页重做,从而可以快速恢复。像 Aurora 一样,Deuteronomy 不需要重做恢复。这是因为 Deuteronomy 延迟了事务,因此只有提交的更新才会发布到持久存储中。因此,与 Aurora 不同,事务的规模可以在 Deuteronomy 中得到约束。
我们将 Aurora 设计为一个高吞吐量的 OLTP 数据库,它在云规模的环境中既不牺牲可用性也不牺牲持久性。最大的想法是摆脱传统数据库的单一架构,将存储与计算分离开来。特别是,我们将数据库内核的下半部分转移到一个独立的、可扩展的分布式服务中,该服务管理日志记录和存储。由于所有的输入/输出都是通过网络编写的,我们现在的基本限制是网络。因此,我们需要专注于减轻网络负担和提高吞吐量的技术。我们依靠仲裁模型来处理大规模云环境中发生的复杂和相关联的故障,并避免异常性能损失,依靠日志处理来减少总的输入/输出负担,以及异步一致性来消除分布式存储中昂贵的多阶段同步协议、离线崩溃恢复和检查点。我们的方法简化了架构,降低了复杂性,易于扩展,并为未来的发展奠定了基础。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。