Amazon新一代云端关系数据库Aurora(上)

达芬奇密码2018-08-15 13:33

在2017年5月芝加哥举办的世界顶级数据库会议SIGMOD/PODS上,作为全球最大的公有云服务提供商,Amazon首次系统的总结了新一代云端关系数据库Aurora的设计实现。Aurora是Amazon在2014 AWS re:Invent大会上推出的一款全新关系数据库,提供商业级的服务可用性和数据可靠性,相比MySQL有5倍的性能提升,并基于RDS 提供自动化运维和管理;

经过2年时间发展,Aurora已经成长为AWS 客户增长最快的云服务之一,包括全球知名的在线游戏网站Expedia、社交游戏公司Zynga都在使用Aurora。Aurora的推出一时引起了国内数据库研究人员的热烈讨论,大家关注的一个焦点就是Aurora是否是基于MySQL推出的一个新的存储引擎?下面我们就根据会议发布的论文,一起走进Aurora。

为什么要有Aurora?

在理解Aurora设计的初衷之前,我们首先来了解一下AWS RDS MySQL的高可用部署架构设计。与我们之前的猜测是一致的,AWS RDS是基于EC2、EBS这样的云基础设施构建的,数据库实例部署在EC2内,数据盘由一组通过镜像实现的两副本EBS提供。
为了实现跨数据中心的高可用,Primary和Replica分别部署在两个可用域内,数据同步采用类似DRBD的方式在操作系统内核通过块设备级别的同步复制实现,所以AWS RDS的Replica平时是不能被读取的,只能用于跨可用域的故障恢复。Replica与Primary是完全对称的,通过内核复制到replica的数据同样存放在一组镜像实现的两副本EBS中。从图中可以看到,这样的一个部署架构,数据库发起的一次写IO需要同步复制5次,其中3次还是串行的,网络延迟对数据库的性能影响非常严重。
MySQL的InnoDB存储引擎遵从WAL协议,所有对数据页的更新都必须首先记录事务日志。此外,MySQL在事务提交时,还会生成binlog;为了保证被修改的数据页在刷新到硬盘的过程中保证原子性,Innodb设计了double-write的机制,每个数据页会在硬盘上写两遍。此外,MySQL还有元数据文件(frm)。在AWS RDS的高可用架构中,所有的这些日志文件、数据文件都要经过网络的传输5次,对网络带宽也是巨大的考验。
这样的一个架构由于不涉及具体数据库内核的改动,满足了AWS发展初期可以快速支持多种类型的关系数据库的需求,但是显然随着规模的增长,这样的架构的缺陷也越来越明显。当我们还在考虑如何优化我们的网络性能和IO路径时,AWS的注意力已经转移到如何来减少数据在网络上的传输,这就有了后来Aurora的架构。

Aurora的系统架构

Aurora与传统关系数据库相比,最大的一个架构上的创新就是将数据和日志的管理交由底层的存储系统来完成,数据库实例只负责向存储系统中写入redo log。由于底层存储节点挂载的是本地硬盘,日志的持久化和数据页的更新并不需要跨网络完成,所以只有redo log需要通过网络传输。
由于MySQL的redo log中包含了对某个数据页的某行记录的更新,通过redo log以及先前的数据页可以构造出更新后的完整页面,所以Aurora选择通过redo log建立起数据库实例和底层存储系统之间的关系。
在Aurora中,数据库实例负责处理SQL查询,事务管理,缓冲池管理,锁管理,权限管理,undo管理,对用户而言,Aurora与MySQL 5.6完全兼容。底层的存储系统负责redo log持久化,数据页的更新和垃圾日志记录的回收,同时底层存储系统会对数据进行定期备份,上传到S3中。底层存储系统的元数据存储在Amazon DynamoDB中,基于Amazon SWF提供的工作流实现对Aurora的自动化管理。

存储系统的设计
Amazon为Aurora实现了一个高可用、高可靠、可扩展、多租户共享的存储系统。

多副本
为了实现数据的可靠性,Aurora在多个可用域内部署了多个数据副本,基于Quorum原则确保多个副本数据的最终一致性。Quorum原则要求V个数据副本,一次读操作必须要读取Vr个数据副本,一次写操作必须要同时写入Vw个数据副本,Vw和Vr需要满足:Vw + Vr > V,且 Vw > V/2。Quorum原则可以确保一份数据不能被同时读写,同时也确保了两个写操作必须串行化,后一个写操作可以基于前一个的结果进行更新。
一般最小的Quorum要求最少3个数据副本,Vr = 2 ,Vw =2,在云环境中,就是3个可用域,每个可用域一个数据副本,一个可用域不可用,不影响数据的读写。但是在真实的场景中,一个可用域不可用的同时,另外一个可用域很有可能也出现故障,为了解决上述问题,Aurora采用了6副本数据,每个可用域2个数据副本,一次写操作需要4个数据副本,一次读操作需要3个数据副本。这样的设计可以实现:
  • 在一个可用域内两个数据副本同时失效,同时另外一个可用域内的一个数据副本失效,不影响整个系统的读;
  • 任意两个数据副本同时失效,不影响系统的写;

