作者介绍
朱阅岸,中国人民大学博士,现供职于腾讯云数据库团队。研究方向主要为数据库系统理论与实现、新硬件平台下的数据库系统以及TP+AP型混合系统。
编者按
Aurora作为AWS云上的关系数据库,完美契合了企业级数据库系统对高可用性、性能和扩展性、云服务托管的需求。在本月中旬刚刚结束的AWS re:Invent 2017大会与数据库顶级会议SIGMOD上,Amazon首度公开了Aurora的技术细节,本文系作者结合自身理解写作而成,权当抛砖引玉。
Aurora是Amazon为云计算而专门定制的一款关系型数据库。其目标主要是最小化网络IO,提升系统的可扩展性与可用性。Aurora的设计哲学是log is database,对数据的更改只写日志,也即write-once。Aurora系统设计人员认为传统的数据库不论如何扩展都在复制整个系统栈,在不同的层面做耦合;为了更好地适应云计算,他们认为应该将数据库系统这个“盒子”打开,在不同的层面进行扩展。Aurora将恢复子系统委托给底层可靠的存储系统,依赖这个来保障系统服务层级(Service Level Agreement, SLA)。
针对Amazon云生态环境做了相应优化以后,在某些工作负载下,Aurora的性能可以比MySQL5.7高出10倍以上。下面我们从不同方面深入解读Aurora的设计理念。
一、前言
关系数据库系统中,处理事务的过程通常被视为一种分层的行为。系统在顶层对SQL语句进行解析,然后将得到的语法树传递给查询优化器层。查询优化器利用启发式规则和统计信息为每个关系操作符选取最优的策略。这个阶段产生的物理执行计划与逻辑存储层交互,完成相应的操作。
在本文中,将事务处理引擎简化为两层模型:查询解析、查询执行以及查询优化视为查询处理引擎(Process Engine,PE);逻辑层存储和物理层存储统称存储引擎(Storage Engine,SE)。这对应MySQL可插拔存储的两层架构。
定义数据库服务器集群的架构决策的关键点在于集群共享发生的程度,它定义协调动作发生在什么层以及哪个层(PE和SE)将被复制或者共享。这不仅确定了系统在可扩展性和灵活性上的权衡,而且关系到每一种架构在现成的数据库服务器上的适用性。以下是四个具有代表性的架构:
图1. 不同的数据库系统架构
Shared Disk Failover (SDF)
集群管理软件确保DBMS服务器只在连接到共享磁盘上的一个节点上运行,节点之间通常使用存储区域网络(SAN)互联。如果当前活动节点崩溃,强制卸载该服务,并在不同的节点上启动服务器。标准日志恢复过程可确保在磁盘上的数据的一致性,因此SDF适用任何DBMS。这种方法的一个变体可以在没有物理共享磁盘的情况下通过使用诸如DRBD的卷复制器达到相同效果。否则,磁盘冗余由RAID配置提供。
这种架构专门为解决服务器崩溃问题而设计,它通常部署在一个简单的两服务器配置。如在图1(b)所示,协调仅发生在DBMS外部,确保仅有一个服务器加载共享卷。如果文件安装在多个节点上,它甚至不能分发只读负载到备用节点,因为缓存一致性的问题会出现。由于复制是在裸盘上执行,在面对更新的时候,无论是PE或SE都不需要被复制,该架构在系统崩溃的时候会导致短暂的不可用情况发生。
Shared Disk Parallel (SDP)
允许多个节点同时访问共享存储需要保证缓存一致。具体地说,每个数据块的所有权随着时间的变化而不同,特别是当集群应用触发一个写操作。分布式并发控制机制负责将页面所有权移交到对应实例。在这过程中,即使页面变脏,也无需进行I/O操作。读操作是共享操作,每当应用发送读请求,数据页的所有者复制该页面。任何时候数据块的写回操作仅由一个副本进行。
如在图1(c)所示,协调动作在存储引擎层内进行。这种体系结构的一个例子是Oracle的RAC,这是基于Oracle并行服务器(OPS)和Cache Fusion技术。
这种架构主要是针对服务器可用的CPU和内存带宽上进行扩展。它提供与SDF等同的容错能力,因为大多数服务堆栈在面对更新事务的时候仍然没有被复制。
Shared Nothing Active(SNA)
通过完全隔离后端服务器,中间件层拦截所有客户机请求并将其转发到独立副本。因为只读请求在可用节点之间得到平衡从而达到系统可扩展。因此,只有更新事务需要积极地在所有副本进行复制。控制器扮演包装器的角色。对于请求者而言,它充当服务器,提供相同的客户端接口。对于原始的服务器而言,该中间层作为客户端。群集节点之间没有直接的沟通,因为协调发生在服务器之外,如图1(d)所示。一个流行的实现是由Sequoia提供的,之前叫做C-JDBC,可以移植到多种后端服务器。
可扩展性上的主要缺点就是更新语句必须是完全确定的,并且需要小心调度以避免冲突从而导致非确定的执行结果和数据库不一致。 实际上,这通常意味着不允许并发执行更新事务。这个架构的目标主要还是增强服务器在面对大多以读为主的工作负载能力。通过完全隔离后端服务器,在处理更新事务的时候,它在所有数据库系统的软件层重复执行,从而容许PE和SE的故障情况。事实上,可移植的实现,例如Sequoia,甚至支持多样的DBMS。它甚至可以在崩溃的状态上进行投票,以掩盖错误的备份。
Shared Nothing Certification-Based (SNCB)
在无共享集群,可以使用基于认证协议避免主动复制更新事务。每个事务直接在副本上执行,而没有任何先验协调。因此,事务仅需要根据本地的并发控制协议在本地进行同步。仅在提交之前,协调的过程才开始。这个时候,发起协调的副本采用全序组通信原语广播更新。这将导致所有节点采用完全相同的更新序列,然后通过测试可能的冲突获取认证。PostgreSQL-R,Group Replication,Garela等属于这类架构的范畴。由于协调发生在PE与SE之间如图1(e),它能够随着更新密集型工作负载的扩展而执行更细粒度的同步。这方法能够接受存储引擎和磁盘层的物理损坏。
Aurora给人眼前一亮的是它的架构。其体系架构更类似SDP,但是它将更新局限在一个DB服务器上,避免了分布式并发控制协议。Aurora设计者们认为,传统的数据库实现可扩展不管是做Sharding、还是分布式、或者共享存储(Oracle RAC),本质上都是在数据库的不同层面耦合(SNA在应用层,分布式是在SQL层,SDP是在缓存层),扩展后的每个实例的程序栈仍然是原来的多层结构。Aurora认为从成本、部署灵活性及可用性等因素考虑,应该考虑把数据库的各层打开,然后在每个层单独做扩展。传统的数据库系统,例如MySQL、PostgreSQL以及Oracle,将所有的功能模块封装成一个整体,而Aurora则是将数据库的缓冲区管理、恢复子系统从这个整体剥离出来,单独定制扩展。
Amazon声称Aurora完全兼容MySQL,具备商业数据库的性能与稳定以及开源项目的低成本和易用性。下面详细介绍Aurora的设计理念。
二、Aurora系统设计
相比传统的数据库系统架构,Aurora具有以下三个明显的优点。
首先,在跨数据中心环境将存储作为一个独立的、具有容错以及自修复能力的服务模块,使得数据库系统免受性能抖动和存储或者网络故障的干扰。Aurora设计人员观察到持久层的故障可以认为是系统长时间不可用事件,而系统不可用事件又可以建模为长时间的系统性能抖动。一个设计良好的系统可以无差别地处理这些问题。也就是,Aurora通过底层可靠的存储系统保证数据库系统的服务层级(Service Level Agreement, SLA)。
第二,Aurora的设计理念是日志即数据。通过只将重做日志记录写入存储层,系统可以将网络的IOPS减少一个数据量级。一旦移除了网络I/O瓶颈,在MySQL的代码基础上可以针对各种资源竞争进行大量优化,获得大幅的性能提升。
第三,将数据库系统中一度被认为最复杂与关键的功能(例如重做、备份等)委托给底层的分布式存储。存储系统以异步方式持续在后台并行构造最新版本的数据。这使得Aurora可以达到即时恢复的效果。
存储与计算分离,对于Aurora来说,并不是一个选择题。在Amazon生态下,存储本来就是和计算分离的,从逻辑上看,可以认为系统有一个巨大的共享存储(或许使用的共享存储就是计算节点的本地存储)。Aurora能够做的事情,就是尽可能减少计算与存储之间的带宽需求,这是整个架构的关键所在。
Aurora通过只传输重做日志记录以消除不必要的IO操作,降低成本,并确保资源可服务于读/写流量。与传统的数据库引擎不同,Amazon Aurora不会将修改后的数据库页面推送到存储层,进一步节省了IO消耗。
在开始介绍实现细节之前,我们先给出如下术语定义。
下面具体介绍Aurora事务的正常读写、提交以及恢复过程。
1、Aurora写流程
图2. Aurora I/O流程
如图2所示,系统将数据库文件切分成大小均等的存储块(一个存储块的大小为10GB),这些存储块分布在不同的存储设备上。每个存储块都有专属的redo日志。更新操作只写日志而不写数据页。在适当的时机底层的存储将日志合并成数据页。也就是,计算与存储之间只传递日志,而不传递脏页,页面的合并由存储端来完成(不要把存储端看做是一组单纯的硬盘,它也包含了计算节点,可以把他看成是一组磁盘服务器,类似Oracle ExaData),注意到Aurora的存储格式是基于日志结构的,所以这是一个整体的设计;系统对每个数据块复制六次,分散在三个不同的可用区域AZs。Aurora认为写操作已经持久化,仅当数据(redo日志)至少写入六个备份数据的其中四份。Aurora不支持跨region复制。
系统设计人员采用如此数据副本放置策略的原因主要如下:
在大规模的云计算环境下,底层的磁盘、数据节点以及网络的故障持续发生。每种故障有不同的持续时间以及波及范围,例如,可能节点网络的暂时不可用,重新启动带来的短暂的停机,或磁盘、节点、机架、网络交换机等的永久性故障,甚至是整个数据中心的不可用。在备份系统里面一个常用的容错方法是多数派投票协议。复制的数据项的每个副本都关联一个投票,读写分别对应副本数Vr以及Vw。
为了满足一致性,必须遵循以下两条规则:
1)为了读到最新的数据,必须满足Vr + Vw > V。
这条法则确保写操作涉及的副本与读操作涉及的副本有交集,读操作可以读到最新版本的数据。
2)为了避免冲突,感知最新的写入操作,写操作涉及的副本数Vw 必须满足 Vw > V/2。
通常的做法是容忍一个节点不能正常工作,设置V=3,读多数派为Vr = 2,写多数派为Vw = 2。Aurora的设计人员认为2/3的多数派是不够的。他们将副本数提升为6个。每个AZ上两个数据副本。这样子的话,为了使得读写条件成立,那么Vr = 3,Vw = 4。这样的配置使得系统可以容忍:
某个机房垮掉,外加一个副本所在的机器不可用(AZ+1)而不会影响读;
损失两个副本,即使这个两个副本位于同一个机房,不会影响写。
那这样子的配置是否足够健壮去容错呢?
Aurora的设计者认为很难去降低两个不相关事件的故障概率(两个副本不可用),于是转而限制平均修复时间,使得在平均修复时间内发生故障的概率几乎不可能。他们将数据库分片限制在10GB大小,在万兆以太网下修复的时间低于10s,而在这个时间段内,一个机房不可用外加一个数据副本不可用的概率几乎为0。这就是他们选择10GB大小的数据分片以及每个分片需要复制6个副本的原因。
在介绍Aurora的更新事务流程之前,我们先看看传统的数据库系统的更新步骤。在类似MySQL的系统中需要将脏数据页写回堆文件或者b树等对象中(延迟写)。此外,还需要将WAL日志写入持久层存储。通过重放WAL日志可以产生数据页修改后镜像。实际上,需要写回的数据远不止这些。Aurora设计人员给出了一个MySQL同步镜像的例子。在这个例子中,写回的数据除了数据页、redo日志还包括binlog、避免数据页损坏的双写文件以及元数据文件。写回这些数据会带来的巨大网络I/O, 还有就是同步这些文件带来的延迟太大了。
Aurora另辟蹊径,采用并行写多个副本保证可靠性,以及利用“log is database”的思想减少传输的数据。在Aurora中,一个更新事务的完整操作流程如下:
同时写多个副本,执行更新操作,但不修改缓冲区中的数据页,仅仅是构造对应的redo日志。
存储节点收到主实例发送的redo日志,负责持久化工作。
具体来讲存储节点执行如下工作:
存储节点将主实例发送的redo日志放入内存队列,然后将日志从队列移出,持久化到磁盘(这个过程是批量操作)。
存储节点给主节点发送一个ACK,告诉主节点数据持久化过程已经完成。这个步骤完成以后,主实例与存储节点的交互就已经完成。从Aurora的角度看来,这两个步骤是执行路径上的关键路径,影响系统的吞吐量以及相应时间。此后的步骤与主实例的通信可以独立,异步的方式进行。
一旦存储节点生成日志文件,它就立刻开始整理这些日志记录以便发现它遗漏了某些日志记录。
运行点对点的Gossip协议,将遗漏的日志记录补上。(通过Gossip协议,它们可以知道集群中有哪些节点,以及这些节点的状态如何?每一条Gossip消息上都有一个版本号,节点可以对接收到的消息进行版本比对,确保二者得到的信息相同)。这个阶段过后,每个节点上的数据是相同的拷贝。
将日志记录合并,生成最新的版本的数据,转换成数据库需要的数据块。
以很高的频率将生成的数据块备份到S3。这个Point-in-time快照技术保证故障恢复的时候可以将数据库恢复到之前特定时间点的一致性状态。通常有两种方法保证Point-in-time快照捕捉到最近的更新。一种方法是指针重定位。当最新版本的Point-in-time快照被创建的时候,它维护一个指针指向原来的快照。另外一种方法是增量维护,只是拷贝被更改的数据。
运行垃圾回收机制,清理过时的数据块与日志文件。
定期扫描数据块,进行校验。如果发现损坏数据块,与相邻节点进行通信获取完好的数据块。这是Aurora实现可自主修复损坏数据块的关键技术。
以上8个步骤具体见图3。
图3 主实例与存储节点的交互
当主实例收到4个以上的日志持久化ACK以后就完成事务提交,可以将执行结果返回给客户端。
从Aurora的写步骤看来,系统只需要等待写入存储节点的日志返回ACK即可,这是写操作的唯一需要同步执行的地方。系统将大部分计算下推到底层的存储层。因为只需写入redo日志,Aurora也可以极大地减少网络IO。从这个角度看来,评价Aurora是一款优秀的专为云计算而设计的DBMS,一点都不为过。
2、Aurora读操作
虽然Aurora对写操作进行了优化,仅仅写入delta更新(redo日志);但是系统的读操作还是以块为单位。如同传统的数据库系统的做法,读事务首先在缓冲区里查找所需的数据块。如果存在,直接读取即可;否则将请求下发到存储系统。如果系统缓冲区已满,那么需要淘汰一个页面来装下新读入的数据页。在已有的系统中,如果被淘汰的数据页是脏页,那么需要写回磁盘。这可以确保随后的事务都可以读到最新版本的数据。
上面我们已经介绍了,Aurora并没有将数据页写回存储系统,只是简单地将相应内存空间标记为free。但是,Aurora满足类似的条件:缓冲区里的数据都是最新的。这个条件的保证通过将缓冲区里页面LSN(关联该页面最新修改操作的日志记录的LSN)大于文件持久化位点VDL的数据页置换掉。
这个协议确保了:
1)页面的所有改动都已经持久化到日志
2)在缓冲没有该数据页的情况下,可以构造出当前VDL的页面。
随着系统提交事务的不断确定,VDL会不断往前推移。最终VDL会大于修改页面的PageLSN。此时,为了让读操作能够读到最新的数据,系统有两种选择:
其一是,重放操作组件将LSN ≤ VDL的日志记录应用到相应的数据页产生最新版本的数据;
其二是,在读操作的时候将旧版本数据页与delta更新(日志记录)进行合并(amazon采用何种方式将缓冲区中的页面变成最新版本的数据并没有说明,但是Log structure结构的数据组织方式决定了Aurora只能采用上述两种处理方式)。
系统只有在故障恢复重启的时候会采用多数派读的方式来确定系统的VDL。正常情况下,读操作并没有采用多数派读的方法。Aurora给读操作指派一个读位点(read point),代表着读请求产生时刻的VDL。系统维护着对应存储节点的SCL,知道哪些节点可以满足当前的读操作。
3、事务提交
Aurora采用的是异步成组提交技术。在传统的数据库中,例如MySQL,为了减少磁盘IO采用成组提交技术。第一个写日志缓冲区的线程需要等待预先设定的时间,然后再执行磁盘IO操作;或者等到日志缓冲区页写满以后再执行IO操作。这样子做的结果是第一个写日志缓冲区的线程需要挂起等待,耗费时间。这是一个同步操作,在持久层存储的ACK返回之前不能进行其它工作。
在Aurora中,写操作不仅仅依赖Linux文件系统,还需要跟网络交互。第一个写日志缓冲区的线程可以马上开始执行IO操作。每个提交事务都无需等待。线程将事务移动到提交列表,写下该事务的commit LSN,转而执行其他工作。在某个时刻,后台进程负责将这些日志记录收集起来,批量发送到存储节点。当主实例收到对应某批次的日志记录的4个ACK,VDL向前推移。系统有一个专门的线程不断检查提交事务队列中commit LSN ≤ VDL的事务,然后回复客户端。这等价于WAL协议。
在图3中,提交队列里面有3个挂起的提交请求,分别是Pending commit group1,Pending commit group2,以及Pending commit group3。主实例收到针对Pending group commit1的4个以上的日志持久化ACK以后,将系统VDL前移至22,第一组的状态从pending变成committed。此时后台线程检查提交队列,然后成批提交LSN小于等于22的事务T1,T2,T3。
注意,即使后面Pending commit group3先于Pending commit group2收集4个以上的存储节点返回的持久化ACK,那么也不能移动数据库持久化位点。因为,这个数据库持久化位点是Aurora崩溃恢复以后决定开始重做的位点。跳过前面的成组提交的LSN会导致数据库丢失某些数据。在系统崩溃恢复的时候,系统检索最新的数据快照与相应的日志记录(其LSN大于数据库持久化位点),即可将数据库恢复到最新的一致性状态。
图4. Aurora成组提交细节
4、恢复
大多数据库系统的恢复协议采用类ARIES算法,依赖WAL日志将数据库系统向前滚动到最新状态。系统周期性地建立检查点,将脏页写回磁盘,同时将检查点记录写入日志。系统重启的时候,页面遇到丢失提交数据或者包含未提交数据。此时,恢复子系统将最新检查点以后的redo日志进行重放,然后利用undo日志撤销未提交事务的修改。通过这两个步骤就可以将数据库恢复到最新一致性状态。灾难恢复是一个很昂贵的操作,增加系统建立检查点的频率可以减少恢复时间。但是,建立检查点会干扰正常执行的事务。Aurora并不需要做这样子折中。
Aurora将恢复子系统的功能完全从事务的执行路径上剥离出来,交给下面的存储层。存储节点持续以并行、异步、分布式的方式进行redo操作无需在性能与恢复时间上进行权衡。图5给出了传统DBMS与Aurora的恢复方式对比。
Aurora将数据库文件切分成10GB大小的块,每个块都有专属的日志记录。在崩溃恢复的时候,系统利用多数派读,确定运行时的一致性状态。恢复模块首先确定最大VCL(最大的顺序LSN),截断此后日志。进一步,可以将需要重放的日志限制在其LSN ≤ CPL。VDL取最大的CPL,LSN大于VDL的日志记录都可以安全地截断。例如,最大已完成的日志记录的LSN为1007(尚未提交),但是系统的CPL为990,1000, 1100。系统可以确定LSN大于1000的日志记录都可以忽略。确定完重放日志的LSN最大值以后,redo操作就可以并行在不同segment上执行了。
根据官方记载,Aurora完成崩溃恢复所耗费的时间大概是60S~120S左右。如果有其它副本存在,用户可以指定主实例发生故障以后各副本提升为主实例的优先级,不会出现单点故障的情况。对比MySQL,重放操作只是单线程的模式进行,在系统更新负载较大的情况下,MySQL的故障恢复时间通常会比较长。为了及时更新备机上的数据,主实例需要将redo日志发送备机。备机收到redo日志以后,查找数据缓冲区。如果存在对应的数据块,则将根据redo日志描述的操作应用在该数据块上(日志LSN需要满足小于等于VDL)。否则,简单地抛弃对应的日志记录即可。数据页的读入操作要等到对该数据页的请求到来的时候。
图5. 传统DBMS与Aurora的恢复方式对比
三、总结
Aurora的设计让我想起PBXT DBMS。PBXT作为MySQL的可插拔存储引擎,定位在支持高并发场景。它没有采用update-in-place的做法,而是利用append的方式进行更新,这减少了维护高速缓存一致性的开销。同时,最近的工业界与学术界也都大致认同append更新方式对于Flash比较友好。最主要的是PBXT的设计哲学也是“log is database,write-once”,可以看见Aurora的影子。有兴趣的同学可以看看研究下PBXT的源码,或许更能加深对Aurora的理解。
总而言之,Aurora是针对云将MySQL进行深层定制的数据库。Amazon通过紧密集成数据库引擎和基于SSD的虚拟化存储层(专为数据库工作负载而开发),其性能和可用性相较于MySQL有大幅提升。通过降低了存储系统的写入次数使之更好的适应云环境的特点。Aurora在自动拓展存储容量、自动修复数据、服务宕掉或者重启时对缓存持久化的处理方式都是很有创新性的。最后是Aurora在RPO,RTO,兼容性以及扩展性方面的总结。
根据amazon官方文档提供的参数[1],Aurora支持Point-In-Time Recovery将数据库还原到指定时间点,通常能够在60s左右的时间完成故障恢复,不会高于120s。在2017年数据库顶级会议SIGMOD上[2],Amazon公布了Aurora与MySQL5.6&5.7的性能对比:r3.8xlarge实例规格,sysbench压测,Aurora的性能要比MySQL5.6与5.7优10倍以上。
参考文献:
[1] http://docs.aws.amazon.com/zh_cn/AmazonRDS/latest/UserGuide/Aurora.Managing.html
[2] Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721