0%

AmazonAurora云数据库

Amazon Aurora 云端分布式数据库

论文: Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Database

Amazon Aurora是亚马逊从2014年开始提供的一种云上关系型数据库架构,基于mysql的基础上改进而来,在实现传统关系型数据库特性的基础上,实现了事务吞吐量、错误恢复等性能巨大提升。

Amazon Aurora到来之前

要理解Amazon Aurora的设计原理,首先要了解一般数据库的事务执行流程;传统单机事务型数据库数据一般以B-Tree形式组织存储在硬盘上,并在内存中存储数据的缓存以加速访问/修改过程。以写事务流程为例,一般的事务执行过程如下:

  1. 首先锁定要想修改的数据,防止其他事务修改
  2. 在WAL(Write-ahead-log)中写入当前事务日志项
  3. 根据操作修改缓存中的数据项
    • 修改前镜像+修改日志+修改后进行
  4. 提交事务,等待特定的时机(缓存区满等),再将缓存持久化到磁盘中

之所以采用WAL的形式,是因为写入磁盘的成本过高,通过追加形式写入WAL降低了提交事务的成本,再合适的时机再将修改持久化到磁盘中。

  • 另外通过多次修改合并,再一次将缓存持久化到磁盘,降低了磁盘读取写入的速度
  • MySQL通过redo、undo log实现了WAL的机制,最终实现了事务的原子性和持久化

为什么会有Aurora

论文中首先分析了在使用Aurora之前,Amazon云服务提供的基于MySQL的云上关系数据库服务存在的问题,基本架构如下图所示:

  • 每个数据库实例采用主从备份形式,分为主实例和从实例。负责存储数据的EBS(Amazon弹性块存储单元,我就理解为一个逻辑上的存储服务器),都带有一个镜像EBS,备份主EBS上的数据。
  • 每当数据修改,首先在主服务上进行提交即执行1操作后,再被备份到从服务上即执行3操作

上述架构存在的问题,导致了Aurora的出现

  1. 写放大问题。写入涉及到大量数据,包括redo log、binaray log、修改的数据页、临时的double-write以及FRM元数据等,另外由于操作1->3->5必须顺序执行,增加了写入的延时。
  2. 备份需要通过网络传输修改涉及到的数据,数据传输量过大

相关概念和名词定义

Availability Zone(AZ)

服务器可用区。定义为一系列具有“区域”临近关系的服务器节点集合(可以理解为一个数据中心内的服务器节点)

  • 一个AZ内的节点被认为是既有错误相关性的,存在AZ内所有服务器因为某种原因宕机的情况(例如:网络中断、洪水、网络升级、软件部署等)
  • 不同AZ之间通过低延迟网络连接,对于上述类型错误具有隔离性(例如:东部的服务器中心停电了,西部的没问题正常工作)
  • 如磁盘错误、服务器过热宕机等等这里不具有相关性的导致服务器宕机的错误,在不同AZ内部普遍存在

Data Segment

将数据库中的数据划分为固定大小(10G)的数据段,数据段是错误和恢复的最小单元

Protection Groups (PGs)

每个Data Segment在Amazon Aurora中共存储6份,这6份数据组成当前数据段的Protection Groups,存储在三个AZ上

storage volume

存储卷是一系列Protection Groups的组合,存储在大量的存储节点上,对外接口表现为一个使用带有存储的EC2的虚拟主机(我理解的对外看起来就是一个磁盘“卷”,屏蔽了底层多个节点分布式存储的细节),通过不断地增加PG,可以增加存储卷的大小(目前支持到64 TB)

设计思想

如果要在经常出错的分布式环境中构建一套可靠的关系型数据库服务,持久性和数据一致性是我们首先要满足的基本需求。另一方面之舍弃单机/主从架构转向分布式,也是从性能的角度出发,期望兼具分布式系统的高性能和高可用特征。针对上述问题,Amazon Aurora提出了主要三个方面的设计思想

  1. Offloading Redo Processing to Storage:只传输log,不传输数据本身,存储节点在接收到log后,执行操作实现数据变更,从而降低了网络IO负担
  2. Replication and Correlated Failures:使用多副本+quorum机制,保证持久性+多副本下的数据一致性
  3. Segmented Storage:通过对数据划分,实现快速错误恢复

传输日志

Aurora中不同副本存储节点之间数据同步并不直接传输数据,只通过网络传输redo log,由存储节点在接收到日志后,在内存中按照日志操作进行变更,基本流程如下:

  1. 主实例收到写操作,将redo持久化到本地,向6个副本节点发送redo log
  2. 存储节点接收到redo log持久化到本地,在内存中按照日志操作进行变更
  3. 主实例接收到多数派应答后,认为日志被持久化
  4. 通过链式复制(chain replication),两外两个AZ中的从实例进行日志备份同步

