Yangming's Blog

beware the barrenness of a busy life

读Paper——Aurora

14 May 2019 » Paper

简介

根据业务进行弹性伸缩(resilience)是选择公有云的重要原因;而在传统的数据库中,单机数据库不方便扩展;基于物理机的分布式方案的扩展粒度比较大,容易有资源浪费。因此,在公有云环境下,将存储和计算分离,可以分别针对计算密集型应用和大数据量应用进行scale-up和scale-out,更加灵活方便。

在亚马逊提出的Aurora云数据库架构中,将传统的数据库内核的计算模块和存储模块解耦为两个服务,其通过网络实现数据交互。因此,网络代价就成为了Aurora的关键优化点。在论文中,其针对网络代价进行的大量优化。

对于一个数据库,重放所有的操作记录即可得到任何时间点的数据;因此我们可以将数据库的数据看做是操作记录的日志,即为存储模块。另外数据读写的时候的命令与调度,即为计算模块;在Aurora中,上层数据库计算节点保留了数据库内核的计算模块(查询处理,事务,锁,缓存管理,访问方法和undo日志管理等);将事务日志,即redo日志,下沉到存储模块中,并且只向存储模块中写日志记录,从而大大减少网络IO;具体的数据通过redo日志并发异步恢复;从而提高整体的事务吞吐量。在论文中,主要从三个方面介绍了Aurora的架构,本文会一一介绍:

  1. 如何保证底层存储模块的数据有效性,即读到的是最新的一致性数据,写数据的有序且不丢失的;
  2. redo日志下沉后的存储模块的数据生成机制
  3. 如何保证数据库分布式事务的ACID基本特性。

存储模块的数据有效性

在分布式环境下,冗余存储是解决高可用与高并发的基本方法。但是多个副本上的数据一致性保证又是一个大问题。在Aurora中,采用quorum+gossip的方式,保证数据的读写一致性。

quorum协议

在多个副本上的数据可能不同,每个副本在读写的是有一份投票权;假设有V个副本,读取需要获得$V_r$的票,写入需要获得$V_w$的票;为了保证读写的一致性,需要保证下面两个条件:

  1. $V_r + V_w > V$:读与写的集合有交集,确保能读到最新的数据。
  2. $V_w > V/2$:写与写的集合有交集,确保写不会覆盖其他的写。

基于以上的条件,最小$V=3,V_r=2,V_w=2$即可满足条件,但是考虑到公有云的可用区分隔,三副本的容错性低;因此,Aurora采用了$V=6,V_r=3,V_w=4$的设计,具体部署在三个AZ中,每个AZ有2个副本。这样能够保证一个AZ+一个节点挂掉时的读可用性与一个AZ挂掉时的写可用性。

当数据写的时候,只要保证将其写入到$V_w$个副本中,即可返回写入成功;其他未写入的副本上缺失的数据通过gossip做到最终一致性。

分段存储

数据整体上COPY了六份,为了方便管理并且提高整体的并发吞吐,Aurora将数据卷(Volume)分成了若干10GB的段(Segment),同一个Segment的6份数据称为一个PG(Protect Group)。

目前的Volume存储上限是64TB

这样,当发生AZ+1的故障时,服务可用按照段为单位进行恢复,恢复时间短;并且从Amazon的经验中,在这个短时间内没有发生过一个AZ+2的故障,这确保了Quorum协议大概率是有效的[个人理解,可能不准确,原文如下]。

A 10GB segment can be repaired in 10 seconds on a 10Gbps network link. We would need to see two such failures in the same 10 second window plus a failure of an AZ not containing either of these two independent failures to lose quorum. At our observed failure rates, that’s sufficiently unlikely, even for the number of databases we manage for our customers.

因此,分段存储能够消除大量的故障恢复工作;并且在进行数据迁移时,也变得很灵活;另外,当升级上层软件的时候,可以安装段进行灰度发布。

存储模块的数据回放机制

事务日志,又叫redolog,下放到存储层是计算与存储分离的关键。在数据库中,我们可以认为日志就是数据库,因为基于日志可以恢复出任何时间点的数据。

写放大问题

在传统数据库架构中,数据更改需要进行多次IO。以MySQL为例,首先需要redo落盘;然后脏页异步刷盘;另外为了防止部分写,刷数据页时采用double-write的方式;还有元数据FRM文件以及可能打开的binlog等写盘。