分段存储系统设计
任何一个高可用系统设计的前提假设都是在一段时间内,连续两次发生故障的概率足够的低。对于Aurora基于Quorum的多副本设计而言,如果一个AZ的副本失效,在修复过程中,同时再有一个副本失效,则整个系统将不可写;如果在AZ+1的副本失效的同时,又有一个副本再失效,则系统将不可读。我们没有办法去阻止连续故障的发生,但是我们可以通过缩短前一次故障的修复时间,从而降低连续两次故障出现的概率,这就是分段存储设计的思想来源。
Aurora将一个数据库实例的数据卷划分为10G固定大小的存储单元,这样可以确保每个单元数据可以快速的恢复。每个存储单元有6个副本,每个可用域内2个副本,6个副本组成了一个PG(Protection Groups)。物理上,由一组挂载本地SSD的EC2云主机充当存储节点,每个存储节点上分布了很多存储单元。一组PG构成了一个Aurora实例的数据卷,通过分配更多的PG,可以线性扩展数据卷的容量,最大支持64TB。
Segment是存储系统故障恢复的最小单元,之所以选择10G大小,如果太小,可能造成元数据过于庞大,如果太大,又可能造成单个Segment的修复时间过长,经过Aurora测试,10G大小的Segment数据恢复时间在10Gbps的网络传输速度下,只需要10秒时间,这样就确保了存储系统可以在较短的时间内完成故障修复。Segment的元数据由一个DynamoDB来负责存储。
基于Segment和Quorum的设计,Aurora可以通过人工标记一些Segment下线,来完成数据迁移,对于热点均衡、存储节点操作系统升级更新都非常有帮助。

以日志为核心的数据库

事务日志的写入
在MySQL数据库InnoDB存储引擎中,所有数据记录都存储在16K大小的数据页中,所有对行记录的修改操作,都首先必须对数据页进行加锁,然后在内存中完成对数据页行记录修改操作,同时生成redo log和undo log,在事务提交时,确保修改操作对应的redo log持久化到硬盘中,最终被更新的数据页通过异步方式刷新到硬盘中。
redo log确保了数据页更新的持久化,每个redo log record都有一个唯一标识,LSN(log sequence number),标识该记录在redo log文件中的相对位置。为了确保一个更新操作对多个数据页,或者一个数据页内部多条记录的修改原子性,一个事务会被切分成多个Mini-transaction(MTR),MTR是MySQL内部最小执行单元,在Aurora中,MTR的最后一个redo log record对应的LSN,称为CPL(Consistency Point LSN),是redo log中一致点。
在每个MTR提交时,会将MTR生成的redo log 刷新到公共的log buffer中,在MySQL内部,一般log buffer空间满,或者wait 超时,再或者事务提交时,log buffer中的redo log会被刷新到硬盘中。在Aurora中,每个PG都保存了一部分数据页,每个redo log record在被刷新到硬盘之前,会按照redo log record更新的数据页所在的PG,划分成多个batch,然后将batch发送到PG涉及的6个存储节点,只有等到6个节点中的4个的ACK,这个batch内的redo log record才算写入成功。最新写入成功的MTR的最后一个记录对应的LSN,我们称为VDL( Volume Durable LSN ),这个点之前的MTR对数据页的修改,都相当于已经持久化到存储系统中。

存储节点对事务日志的处理
每个存储节点在接收到redo log record batch之后,首先会将其加入到一个内存队列中,然后将redo log record持久化到硬盘后,返回ACK 给写入实例(Primary)。接下来,由于每个存储节点可能保存的batch不完整(由于Quorum 4/6机制),所以需要通过与同一个PG下的其他存储节点进行询问,索要缺失的batch。
Aurora 中存储节点对数据的管理采用了log-structured storage方式,每个PG的redo log record首先按照page进行归类,同一个page的redo log record在写入时,直接append在该页面之后,页面中的已有记录会有一个链接指针,指向最新的记录版本。
除此之外,每个PG内的每个segment上的redo log record都包含一个指针,指向他的前一个log record,通过这个指针,我们很容易
建立起每个PG内部的每个Segement的最大完整的记录LSN,SCL(Segement Complete LSN),通过SCL,我们很容易判断出每个segment缺失哪些log,然后通过与同一个PG内部的其他的存储节点进行相互询问(gossip 协议),补齐缺失的redo log,从而保证最终所有的同一个PG内的所有segment都包含完整的数据。