存储节点内存的持久化(写入磁盘),会在特定的时机(缓存满等)进行,同样会将多个修改合并为一次磁盘写入。

  • Aurora设计原则上保证后台处理与前台处理负相关(优先满足前台请求,与传统数据库不同),写入磁盘实际上就是后台处理

通过传输日志,相较于上文中的mysql主从复制架构中传输数据,实现了性能35倍的提升(事务处理速度)

The results of our experiment are summarized in Table 1. Over the 30-minute period, Aurora was able to sustain 35 times more transactions than mirrored MySQL.

多副本+quorum

如上图所示,Aurora中每个数据段存储6个副本,每两个副本存储在一个AZ中,多副本在读取和的写入时就涉及到了共识问题

  • 确保写入的数据在下次读取中能够读到;假设写入需要W个节点的确认,读取需要R个节点的确认,当 W + R > N(副本节点数量)时,保证读取一定能够读到之前的写入。
  • Aurora中有6个副本,设定W=4、R=3,即W + R = 7 > 6

通过以上设置,Aurora实现了读和写不同程度的容错

  1. 写操作:最低可保证在一个AZ失效,或者两个不同AZ节点失效时可正常写入
  2. 读操作:保证AZ+1,即一个AZ失效+一个节点失效的情况下可以读到所有写入的数据

数据分块

数据分块主要从错误恢复的角度出发,其中论文定义列两个概念

  1. MTTR(Mean Time to Repair)平均错误修复时间。当一部分数据失效时,我们需要花费一定时间恢复数据,使得副本恢复到失效前状态
  2. MTTF(Mean Time to Failure )平均错误时间。即系统错误出现的时间间隔

论文中认为MTTF时难以改变的,只有通过减少MTTR,使得在错误修复过程中尽量的避免另外的错误发生

  • 数据块一旦失效,需要从其他副本传输获得失效的数据,此时数据大小和网络IO是决定MTTR的主要因素
  • 因此Aurora通过将数据分块,降低数据块的大小,从而降低MTTR

论文中列举的数据是10G的数据块通过10Gbps的网络需要花费十秒完成错误恢复

A 10GB segment can be repaired in 10 seconds on a 10Gbps network link.

如何保证日志持久性和一致性

按照上文中描述qurorum模型,由于不同副本节点log接收的情况不同,可能存在不同存储节点缺失不同数量的log情况,当单个存储节点失效或者数据库系统失效时(即主实例),如何保证日志持久性和一致性,也就相当于保证了数据库的持久性和一致性,不难想到肯定是给log进行编号

  • 数据库系统主实例会为每个事务的log添加一个原子递增的编号:Log Sequence Number (LSN)
  • 主实例维护一个特殊的编号 VDL or the Volume Durable LSN,我理解时当前数据库提交的(已经认为持久化的)最大的LSN
  • 不同副本节点会定时与同一个PG中的其他节点进行通信,根据LSN同步自己缺失的log

当系统重启时,主实例会与存储节点进行通信(qurorum 读),确定当前PG的持久化点(VCL),然后发送命令通知所有的存储节点,截断LSN大于VCL的日志。

单个节点失效重新上线时,需要进行状态同步,即将未执行的log在数据副本的基础上进行重放

  • Aurora则将重放的过程放到了数据存储节点,完全后台化操作。即使故障发生时正在处理100K TPS,也能在10秒内恢复

另外Aurora将事务进行更细粒度的划分-mini transection:

  1. 每个数据库层的事务会被划分为多个 mini 事务,这些事务是有序的,并且被原子地执行
  2. 每个 mini 事务由多个连续的日志记录组成
  3. mini 事务的最后一个日志记录就是一个 CPL (不是所有的mini事务都能提交,只有CPL才能提交)

mini事务+CPL使得每条日志提交,变成了每个事务最后一个mini事务的日志提交(细中带粗)

实现细节

In Aurora, background processing has negative correlation with foreground processing.

正如上文中所描述的,Aurora的设计出发点之一就是要保证前台操作的高吞吐,所有的后台操作的优先级都是低于前台操作,在这个基础上去理解实现细节,稍微有条理一些

写操作

在遵循quorum机制的基础上,添加了以下实现细节

  1. 每当主实例写入log收到存储节点的多数派响应后,增加VDL(相当于记录当前PG的log提交进度)
  2. LSN Allocation Limit (LAL):限定当前分配的LSN不能超过VDL一定的数量(接收的请求不能超过提交过多),以避免写入请求接受速度远远大于数据库写入速度。通过LSN约束,反向降低了接收请求的数量
  3. Segment Complete LSN(SCL):每个数据段只能看到影响自己的log,每个log中包含一个backlink,用来标记当前log在PG中的前一个log。这个反向连接在节点之间相互通信时用来确定自己缺失的log。

