声明
本文是基于Aws Aurora论文的翻译、理解以及思考,论文的原地址是:
https://dl.acm.org/doi/pdf/10.1145/3035918.3056101
整体理解
简而言之Aurora实际上只是将 MySQL 的 redo log 设计成一个独立的存储服务, 从而提高了MySQL的吞吐,关于redo log的基础知识请自行学习,这里只做简答说明。
redo log 就是 MySQL 的 WAL log, 是为了确保事务的持久性的. 每次事务提交前都需要确保 redo log 被持久化. 当数据库 crash 时, 会使用 redo log 进行 point-in-time recovery。
从数据库架构上来说, Aurora仅仅是将下图中的 Log Manager 中的一部分功能剥离出 MySQL, 做成一个高可用的服务, 其他部分不变.
MySQL整体
概述
Amazon Aurora是AWS提供的OLTP类型的数据库服务。本文主要说明Aurora的整体架构以及进行此架构设计的背后原因。进行aurora设计的主导思想是:AWS认为大数据量处理的瓶颈已经由原来的计算和存储资源转移到了网络资源。为了解决存在的网络瓶颈, Aurora将redo log的处理独立处理 ,单独设计成一个多租户且可弹性横向扩展的存储服务。这种设计不仅可以减少网络流量,还可以实现:快速故障恢复,在不丢失数据的情况下实现master故障转移到slave,容错,以及具备自我修复功能的存储服务。Aurora实现了一种高效的异步方案可以在众多存储节点(storage node)上实现基于持久状态的共识,而不需要繁琐的恢复协议。
1 引子
现在越来越多的instructure都要上云。上云的原因包括:按需灵活配置容量以及按需付费等。当然这些上云的instructure中也包括数据库服务。 在现代分布式云服务中,已经有很多先行者实现了部分的存储计算分离,从而可以实现弹性伸缩,进而可以更加便捷的进行数据库从库添加、master故障切换、在线弹性伸缩数据库实例等。
在云环境下的存储计算数据库面临的I / O瓶颈有所改变。由于I / O可以分布在多租户机群中的许多节点和许多磁盘上,因此数据库实例上的磁盘已经不再是瓶颈。相反,瓶颈转移到了多租户请求I / O的数据库层和执行这些I / O的存储层之间的网络。除了每秒数据包(PPS)和带宽的基本瓶颈之外,由于存储计算分离数据库将并行向存储队列发出写操作,因此会进一步增加写入流量。底层的存储节点(storage node)的磁盘或网络性能会影响数据库的响应时间。
尽管数据库中的大多数操作可以并行执行,但是在某些情况下需要同步执行。这种同步执行的操作会导致停顿和上下文切换。一种典型的场景:由于数据库的buffer pool中数据页命中而导致而导致磁盘读取。读取线程在读取完成之前无法继续进行后续操作。由于buffer pool未命中还可能会产生刷脏页(将buffer pool中的脏页刷到磁盘上)等操作(当未命中而从磁盘读取,但是此时buffer pool满了,新读取到的数据页要想存入到buffer pool中就必须将buffer pool中现有的一些脏页刷到磁盘中,从而腾出内存空间供从磁盘中新读取的数据页使用) 。虽然诸如check point和脏页写入之类的后台处理可以减少这种损失的发生,但也可能导致停顿,上下文切换和资源争用的发生。
事务提交是另一个可能会产生负面影响的动作。事务提交导致的停顿可能会阻止其他事务的执行。在云环境的分布式系统中,不太适合使用多阶段提交协议(例如2阶段提交提交(2PC)),因为这类协议对故障的容忍度比较低,而大规模分布式系统中硬件和软件故障确是常态化的。除此之外,由于大规模的分布式系统经常跨多个数据中心,因此网络的是高延迟也是一个很严重的问题。
Amazon Aurora通过在高度分布式的云环境中更加积极地利用redo log来解决上述问题。Aurora基于多租户横向扩展的存储服务(Storage service,)实现了一种新颖的面向服务的体系架构,如下图所示:
该存储服务抽象出一种虚拟化的分段redo log并且与数据库实例完全解耦。尽管每个数据库实例仍包含传统数据库内核的大部分组件和功能(query processor, transactions, locking, buffer cache, access methods and undo management),但是将某些功能(redo logging, durable storage, crash recovery, and backup/restore)从数据库实例中剥离出来,并下推到了存储服务(storage service)中。
与传统的数据库相比,Aurora体系架构具有三个重要优势。
首先,通过将构建跨多个数据中心的、独立的、可以容错和自我修复的存储服务,可以让数据库服务免受由于网络或存储层的瞬间或永久性故障而带来的性能损耗。
其次,通过仅将redo log写入存储服务,aurora能够将网络IOPS降低一个数量级。一旦消除了网络瓶颈,我们就可以积极的继续优化其他的瓶颈,这种对网络瓶颈的优化相对于对MySQL代码的优化而言,收益显著。
第三,aurora将一些最复杂和关键的功能(备份和重做恢复)从数据库引擎剥离出来,以前在数据引擎中每做一次这种操作都会直接对数据库的读写性能产生负面影响。现在剥离出来了,转移到大型分布式集群中,并且分摊到集群中的每个节点上执行持续的异步操作。这样就可以在没有check point的情况下实现近乎即时的故障恢复并且也可以实现对数据库读写无感的轻量备份。
2 大规模数据的持久性
数据库的持久化要求:数据一旦写入,就可以被读取。在本节中,我们将讨论quorum模型的原理,为什么我们要对storage进行分段以及如何将各个段结合在一起,从而不仅可以提供持久性,可用性和减少性能抖动,还可以帮助我们解决很多大型存储集群的运维问题。
2.1 数据复制以及故障处理
数据库实例的存活周期与存储的数据的存活周期无必然联系。数据库实例故障时,客户可以将其关闭。并且可以根据负载上下调整实例大小。因此这种特性是有利于实现存储计算分离。
但是一旦实现了存储计算分离,存储节点(storage node)和磁盘也可能发生故障。因此,必须实现数据复制并提供故障恢复能力。在大规模云环境中,节点,磁盘和网络故障常态化。每种故障可能具有不同的持续时间和影响范围。例如,一个节点可能会出现暂时的网络不可达,重新启动时可能会暂时停机,或者磁盘,节点,机架,分支或主干网络交换机甚至数据中心永久性故障。
Aurora存储层的复制基于Quorum协议,假设复制拓扑中有V个节点,每个节点有一个投票权,读 或 写 必须拿到Vr 或 Vw个投票才能返回。为了满足一致性,需要满足两个条件,首先Vr + Vw > V,这个保证了每次读都能读到拥有最新数据的节点;第二,Vw > V/2,每次写都要保证能获取到上次写的最新数据,避免写冲突。比如V=3,那么为了满足上述两个条件,Vr=2,Vw=2。为了保证各种异常情况下的系统高可用,Aurora的数据库实例部署在3个不同AZ(AvailablityZone),每个AZ包含了2个副本,总共6个副本,每个AZ相当于一个机房,是一个独立的容错单元,包含独立的电源系统,网络,软件部署等。结合Quorum模型以及前面提到的两条规则, V=6,Vw=4,Vr=3,Aurora可以容忍任何一个AZ出现故障,而不会影响写服务;任何一个AZ出现故障,以及另外一个AZ中的一个节点出现故障,不会影响读服务且不会丢失数据。
2.2 分段存储
通过Quorum协议,Aurora可以保证只要AZ级别的故障(火灾,洪水,网络故障)和节点故障(磁盘故障,掉电,机器损坏)不同时发生,就不会破坏协议本身,数据库可用性和正确性就能得到保证。那么,如果想要数据库“永久可用”,问题变成如何降低两类故障同时发生的概率。由于特定故障发生的频率(MTTF, Mean Time to Fail)是一定的,为了减少故障同时发生的概率,可以想办法提高故障的修复时间(MTTR,Mean Time To Repair)。Aurora将存储进行分片(segment)管理,每个分片10G,6个10G副本构成一个PGs(Protection Groups)。Aurora存储由若干个PGs构成,这些PGs实际上是EC2(AmazonElastic Compute Cloud)服务器+本地SSD磁盘组成的存储节点构成,目前Aurora最多支持64T的存储空间。分片后,每个分片作为一个故障单位,在10Gbps网络下,一个10G的分片可以在10s内恢复,因此当前仅当10s内同时出现大于2个的分片同时故障,才会影响数据库服务的可用性,实际上这种情况基本不会出现。 通过分片管理,巧妙提高了数据库服务的可用性。
2.3 弹性运维
基于分片管理,系统可以灵活应对故障和运维。比如,某个存储节点的磁盘IO压力比较大,可以人为的将这个节点标记为bad,quorum将会通过快速将这部分数据迁移到集群中的其他冷节点上来将这部分被标记为bad的数据进行修复。另外,也可以使用相同的方式进行集群中节点的软件升级。所有这些故障和运维管理都是分片粒度滚动进行的,对于用户完全透明。
3. 日志即数据库
3.1 写放大问题
Aurora的存储容量分片模型、6副本、多数派quorum共同保证了aurora的高度弹性。不幸的是,若只是简单地将此模型应用在传统数据库(如MySQL)上,会导致数据的性能极不稳定,因为每当应用执行一次数据库写入,数据实际上会产生多次不同的I/O操作。而这种高I/O操作又通过复制进行了放大。而且,很多I / O都是同步操作,从而使数据处理流程停滞并增加延迟。
让我们研究一下传统数据库中的写入操作做了哪些事情。以mysql为例:首先将数据页写入到堆文件或者b树中,并将redo log写入到预写日志(WAL)。每个redo log记录都包含了被修改页的前后差异。可以将redo log应用于旧页从而产生新页。
实际上,除了redo log,还必须写入其他数据。例如,考虑一个进行同步复制的mysql集群,并且要求该集群在数据中心之间实现高可用性,并以primary-standby的模式运行,如图所示:
AZ1中有一个primary的MySQL实例中的数据存储在Amazon Elastic Block Store(EBS)上。 AZ2中还有一个stand by的MySQL实例,数据也存储在EBS上。通过mysql的同步复制实现primaryEBS卷的写入与standby的EBS卷同步。
上图中显示了数据库引擎需要写入的各种不同类型的数据: redo log和binlog(将会被归档到S3存储上,以支持基于时间点的数据恢复)修改后的数据页、double-write的数据页和元数据文件(FRM)。
该图还显示了实际IO流的顺序。在第1步和第2步中,将向EBS发出写操作,然后由EBS将其发送到同AZ中的本地镜像EBS,并在完成这两个操作完成后接收确认。接下来,在步骤3中,将写操作同步到standby mysql实例。最后,在步骤4和5中,将数据写入到standy 实例使用的EBS以及相应的同AZ中的本地镜像EBS。
上图中展示的MySQL架构模型由于写放大而被人诟病。首先,步骤1、3和5是顺序的且同步的。
这种模型延时很高,因为要进行4次网络IO,且其中3次是同步串行的。从存储角度来看,数据在EBS上存了4份,需要4份都写成功才能返回。 所以这种模型会导致数据库的性能极差。
3.2 将redo log的处理下推到存储层
当传统数据库修改数据页时,将生成一个redo log record,并调用日志应用程序,该应用程序将redo log record应用于内存中的数据页,从而生成最新的数据。事务提交的时候必须即时写入日志,但是数据页的更新可能会延后(mysql中的change buffer)。
在Aurora中的数据库实例唯一通过网络执行的写入数据就是redo log record,无论从数据库层还是从后台都不会写入任何数据页,因此也不会有check point和cache的换入换出。Aurora数据库实例中的log applicator会直接将redo log record推送给存储层(storage service),而storage service则会在后台或者需要的时候基于redo log生成数据库的data page。当然,基于redo log每次都从头去应用redo log进而生成每个data page的代价是非常大的。因此,Aurora的存储服务会不断在后台去应用redo log 从而生成数据库data page,以避免每次都从头开始重新生成它们,这个过程称之为后台物化(background materialization)。从正确性来看,后台物化完全是可行的:就引擎而言,log就是数据库,而存储系统生成的data page都是log的缓存。只有含有大量redo log(修改记录)的data page是需要考虑被从后台重新物化(生成)的,因为这种含有大量redo log的数据页若不及时应用,则在真正请求该数据页的时候,需要应用大量的redo log,从而导致请求性能极低。因此storage node中的check point是由整个redo log chain的长度控制的。 Aurora中data page的物化操作是由data page所对应的redo log chain的长度控制的。
尽管放大了复制的写操作,但是这种设计仍大大降低了网络开销,并提供了性能和持久性。存储服务以并行方式扩展了 I/O,而且也不会影响数据库引擎的写吞吐量。如图所示:
上图显示了一个Aurora群集,其中有一个主实例和跨多个AZ部署的多个副本实例。在此模型中,主数据库仅将redo log record写入存储服务,并将这些redo log record以及元数据更新信息流式地传输到副本实例。 IO流基于分片(逻辑分片 logic segment,即PG)对一批redo log record进行排序,并将每批完成排序的redo log records写入到所有6个副本(storage node)中,并且在每个storage node上将这些redo log record保存在磁盘上,数据库引擎等待6个中的4个进行确认答复后才会认为写入成功从而保证redo log的持久性。每个副本都会应用redo log record从而更新各自的buffer cache。
通过SysBench 我们对Aurora进行了只写工作负载测试,该工作负载通过分别对上述的跨多个AZ的mirrored mysql集群和跨多个AZ的 Aurora MySQL集群写入100GB的数据进行测试,并且双方均运行持续压测了30分钟。结果如下:
测试结果解读:
相同时间内,Aurora能够处理的事务是镜像MySQL的35倍。
Aurora中数据库节点上每个事务的I / O数量比mirrored MySQL少7.7倍。
由于减轻了网络负载,因此可以积极地通过复制数据以提高持久性和可用性,并且每次发起读写请求都可以并行执行,以最大程度地减少性能抖动。
存储服务来处理redo log还可以最大程度地减少崩溃恢复时间来提高可用性,并消除由后台处理(如check point,后台数据页写入和备份)引起的性能抖动。
我们研究一下崩溃恢复过程。在传统数据库中,崩溃后,系统必须从最新的check point开始并重新应用redo log,以确保应用所有已经持久化的redo log。在Aurora中,持久化的redo log record在存储服务上持续不断地异步地在整个集群上被一直被应用。如果data page不是最新的,则对data page的任何读取请求都可能需要应用一些redo log record。因此,崩溃恢复的处理过程分散在所有常规的数据请求处理中。因此数据库启动或者故障恢复时几乎不需要任何操作。
3.3 存储服务设计要点
存储服务(storage service)的核心设计原则是最大程度地减少写入请求的延迟。因此aurora将大部分的存储处理都移至后台。例如,当存储节点(storage node)忙于处理写入请求时,不必立即执行旧data page的垃圾回收(GC),除非磁盘快要达到容量上限时,才必须马上启动旧数据的gc。在Aurora中,后台处理程序与前台业务处理具有负相关性。这不同于传统的数据库,在传统的数据库中,data page的后台写入和check point与系统上的前台数据写入负载具有正相关,也就是说后台的操作会直接影响前台的数据操作的性能 。由于在aurora中的各个分片是极度平均的分散在整个集群的各个存储节点上的,而且在存储服务上还存在4/6的quorum模型进行仲裁,因此后台操作对前台的读写性能的影响是可控的。
我们更详细地研究一下存储节点上的各种处理逻辑。如图所示,它涉及以下步骤:
(1)接收redo log recored并将其添加到内存队列中
(2)将redo log record 持久存储到磁盘上并将确认消息发送给数据库实例;
(3)将受到的redo log record进行组织并识别出由于某些batch丢失而导致的redo log record空洞。
(4)通过gossip协议与其他的storage node同步,获得丢失的redo log record,进而填补确实的空洞。
(5)将redo log record合并到新的data page中。
(6)定期将阶段性的redo log和对应的新的data page暂存到S3。
(7)定期对旧版本数据进行GC。
(8)定期验证data page上的CRC code。
上述步骤中仅(1)和(2)是处于读写执行路径上的并且,这两部是同步执行的,可能会影响到读写延迟,其他步骤都是异步执行的。
4. 日志处理
本节描述如何从数据库引擎生成log,进而始终保证持久状态,运行时状态和副本状态的一致。特别是,会详细说明如何在不使用昂贵的2PC协议的情况下有效地实现一致性。
4.1 异步处理
由于aurora的核心就是redo log stream,因此可以利用redo log的有序性、顺序性,且记录了data page的变化记录的特性进行进一步处理。实际上,每个redo log record都有一个关联的日志序列号(LSN),并且它是由数据库生成的单调递增的值。
基于上述redo log的特性,storage node通过异步方式实现了一个简单的维护状态的一致性协议,而没有使用像2PC协议,因为2PC协议容易出错且对故障的容忍度极低。从更高的层面去看,storage node会一直维护一致性和持久性的point,并在收到未完成的存储请求的确认后不断向前推进这些point。由于任何单个存储节点都可能都丢失一个或多个redo log record,因此它们会与PG的其他成员通过gossip协议找到丢失的redo log record进行填补。由于数据库本身就维护的运行时状态,因此我们可以使用单分片读取来取代基于quorum的读取,除非在故障恢复的时候数据页需要rebuild,这时丢失了运行时状态,才需要基于quorum进行读取。
数据库实例中活跃着很多事务,事务的开始顺序与提交顺序也不尽相同。当数据库异常宕机重启时,数据库实例需要确定每个事务最终要提交还是回滚。
存储服务记录了VCL(Volume Complete LSN),可以保证LSN小于VCL的redo log record都是是可用的。在故障恢复时,所有LSN大于VCL的日志都要被截断。但是数据库可以通过对redo log record 打标从而其标识为CPLs或一致性点LSNs(ConsistencyPoint LSNs)来进一步限制允许被截断的point的子集。因此VDL/持久卷LSN( Volume Durable LSN)就是小于或等于VCL,且已经持久化了的最高CPL,因此在故障恢复时需要截断所有大于VDL的LSN号对应的redo log record。
如下图所示:
每个事务在物理上由多个mini-transaction组成,而每个mini-transaction是最小原子操作单位,比如B树分裂可能涉及到多个数据页的修改,那么这些页修改对应的对应一组日志就是原子的,在回放redo log时,也需要以mini-transaction为单位。
CPL就是属于一个mini-transaction的一组日志中最后(最大,因为LSN是递增的)一条日志的LSN,一个事务由多个CPL组成,所以称之为CPLs。为了保证不破坏mini-transaction原子性,所有大于VDL的日志,都需要被截断。
举个例子:
即使我们的VCL为1007,并且现在的CPLS为:900, 1000和1100,我们也必须截断LSN>1000的日志。
因此完整性和持久性的含义是不同的,VDL表示了数据库已经持久化了的处于一致状态的最新位点,在故障恢复时,数据库实例以PG为单位确认VDL,截断所有大于VDL的日志。 数据库和存储交互过程如下:
1.每个数据库级别的事务被分解为多个有序的mini-transactions(MTR),这些事务中的每一个都可以原子执行。
2.每个mini-transaction都由多个连续的log record(根据需要)组成。
3.mini-transaction中的最终log record是CPL。
恢复时,数据库与storage service进行交互以建立每个PG的持久点(durable point),并使用该持久点来建立VDL,然后发出命令以截断大于VDL的log record。
4.2 数据库操作流程
现在,我们针对数据库引擎的一些常规操作流程进行说明。
4.2.1 Writes
在Aurora中,数据库实例向存储节点传递redo日志,达成多数派后将事务标记为提交状态,然后推进VDL,使数据库进入一个新的一致状态。在任意时刻,数据库中都会有大量活跃的并发事务,每个事务都会生成自己的redo log record。数据库会为每个log record分配一个唯一有序的LSN,这个LSN一定大于当前最新的VDL,为了避免前台事务并发执行太快,而存储服务的VDL推进不及时,定义了LSN Allocation Limit(LAL),目前定义的是10,000,000,这个值表示新分配LSN与VDL的差值的最大阀值,设置这个值的目的是避免存储服务成为瓶颈,进而影响后续的写操作。
由于底层存储按segment分片,每个分片管理一部分数据 ,当一个事务涉及的修改跨多个分片时,事务对应的日志会被打散,每个分片只能看到这个事务的部分日志。 为了确保各个分片日志的完整性,每条日志都记录前一条日志的链接,
我们发现PG中的每个segment都只管理一部分数据,当一个事务涉及的修改跨多个分片时,事务对应的日志会被打散到多个segment上,为了确保各个分片日志的完整性,每条日志中都包含一个前向链接,前向链接指向了前一条log record。通过前向链接就可以遍历分布在PG中各个segment上的所有redo log,进而构建一份完成的LSN:SCL(Segment Complete LSN),SCL代表PG当前receive到的最大LSN。SCL主要用于各个storage node通过gossip协议查找和交换各自缺失的redo log record。
4.2.2 Commits
在Aurora中,事务的提交是异步的。每个事务由若干个日志组成,并包含有一个唯一的“commit LSN”。当工作线程处理事务提交请求时,处理提交请求的线程将事务的“commit LSN”提交到持久化队列中并将事务挂起继续处理数据库请求,该持久化队列中记录了所有待提交事务的“commit LSN”。当VDL的位点大于事务的commit LSN时,表示这个事务redo日志都已经持久化,可以向客户端回包,通知事务已经成功执行。在Aurora中,有一个独立的线程处理事务成功执行的回包工作,因此,从整个提交流程来看,所有工作线程不会因为事务提交等待日志推进而堵塞 ,他们会继续处理新的请求,通过这种异步提交方式,大大提高了系统的吞吐。
整个处理过程如下:
- StorageNode的工作线程T1接收到事务1的提交请求,并将对应的commit-LSN-1放入到持久化队列中,然后马上将事务1挂起,继续处理其他数据库请求。
- StorageNode中的另一个异步线程T2,不断地将持久化队列中的每个commit-LSN持久化到磁盘上,并记录VDL,让VDL大于commit-LSN-1时,表示事务1的redolog全部持久化。
- 由单独的线程T3不停的检测提交成功的事务列表,并基于事务向客户端返回包,通知事务执行成功。
4.2.3 Reads
与大多数数据库一样,在Aurora中,数据是从buffer cache中进行读写的,并且仅当buffer cache中不存在所请求的数据时,才会从磁盘中获取。从磁盘中读取的数据也要放入到buffer cache中,而如果buffer cache已满,则根据特定的淘汰算法(比如LRU),系统会选择将一个数据页淘汰置换出去,如果需要被置换的数据页被修改过,在传统的数据库系统中,首先需要将这个数据页刷盘,确保下次访问这个页时,能读到最新的数据。
但是Aurora不一样,需要被置换出去的数据页并不会刷写到磁盘,而是直接丢弃。 这就要求Aurora缓冲池中的数据页一定有最新数据,被淘汰的数据页的LSN需要小于或等于VDL。(注意,这里论文中描述有问题,page-LSN<=VDL才能被淘汰,而不是大于等于) 这个约束保证了两点:1.这个数据页所有的修改都已经在日志中持久化,2.当缓存不命中时,通过持久化的数据页和VDL总能得到最新的数据页。
在正常情况下,进行读操作时并不需要达成Quorum。当数据库实例从disk去读数据页时,将当前请求发起时的最新VDL作为本次读的一致性位点read-point,并选择一个VDL大于等于read-point的storage node,这样只需要访问这一个storage node即可得到本次请求所需要的数据页的最新版本。从实现上来看,因为所有数据页通过segment管理,数据库实例记录了storage node管理的分片以及SCL信息,因此进行IO操作时,通过元信息可以知道具体哪个storage node有需要访问的数据页,并且SCL>read-point。
数据库实例接收客户端的请求,会计算出每个PG的Minimum Read Point LSN,在有读副本实例的情况下,写实例会通过gossip协议与各个读副本实例之间建立起每个PG的Minimum Read Point LSN(基于每个storage node得出的Minimum Read Point LSN) ,称之为PGMRPL( Protection Group Min Read Point LSN)。PGMRPL是全局read-point的低水位,PG中所有LSN低于PGMRPL的redo log record都可以删掉。换而言之,任意一个storage node上都不会再发起read point LSN小于PGMRPL的数据页查询请求。
每个storage node都会记录PGMRPL,因此可以不断地应用redo log record生成新的将数据页进行持久化,并回收不再使用的redo log record。
4.2.4 Replicas
在Aurora中,写副本实例和最多15个读副本实例共享一套分布式存储服务,因此增加读副本实例并不会造成写放大,也不会增加磁盘存储资源。这也是共享存储的优势,在不增加存储成本的前提下增加新的读副本,从而提升读能力。读副本和写副本实例间通过redo log同步。写副本实例往storage node发送日志的同时也会向读副本发送日志,读副本按日志顺序回放, 如果回放日志时,若需要访问的数据页:page1不在缓冲池中,则直接将日志丢弃。可以丢弃的原因在于,storage node拥有所有日志,当下次需要访问page1数据页时,storage node根据read-point,可以构造出特定版本的数据页。 需要说明的是,写副本实例向读副本发送日志是异步的,写副本执行提交操作并不受读副本的影响。副本回放日志时需要遵守两个基本原则,1).回放日志的LSN需要小于或等于VDL
2).回放日志时需要以MTR为单位,确保副本能看到一致性视图。
在实际场景下,读副本与写副本的延时不超过20ms。
4.3 Recovery
大多数数据库基于经典的ARIES协议处理故障恢复,通过WAL机制确保故障时已经提交的事务持久化,并回滚未提交的事务。这类系统通常会周期性地做检查点,并将检查点信息计入日志。故障时,数据页中同时可能包含了提交和未提交的数据,因此,在故障恢复时,系统首先需要从上一个检查点开始回放日志,将数据页恢复到故障时的状态,然后根据undo日志回滚未交事务。从故障恢复的过程来看, 故障恢复是一个比较耗时的操作,并且与检查点操作频率强相关。通过提高检查点频率,可以减少故障恢复时间,但是这直接会影响系统处理前台请求吞吐,所以需要在检查点频率和故障恢复时间做一个权衡,而在Aurora中由于其存储计算分离的架构设计,不需要做这种权衡。
传统数据库中,故障恢复过程通过回放日志推进数据库状态,重做日志时整个数据库处于离线状态。
Aurora也采用类似的方法,区别在于将回放日志逻辑下推到存储节点,并且在数据库在线提供服务时在后台常态运行。 因此,当出现故障重启时,存储服务能快速恢复,即使在10wTPS的压力下,也能在10s以内恢复。数据库实例宕机重启后,需要故障恢复来获得运行时的一致状态,实例与Read Quorum个存储节点通信,这样确保能读到最新的数据,并重新计算新的VDL,超过VDL部分的日志都可以被截断丢弃。在Aurora中,对于新分配的LSN范围做了限制,LSN与VDL差值的范围不能超过10,000,000,这个主要是为了避免数据库实例上堆积过多的未提交事务,因为数据库回放完redo日志后还需要做undo recovery,将未提交的事务进行回滚。在Aurora中,收集完所有活跃事务后即可提供服务,整个undo recovery过程可以在数据库online后再进行。
5. 总结
Aurora的整体架构图如下所示:
Aurora数据库引擎基于社区版InnoDB引进行改造,主要对磁盘IO读写数据的方式进行了改造,将这些操作分离到存储服务层。在社区InnoDB中,一个写操作会修改缓冲池中数据页内容,并将对应的redo日志按顺序写入WAL。事务提交时,WAL协议约定对应事务的日志需要持久化后才能返回。实际上,为了防止页断裂,缓冲池中修改的数据页也会写入double-write区域。数据页的写操作在后台进行,一般是在页面置换或是做检查点过程中发生。InnoDB除了IO子系统,还包括事务子系统,锁管理系统,B+Tress实现以及MTR等。MTR约定了最小事务,MTR中的日志必需以原子的方式执行(比如B+Tree分裂或合并相关的数据页)。
在Aurora InnoDB中,每个redo log record都代表着可以在每个MTR中原子执行的数据页的改变。多个redo log records会被组织成batch,一个batch的redo log records会按照PGs划分,写入到storage service中。每个MTR的最后一条日志是一致性位点。与社区的MySQL版本一样,支持标准的隔离级别和快照读。 Aurora只读副本不断从写副本实例获取事务开始和提交信息,并利用该信息提供快照读功能。Aurora的并发控制完全在数据库引擎中实现,不会影响存储服务。数据库实例与存储服务层相互独立,存储服务层向上为数据库实例提供统一的数据视图,数据库实例从存储服务层获取数据与从本地读取数据一样。
上图展示了云上Aurora的部署架构,Aurora利用AmazonRelational Database Service (RDS)来进行管理。RDS在每个实例部署一个agent,称之为Host Manager(HM)。HM监控集群的健康状况并确定是否需要做异常切换,或者是该实例是否需要被替换。每个集群由一个写副本,0个或多个读副本组成。所有实例在都一个物理Region(比如美国东部,美国西部),一般是跨AZ部署,并且每个实例都会与同一个region中的分布式存储服务层进行交互。为了保证安全,Aurora在数据库层,应用层和存储层做了隔离。实际上,数据库实例通过3类Amazon Virtual Private Cloud (VPC)网络可以相互通信。应用程序可以通过应用层VPC访问数据库;数据库可以通过RDS VPC管控节点交互;数据库可以通过存储层VPC和存储服务节点交互。
存储服务实际上是部署在一组EC2集群上的,这个集群横跨至少3个AZ,对多个用户提供存储,读写IO,备份恢复等服务。storage node管理本地SSD并与数据库实例和其它storage node交互,备份/还原服务不断备份新数据到S3,在必要时从S3还原数据。存储服务的管控系统借助Amazon DynamoDB服务作为持久化存储,存储内容包括配置,元数据信息,备份到S3数据的信息等。Aurora使用Amazon Simple Workflow Service来编排和管理需要长时间执行的操作,例如:数据库存储卷的恢复操作或者当一个storage node宕机后需要执行的修复操作。为了保证存储服务高可用,整个系统需要在异常影响到用户之前,主动快速发现问题。存储系统中的所有关键操作都被监控,一旦关键性能或者可用性方面出现问题,则立即会产生报警。
6. 性能数据
性能数据我这里不一一展开了,具体数据大家可以参考原论文。
7. 实践经验
我们发现越来越多的应用迁移到Aurora集群,他们有一些共通点,我们希望抽象出一些典型的场景,并总结经验。
7.1多租户
Aurora很多用户都在做Software-as-a-Service(SaaS)服务,这些服务底层存储模型一般比较稳定,通过多个用户共享一个数据库实例来减少成本。这种模式下,数据库实例上会有大量的表,导致元数据暴增,这加大了字典管理的负担。这种架构下,用户一般会面临解决三类问题:
1).实例上持续较高的吞吐和并发,
2).能够灵活应对磁盘空间问题,提前评估磁盘空间并具备快速的扩展能力,
3).不同租户间的影响要控制在最小。
Aurora能完美解决这三类问题。
7.2高并发处理能力
互联网应用通常会因为各种原因导致压力骤增,这需要系统有良好的扩展能力和处理高并发的能力。在Aurora中,由于底层的存储服务和上层的计算节彻底分离,因此可以方便地自动扩容,因此Aurora具备快速扩展能力,并且实际上Aurora有很多用户连接数长期维持在8000以上。
7.3表结构变更
由于表结构变更往往伴随着锁表和拷表,持续时间也比较长,而DDL是一个日常操作,因此需要有一套高效的online DDL机制。主要包括2点:1).schema多版本,每个数据页都存储当时的schema信息,页内数据可以通过schema信息解析,
2).对于修改的page采用modify-on-write机制来减少影响。
7.4软件更新
由于用户使用Aurora实例通常只有一个主实例,因此发生任何问题都特别严重。Aurora中,所有的持久化数据都在存储层,数据库实例的状态信息可以借助存储层和元数据获得,因此可以很方便的构造一个新的数据库实例,为了提高软件升级效率,我们通过Zero-Downtime Patch (ZDP)来滚动升级
8. 总结
Aurora诞生的原因是在弹性伸缩的云环境下,传统的高吞吐OLTP数据库既不能保证可用性,又不能保证持久性。 Aurora的关键点在于将传统数据库中的存储与计算分离,具体而言,将日志部分下推到一个独立的分布式存储服务层。由于这种分离架构下,所有IO操作都是通过网络,网络将成为最大的瓶颈,因此Aurora集中精力优化网络以便提高系统吞吐能力。Aurora依靠Quorum模型,在性能影响可控的前提下,解决云环境下的各种异常错误。在Aurora中,日志处理技术减少了I/O写放大,异步提交协议避免了同步等待,同时分离的存储服务层还避免了离线故障恢复和检查点操作。Aurora的存储计算分离方案使得系统整体架构非常简单,而且方便未来的演进。
Q&A
-
一般了解到的Quorum算法只需要满足Vr + Vw > N即可,为啥Aurora还需要满足第2个条件?
从文中了解到Aurora中Quorum算法需要满足两个条件:
a).Vr + Vw > N,NWR算法,确保读和写有交集,能读到最新写入的数据。
b).Vw > N/2,避免更新冲突。
Quorum算法的基本含义是确保读副本数与写副本数有交集,能读到最新写入的数据。Aurora加入第二条约束主要是为了保证每次写入集合都与上次写入的节点集合有交集,确保能读到最近一次的更新,这样日志能够以自增的方式追加,确保不会丢失更新。从另外一个角度来说,写副本数设置为Vw > N/2也间接在读写性能做了均衡。假设N=5,W=1,R=5,满足第一个条件,每次写入一个节点即可返回成功,那么读取时则需要读取5个副本,才能拿到最新的数据。 -
每个segment分片是10G,如果事务跨多个segment分片如何处理?
每个事务实际是有若干个MTR(mini-transaction)构成,MTR是InnoDB中修改物理块的最小原子操作单位,MTR的redo日志作为一个整体写入到全局redo日志区。在Aurora中存储按照segment分片管理,发送日志时,也按分片归类后发送,那么就存在一种情况,一个MTR跨多个segement分片,MTR日志被打散的情况。本质上来说,这种情况也不存在问题,因为事务提交时,如果事务跨多个segment分片,则会需要多个PG都满足Quorum协议才返回,进而推进VDL。所以如果VDL超过了事务的commit-LSN,则表示事务涉及到的日志已经全部持久化,不会存在部分segment日志丢失的问题,所以能够保证事务持久性。
3.Aurora 读副本如何实现MVCC?
在Aurora中,写副本实例往存储节点发送日志的同时会传递日志到读副本实例,读副本以MTR为单位回放日志;与此同时,写副本还会将事务开始和提交的信息传递给读副本,这样在读副本实例上也能构造出活跃的事务视图。在读副本上执行读操作时,会依据活跃事务视图作为记录可见性判断依据,进而能读到合适版本的数据。当然,Aurora读副本与写副本之间是异步复制,至多有20ms的延迟,因此在Aurora读副本上可能不能读到最新的提交数据。
- 为什么Aurora有成本优势?
Aurora中,多个数据库实例(写副本+多个读副本)共享一个分布式存储层。对于数据库引擎而言,整个存储服务一个大的资源池,用户根据需求申请存储空间,最小粒度是10G,相对于单个实例的本地存储,存储空间利用率大大提升,而且非常方便扩展。另一方面,所有数据库引擎公用一份存储,零存储成本增加数据库引擎,能大幅降低成本。
5.Aurora 的优势和缺陷?
优势:
1). 存储节点,计算节点弹性伸缩,按需配比
2). 一份存储,对接多个计算节点,多租户存储服务,成本低。
3). 只传递日志,巧妙的解决写放大问题。
4). 实例快速故障恢复
5). 架构简单,通过多副本能快速扩展读能力,单个写副本则巧妙地避免了分布式事务等复杂实现。
缺陷:
1). 适合于读多写少的应用,写水平扩展依赖于中间件方案。
2). SQL层与社区版MySQL一样,复杂查询能力(比如OLAP场景)较弱。
3). 单个写副本,无分区多点写能力(用户维度+时间维度)
4). 总容量有上限,64TB
6.Aurora 与Spanner的异同点?
image从对比来看,Aurora与Spanner走的两条不同的路线,Aurora以市场用户为导向,所以选择全面兼容MySQL/PostgreSQL,用户可以无缝迁移到Aurora。另外就是,Aurora基于MySQL修改,并通过共享存储方案避免了二阶段提交,分布式事务等复杂的实现,因此开发周期相对较短,也更容易出成果。反观Spanner,是一个重新设计的数据库,无论是基于Paxos的强同步还是分布式事务的支持,以及利用TrueTime机制实现全局一致性读等都是比较复杂的实现,功能非常强大,但是在SQL兼容性这块确做地不够好,这也可以解释为什么Aurora广泛用于云上业务,而Spanner则更多地是在Google内部使用,相信即使Spanner现在开放了云版本,其SQL兼容性也是一个很大的挑战。当然Aurora本身的数据库引擎就是MySQL,其对于复杂查询的短板还需要持续改进和优化
-
Aurora有没有checkpoint?
Aurora的PGMRPL可以认为是检查点。LSN⼩小于这个点的数据⻚页已经刷盘,⽽而⼤大于这个点的⻚页可 以刷盘,也可以不不刷盘,LSN⼩小于这个点的⽇日志都可以丢弃。PGMRPL在逻辑上与存储引擎的检 查点是等效的。可以认为是PG上的检查点。 -
Aurora与polardb
Aurora与Polardb都是存储与计算分离、单写多读、共享分布式存储的架构。很显然,polardb在写放⼤上⾯没有做优化。从节点需要同步主库的脏⻚页,共享的存储,私有的脏⻚页,是个很难解决的矛盾。因此polardb的读节点会影响写节点。⽽Aurora可以认为没有脏⻚页。日志即数据库,可以把日志应⽤服务器看作更慢的存储,存储服务与缓存都可以认为是日志应用服务器的cache。
从节点与主节点之间有延迟,⽽aurora存储的数据页有多版本,文中明确指出存储有旧⻚回收处 理。从节点依据读取点LSN读取到指定版本的页,⽂文中指出,写节点到读复制节点之间的 延迟⼩于20ms,因此,不可回收的旧版本⻚应该不会太多。 -
临时表
每个读节点虽然只有查询操作,但是查询操作会⽣成临时表用以保存查询的中间结果。⽣成临时表数据是不会产生日志的。但是这里仍有写IO,个⼈人认为,Aurora有可能直接写在本地存储,这样不会产生网络上的负担
参考与感谢
https://www.jianshu.com/p/db00b1e4695e
https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Replication.html
https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Replication.MySQL.html
https://www.cnblogs.com/cchust/p/7476876.html
网友评论