数据页的合并和旧版本记录的回收
类似HBase、Cassandra采用LSM Tree的NoSQL系统,Aurora也需要有一个垃圾回收和数据页合并的过程。在MySQL中,脏页的刷新是通过Check point的机制来完成的,redo log的空间是有限的,必须要将redo log涉及的数据页持久化到硬盘中,redo log空间才能释放,新的redo log 才能写入,所以MySQL的脏页刷新与客户端的事务提交是密切相关的,如果脏页刷新过慢,可能导致系统必须等待脏页刷新,事务无法提交。另外,Check point机制也决定了脏页是否刷新是根据整个redo log大小来决定的,即使一个页面只是偶尔一次更新,整个数据页在check point推进过程中,都必须重新写入,同时为了确保一个数据页的完整性,MySQL还有double write机制,页面被写两次,代价非常昂贵,显然是不合理的。
Aurora的设计更加巧妙,因为数据是有热点的,不同的数据页的更新频率是不一样的,根据每个Page待更新的redo log record数量,来决定page是否进行合并。
纵观Aurora的设计,一个核心的设计原则就是将数据页看成是日志的一个缓存,通过牺牲一定的读,换取了很好的写性能,这是所有基于log-structured system 共性。

对数据库的操作

写操作
在Aurora中,当每个MTR的redo log record被拷贝到公共的log buffer中,然后写入到对应PG的segment中,得到至少4个segement的ACK时,该MTR就算被持久化,此时VDL就会被推进。
考虑到网络和存储的性能,Aurora也设计了保护措施,如果当前分配的LSN大于VDL+LAL(LSN Allocation limit),则暂时停止分配新的LSN,确保已有的MTR尽快的写入成功。
提交事务
在MySQL中,虽然在事务执行过程中,各个事务是并发执行的,但是在提交时,都是串行的,虽然MySQL 5.6推出了Group Commit,可以批量提交,但是在前一个group提交过程中,其他线程也不得不sleep等待唤醒,这样无疑造成了资源浪费。
在Aurora中,事务的提交完全是异步的,每个事务执行完成以后,提交的过程只是将该事务加入到一个内部维护的列表中,然后该线程就被释放了。当VDL大于该列表中等待提交事务commit对应的lsn时,则由一个线程,向各个客户端发送事务提交确认。

读操作

在MySQL中,所有的读请求都是首先读buffer cache的,只有当buffer cache未命中的情况下,才会读取硬盘。Buffer cache的空间是有限的,在MySQL中,通过LRU的机制,会将一些长时间没有被访问的数据页占用的buffer空间释放。如果这些页面中包含脏页,则必须要等到脏页刷新到硬盘以后才能释放。这样就确保了下次读取该数据,一定能够读取到最新的版本。
在Aurora中,并不存在脏页刷新的过程,所有数据页的合并都是由底层存储节点来完成的。所有数据页在内存中被修改以后,由于这些数据页涉及的redo log可能还没有成功写入底层存储系统,所以这些数据页是不能被换出的。所以在Aurora中,数据页被换出,只有一个条件:数据页的LSN(标识该数据页最近更新对应的LSN)必须小于或者等于VDL,这样的数据页满足两个条件:
  1. 这些页面的对应的redo log一定已经持久化到硬盘中;
  2. 如果内存缺失,这些页面可以通过读取底层的存储系统获得;
(在原论文中,此处有个错误,写的是数据页的LSN 大于等于VDL,应该是作者的笔误)。
有的同学可能会有疑问,如果一个MTR中涉及的页面分别属于两个或者多个PG,如何来确保日志写入的原子性?会不会涉及分布式事务?实际上,此处根本不需要原子性,因为数据库实例在每次读取底层存储系统的数据时,都会按照当前的VDL去读,如果一个MTR中的一个PG成功,一个PG失败,实际这个MTR并没有成功,VDL也不会包括这个MTR,所以即使某个PG多出了部分大于VDL的日志记录,但是数据库实例还是都不到这些数据的,也不存在破坏原子性的问题。
所以在Aurora日常读取过程中,并不需要同时读一个PG的3个segmenet,达到3/6 Quorum的要求,因为有VDL的存在,每个PG的每个segment都维护了SCL,小于这个LSN的所有记录该Segment都已经包含了,所以当数据库实例读取某个PG时,直接找到SCL>VDL的Segment读取即可。
同时,通过同一个PG内部的Segment之间的相互询问,可以建立一个PG的最小的read point,该read point以下的log record实际上才可以被回收合并。
只读节点

在Aurora中,最多可以为一个writer实例创建15个只读实例,这15个只读实例挂载的是相同的存储卷,只读实例不会额外增加存储的开销。为了减少延迟,Writer实例会将写入到存储系统的redo log日志同样发送给只读实例,只读实例接收到redo log日志后,如果要更新的数据页命中了buffer cache,直接在buffer cache中进行更新,但是需要注意的是,如果是同一个MTR的redo log record,必须确保MTR的原子性。如果buffer cache没有命中,则该记录被丢弃。另外,如果被执行的log record的LSN大于当前的VDL,也不会被执行,直接丢弃。
这样的设计确保Aurora只读实例相较于Writer实例延迟不超过20ms。

本文未结束,后续请查看 新一代云端数据库Aurora设计与实现(下)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者郭忆授权发布。