并且部分磁盘IO还需要同步等待,可能导致系统夯住。

日志下沉并重放数据

在Aurora中,上层只写redo记录,数据库层不在刷写数据页。redo日志下推到存储层后,异步的生成数据页。当读取数据页时,如果下层还未合成好(如何判断数据是否合成好,见下午的计算模块的事务),才会等待下层的数据合成。上层的数据只看做是下层日志重放的缓存(cache,而不是buffer)。

相比于原来需要设置间歇性的执行CHECKPOINT来刷新数据页;Aurora中的数据合成是同步推进的,能够减少整体的性能抖动。并且,原来的CHECKPOINT的间隔取决于整个库的redo大小,而这里的CHECKPOINT只和某些频繁修改的段相关。

另外,由于将尽可能多的数据操作移到后台处理,Aurora设计上能够减少前台用户写的响应时间。如下图是Aurora的存储节点上的日志写入与数据回放流程:

image-20190516154245961

  1. 首先将redo日志写入内存中的队列
  2. 日志持久化到磁盘的更新队列中后,向上层回ack(注意这里不是commit ok)。
  3. 将日志按顺序重新组织,发现日志中的gap。
  4. 基于gossip协议,从其他段中获得gap中的日志。
  5. 基于持久化的redo记录,合成数据页。
  6. 周期性地备份redo记录与数据页。
  7. 周期性地清理数据页中的过期版本数据。
  8. 周期性地对数据页进行crc校验。

其中只有1与2两步是串行同步的,其他的都是并行异步的处理。

redo异步恢复下的事务一致性

上节介绍了存储模块中的数据回放,而在计算模块中,为确保数据读取的正确性,维护了数据库的持久化状态,运行时状态和复制状态,从而确保在非2pc的分布式事务模型下的数据一致性。

数据一致性状态

  • VCL(Volume Complete LSN):每个segment有6个副本,构成一个PG;在PG中的各个seg中的redo记录可能不连续;那么在该PG的所有副本中都连续的部分的最大LSN就是VCL。该点表示该segment的数据在VCL之前的都已经一致了。
  • CPL(Consistence point LSN):每个mtr的最后一条日志记录是CPL。
  • VDL(Volume Durable LSN):MySQL的持久化的最小单位是mtr;在数据恢复的时候,VDL就是最大的CPL。而在正常运行的时候,由于1.2步是同步的,那么可以实时的维护。

首先上层写redo日志时,每个日志记录都有一个全局单调递增的LSN(Log Sequence Number)。由上面的第1,2步得知,数据库层与存储层始终保持lsn状态的同步,因此在在存储服务中,Aurora维护了一些一致性点和持久化点。那么数据库运行时,读取的最新数据可以直接由维护的运行时状态得知,不需要quorum协议来判断了。然而在数据库恢复时,由于运行时状态丢失,仍然需要quorum协议来读取。

基本操作

介绍了Aurora与存储层的基本操作。

write

  • VCL更新:由于是按照quorum协议的分段写入,每个段中的日志记录可能不是连续的;当前segment对应的PG中,公共连续的最大LSN为SCL,其作为当前PG基于gossip协议寻找缺失记录的依据,如下:

    每个记录内部维护了一个backlink后向指针,其指向前一个日志记录。每个segment从后向前(即seg最新的LSN开始按照backlink向前追溯,直到碰到SCL结束)逆向寻找空缺的日志记录并按照gossip协议补全缺失的日志记录,同时更新当前PG的SCL,进而更新VCL

  • VDL更新:计算层是按照LSN的顺序向存储中写入且只写redo日志;随着存储层直接从计算层获取或者基于gossip协议获取的记录不断增加,存储层将redo日志进行持久化,并更新VDL;注意这里持久化的日志一定要保证是连续的,因此VDL永远比VCL小;并且下文当VDL更新后,当大于某个commit lsn时,则会想上层回复commit ok。

    为了防止数据库层分配的LSN速度远远快于存储层的VDL推进的速度,引起过多的redo日志刷盘等待;因此设置了一个阈值LAL(LSN Allocation Limit)=10million,当LSN>VDL+LAL时,上层需要等待存储层推进。

commit