在日志写入提交时,并不时满足提交条件即立即提交(asynchronously)

  • 处理当前事务的线程将待提交记录添加到在一个单独的事务队列(COMMIT LSN)中等待被确认提交。
  • VDL 不断的增加,数据库找到哪些事务等待被确认,用一个单独的线程给等待的客户端返回事务完成的确认。
  • 属于是 事务处理线程只管先扔到队列里然后继续处理其他事务,由另一个线程专精队列中事务提交+响应 (~没看明白这么做有什么意义

读操作 + 副本

首先介绍一下传统数据库中的读操作:

  1. 首先寻找缓存中是否存在当前读操作命中的缓存页
  2. 如果没有,需要从磁盘加载到内存中
  3. 如果缓存满了,需要将缓存中的某些页换出,如果换出的页是脏页(dirty page),则需要持久化到磁盘上

上述机制就导致了读操作可能会引发数据库的持久化写操作,这与Aurora的前台操作(读操作)与后台操作(持久化)操作负相关的原则是不符的,理想的方式是:前台读操作不引发缓存持久化,由系统在后台根据前台请求负载周期性(特定条件触发)进行持久化操作。

针对以上问题,Aurora修改了缓存页踢出机制

  • Aurora在缓存踢出是不会进行缓存持久化,而是简单的踢出
  • 只有LSN版本号大于等于VDL的缓存数据页才会被踢出,该性质保证
    1. 所有页面的修改均已持久化在log中
    2. 如果缓存失效,可以通过获取最新页来构造当前VDL 所对应的页面。(难道说是:磁盘加载+日志重放)

Aurora的读取不需要通过quorum机制实现

  1. 当从盘里面读一个页的时候,数据库建一个读取点,代表请求发生时的 VDL
  2. 数据库选择一个带有当前VDL最新修改的数据页节点,返回给读取请求

持久化时机

Aurora不通过缓存踢出持久化,如何实现修改的持久化?实际上还是基于日志的持久化操作

  1. 数据库系统首先从所有未完成的读取记录中,获得读取数据版本的最小LSN,即之后的所有读取都是在LSN操作发生后进行读取的
  2. 通过合并最小LSN之前的日志项并将对应修改的数据页持久化到数据库中,从而实现日志的垃圾回收和修改的持久化

实现架构

  1. 数据引擎是魔改MySQL得到的,论文中说法是:fork of “community” MySQL/InnoDB,支持与MySQL相同级别的写隔离

  2. 每个数据库集群包括一个主读写实例和多个只读实例,不同实例之间通过RDS VPC(Amazon Virtual Private Cloud)通信,通过RDS(Amazon Relational Database Service)管理主从实例

  3. 存储节点部署在 EC2 虚拟机上,通过SSD存储日志和持久化数据,不同存储节点之间通过Storage VPC通信

  4. 并使用Amazon DynamoDB在S3备份存储节点元数据,在节点失效时进行恢复操作

总结

基本上算是理解Aurora的设计思路和实现原理,但是由于数据库知识缺失的比较多,所以很多知识点和设计出发点理解的不是很深入,不是很理解为什么要用某些设计、某个设计为什么能起到效果,等进一步学习数据库知识后,再回看可能会由理解上的进步。

从Aurora的设计思路和设计出发点,我觉得可以总结几条经验(就是说点废话)

  1. 问题来自生产实际,Aurora许多设计思想实际上都是来自于具体的业务需求,有需求才有改进的方向,闭门造车哪来的问题?
  2. 要做分布式还得是再真正的云环境下。每天学学课本,在一两台机器上跑跑实验,是真的很难切实感受到分布式理论中所要解决的内些问题,因为在小吞吐量下,类似问题根本就不会出现,而从未遇到并解决这些问题,又怎么能说自己精通分布式原理?(想表达的意思:工作还是要找大厂,遇到实际问题并解决,才是真的能力提升,小厂连业务量都不够,哪能遇到问题?)
  3. 大的progress往往是修修补补的积累。可以参考Aurora的实现,无非是在amazon已有的技术的基础上,进行拼装修改组合+新思路,才最终实现了一个成熟的系统。研究生阶段很多项目、很多研究上来就是做个什么高大上的东西出来,然而并没有先前的积累我能做出来什么?要么是借学长东风,要么是找现成的方案拼凑,这样能做出来有价值的工作嘛?我觉得并不行。简而言之,就是培养积累的过程缺失,很难做出有价值的工作