Aurora中的事务提交同样是异步的。当client提交一个事务时,用户线程将该提交请求排队,并记录下commit lsn,然后就处理其他任务了。随后,随着VDL推进,当VDL大于该commit lsn时,有一个专用的线程向用户返回commit ok。

因此,在这里Userthread在提交的时候,不会等待,提高了事务吞吐量。

read

当redo记录持久化后(最后一条就是VDL),同时另一个线程不断将持久化的记录重放来构造数据,假设当前segment重放到LSN_k;

当发起读取的时候,卷的VDL就是当前的ReadPoint(读一致点)。存储模块从各个segment中找到$SCL \ge ReadPoint$的存储节点(即,找到日志连续且最大连续LSN>=读一致点的段),进而从该段中读取数据。当读取时,如果LSN_k<ReadPoint,相应的数据页还未合成完毕,那么会等待回放日志。

在一个PG中,根据自身收到的read请求,可以维护一个PGMRPL(最小读一致性点:当前活跃的最老读请求);因此,可以将这之前的数据持久化到磁盘中,并回收这之前的redo日志。

将底层数据读取后的并发控制(事务隔离,页面可见性等)还是在上层引擎中实现,基于回滚段与读取的数据页进行处理。

replica

在计算存储分离架构下,添加read-only的备机几乎是零成本,在Aurora中最多基于共享存储添加15个备机。

主节点写redo日志的同时也会异步的向备机发送redo日志,备机对接收到的redo日志重放,但如果重放时,相应的数据页不在备机的bufferpool中,那么就会丢弃该日志。另外,如果数据页在缓存中,那么按照下面两个规则进行重放:

  1. 回放日志的LSN应小于等于VDL,当大于VDL时,将redo日志缓存等待VDL推进后,再重放。
  2. 回放日志时需要以MTR为单位,确保能看到一致性的数据视图。

故障恢复

基于ARIES的数据库恢复算法,一般需要周期性的记录CHECKPOINT;然后从CHECKPOINT开始重放redo日志,然后按照undo日志,回滚事务。因此,在传统rdbms中,需要在CHECKPOINT周期的性能抖动与恢复时间之间做权衡。而在Aurora中不需要这种权衡,因为日志恢复和数据库是同步进行的。

在重启恢复时,由于丢失了内存中的信息。存储服务会按照PG为单位根据存储的日志信息进行故障恢复,保证数据库上层看到的是全局一致的存储视图。

首先,确定当前PG中的公共连续的最大redo日志的lsn为VCL;将大于VCL的redo记录进行truncate。

另外,每个mtr的最后一条记录是CPL;所有CPL的最大值为VDL,同样将所有大于VDL的redo记录truncate。一般$VDL \le VCL$。

在日志下沉章节的图中,可以知道1.2两步是会将接受到的记录持久化,但是这里持久化的并不一定连续;那么恢复的时候首先找到连续可用的VCL进行truncate;然后再计算VDL,进行truncate;如下图例子:

image-20190605103026176

在Aurora的恢复中,为了方式恢复失败,恢复前将需要truncate的LSN区间进行持久化;为了防止恢复失败并再次恢复时的歧义,持久化的truncate区间会带上epoch信息。

存储模块将无效的redo记录truncate后,上层数据库模块开始收集未完成的事务后,即可提供服务;undo的恢复可以在线进行。

综述

Aurora基于社区版的InnoDB改进,主要修改在于InnoDB的磁盘读写操作。redo日志划分为若干段。存储服务层向上提供和本地读取一样的接口。

在公有云上,Aurora的部署架构如下:

  1. 每个数据库实例部署了一个HostManager,负责故障切换等操作。
  2. 所有实例都在一个物理区内,但是是跨AZ部署;同样的相应的存储服务也在同一个物理区中。
  3. 安全起见,按照3个VPC隔离部署。RDS VPC负责管控平台的通信;Customer VPC负责客户的通信。Storage VPC负责数据库层与存储服务的通信。
  4. 存储服务有一组跨AZ的EC2集群构成,支持多用户存储,且从卷中读写与备份恢复等功能。
  5. 存储节点将数据备份到S3上,必要时从S3上还原数据。
  6. 存储服务的管控信息通过DynamoDB来持久化存储,其进行了高可用的保证。