在这一节中,我们介绍了数据库引擎是如何产生日志的,这样可持久化状态、运行时状态、以及复制状态永远是一致的。重点讲述了如何不通过复杂的2PC协议来高效地实现一致性。首先,我们展示如何在故障恢复的时候,避免使用昂贵的REDO日志重放。其次,我们介绍了一些常规操作,以及我们能如何保持运行时和复制状态。最后,我们介绍故障恢复过程的细节。
由于我们将数据库建模为日志流,这样日志的不断滚动的过程可以看作一连串顺序的变更。在实现上,每一条日志记录都有一个由数据库产生单调递增的日志编号LSN。
这让我们可以使用异步的思路简化用来维护状态的一致性协议,而不是使用2PC这种沟通复杂且对错误容忍度低的协议。从上层来看,我们维护一致性和可持久性的状态点,并随着我们收到发出去请求的确认消息,不断地推进这些点。由于任何单个的存储节点都有可能丢失一个或者多个日志记录,这些节点与节点相互之间交流,找到并填补丢失的信息。数据库维护的运行时状态可以让我们用单个数据段的读来代替大多数读,除非是在故障恢复的时候,状态丢失了必须通过重建。
数据库可能同时发起了多个独立的事务,这些事务完成的顺序与发起的顺序是不一致的。假如这时数据库崩溃了重启,每个事务决定是否需要回滚是相互独立的。跟踪未完成的时候并回滚的逻辑还是在数据库引擎中完成,就如同它在写单个盘一样。不过,在数据库重启的时候,在它被允许访问存储之前,存储服务需要进行自己的故障恢复流程,然而重点不在用户级的事务上,而是确保数据库能看到存储的一致性视图,尽管存储本身是分布式的。
存储服务首先确定VCL(Volume Complete LSN),能确保之前日志记录都可用最大的LSN。在存储恢复的过程中,大于VCL的日志就都必须被截断。数据库可以通过找到CPL(Consistency Point LSN),并使用这些点进行进一步的日志截断。这样我们可以定义VDL(Volume Durable LSN)为所有副本中最大的CPL,CPL必须小于或者等VCL,所有大于VDL的日志记录都可以被截断丢掉。举个例子,即使我们有到LSN 1007的完整数据,数据库发现只有900、1000和1100是CPL点,那么,我们必须在1000处截断。我们有到1007的完整数据,不过我们只有到1000的可持久性。
因而,完整性和可持久性是不同的。一个CPL可以看作描述带某种形式限制的存储系统事务,这些事务本身必须按序确认。如果客户端认为这些区分没用,它可以将每个日志记录看作一个CPL。在实现中,数据库和存储必须如下交互:
每个数据库层的事务会被划分为多个mini事务,这些事务是有序的,并且被原子的执行
每个mini事务由多个连续的日志记录组成
mini事务的最后一个日志记录就是一个CPL
在故障恢复的时候,数据库告诉存储服务建立每个PG的可持久化点,并使用这些来确认VDL,然后发送命令截断所有大于VDL的日志记录。
我们现在介绍数据库的常规操作,重点依次介绍写,读,事务提交,副本。
4.2.1 写
在Aurora中,数据库不断的与存储服务交互,维护状态来保持大多数派,持久化日志记录,并将事务标记为已提交。比如,在正常/前台路径中,如果数据库收到写大多数派的写确认回复,它会将VDL往前推进。在任意一个时间点,数据库中都会存在着大量并发的事务,每个事务产生自己的REDO日志。数据库为每个记录分配一个唯一有序的LSN,这些LSN不能大于VDL加上LAL(LSN Allocation Limit)(目前被设为10m)。这个限制保证数据库不会领先存储服务太多,以至于导致后台处理的压力过大(如网络或者存储跟不上)阻塞写请求。
注意到每个PG中的每个数据段只会看到整体的一部分日志记录。每个日志记录含有一个反向的指针指向这个PG中的前一个日志记录。这些反向指针可以用来追踪每个数据段的完整性点,来确认SCL(Segment Complete LSN),SCL是PG收到的连续日志的最大LSN值。SCL被数据库节点用来与其他节点交流,找到缺失的日志记录并添补它们。
4.2.2 提交
在Aurora中,事务的提交是异步完成的。当客户端提交一个事务,处理这个提交请求的线程将事务放在一边,并将COMMIT LSN记录在一个单独的事务队列中等待被确认提交,然后就去做其他事情了。这等同于实现了WAL协议:确认一个事务提交完成了,当且仅当最新的VDL大于或者等于这个事务的COMMIT LSN。当VDL不断的增加,数据库找到哪些事务等待被确认,用一个单独的线程给等待的客户端返回事务完成的确认。Worker线程不会等待事务提交完成,它们会继续处理等待着的请求。
4.2.3 读
在Aurora中,与大多数数据库一样,数据页是从buffer cache中读取,只有在被请求的页不在cache中时,才会发起一次存储IO请求。
如果buffer cache满了,系统会找到一个页并将其踢出缓存。在传统的数据库中,如果这个被踢出的页是脏页,它在被替换之前会被刷新到数据盘中。这是为了保证接下来读取的数据页永远是最新的数据。不过Aurora在踢出页的时候不会写磁盘,它提供了一个类似的保证:buffer cache中的数据页永远是最新的数据。这个保证通过踢出page LSN(数据页上应用的最新的日志记录的LSN)大于或者等于VDL的数据页来实现。这个协议确保:(a)所有对数据页的变更都已经持久化在日志中了,(b)如果缓存失效,可以通过获取最新页来构造当前VDL所对应的页面。
数据库在通常情况下都不要通过多数派读来获得一致性。当从盘里面读一个页的时候,数据库建一个读取点,代表请求发生时的VDL。数据库可以选择一个对这个读取点是完整的存储节点,这样读取的数据肯定是最新的版本。从存储节点返回的数据页必须与数据库中mini事务的语义一致。由于数据库直接将日志记录发送给存储节点,并跟进日志处理的进程(也就是,每个数据段的SCL),通常它知道哪些数据段是可以满足一个读请求的(SCL大于读取点的数据段),因而可以直接将请求发送给有足够数据的数据段。
考虑到数据库记录了所有的当前读操作,因而可以计算出在任意时间点每个PG的最小读取点LSN。如果有读副本,写副本会与它们沟通获取所有存储节点上每个PG的最小读取点LSN。这个值称作PG最小读取点LSN(PGMRPL),代表低水位点。在此以下的PG的所有的日志记录都是不必要的。换句话说,存储节点中数据段确认不会再有读取请求的读请求点小于PGMRPL。每个存储节点都能通过数据库获取到这个值,并且能合并老的日志记录并继续生产新的数据页,然后放心的将这些日志记录回收掉。
跟传统的MySQL数据库一样,实际的并发控制协议在数据库引擎中执行,就像数据页和UNDO段在本地存储一般。
4.2.4 副本
在Aurora中,一个写副本和多至15个读副本可以挂载同一个共享的存储空间。因而,读副本不会增加任何的存储和写开销。为了减少延时,写副本产生的日志流发送到存储节点的同时,也会发送到所有的读副本。在读副本中,数据库会依次消费每一个日志记录。如果日志记录指向的是一个buffer cache中存在的页,它就用log applicator应用日志的变更到数据页上。否则的话,它就直接丢掉这条日志。注意,从写副本的角度来看,读副本是异步的消费这些日志,而写副本确认用户事务的完成是与读副本无关的。读副本在应用这些日志的时候遵守两条重要的规则:(a)只有SDL小于或者等于VDL的日志记录会被应用,(b)一个mini事务中的日志记录会原子的被应用,确保副本可以看到所有数据库对象的一致性视图。在实际中,每个读副本滞后写副本一小段时间(20ms或以内)。
大多数传统的数据库使用类似ARIES的恢复协议,这些协议依赖可以代表所有已提交事务的WAL。这些系统也会粗粒度的为数据库周期性的,通过刷新脏页和将检查点写入日志,来建立检查点。在重启的时候,一个数据页可能丢失一些已经提交的数据,或者包含未提交的数据。因而,在故障恢复的时候,系统重放自上一个检查点其的所有REDO日志到相关的数据页。这个过程将数据库的页重新置为在故障发生那个时间点的一致性状态,之后通过执行相关的UNDO日志可以将正在执行的事务回滚。故障恢复是一个代价昂贵的操作。降低建立检查点的时间间隔会有所帮助,不过是以干扰前台事务为代价的。在Aurora中不需要做这样的折中。
传统数据库的一个简化规则是,在前台处理和故障恢复同步使用的REDO日志applicator,也会在数据库离线在后台服务中使用。在Aurora中,我们也依赖于同样的规则,只不过这里REDO applicator是与数据库解耦的,一直并行的在后台运行在存储节点上。当数据库启动的时候,它会与存储服务协助进行数据恢复,因而Aurora数据库可以恢复非常快(通常在10s以内),即使在崩溃的时候正在执行100K TPS的写入。
数据库在故障重启的时候仍然需要重建运行时状态。在这种情况下,数据库连接每一个PG,数据段的读多数派如果能确认发现其他数据,也可以形成一个写多数派。一旦数据库为每一个PG建立了读多数派,它可以计算出REDO可以截断的范围,这个范围是新的VDL到当前数据库已经分配的最大的LSN。数据库能推导出这个上限值,是因为它分配LSN,并且限制了最大的LSN为VDL+LAL(之前已经介绍过的,值为10m)。这些截断范围是用时间戳来标记的并且写到存储服务中,因而在截断的时候并没有任何歧义,即使恢复过程被打断或者重启。
数据库仍然需要执行UNDO恢复来回滚在故障时间点正在进行的事务。不过,UNDO恢复可以在系统启动后通过UNDO段来获取正在进行的事务之后再进行。
在这一节中,我们从整体来描述构成Aurora的组件,如图5所示。
数据库引擎是社区版MySQL/InnoDB的分支,主要区别是InnoDB如何从数据盘读取或者写入数据。在社区版InnoDB中,一个写操作的执行包括:数据页在buffer中被修改,REDO日志按LSN有序写入到WAL的buffer中。在事务提交的时候,WAL协议只要求事务的REDO日志写入到数据盘。真正被修改的数据页最终会写入数据盘,这里使用了双写技术来避免数据写盘不完整。这些数据页的写入可能发生在后台,可能由于cache的踢出,也有可能由于建立检查点。除了IO子系统之外,InnoDB还有事务子系统,通过B+树和mini事务实现的锁管理器。Mini事务是只在InnoDB中使用的结构,描述的是一组必须原子执行的操作(比如,分裂或者合并B+树的页)。
在Aurora版本的InnoDB中,每个Mini事务中的REDO日志会按所属的PG分组打包,然后批量写入存储服务中。每个Mini事务的最后一个日志记录被标记为一个一致性点。Aurora写副本支持社区版MySQL相同的隔离级别。Aurora的读副本会不断的从写副本中获取事务开始和提交的信息,并使用这些信息来支持本地只读事务的快照隔离级别。注意到并发控制完全在数据库引擎中实现,不会影响存储服务。存储服务为数据提供一个一致性的视图,在逻辑上等价于社区版InnoDB写数据到本地存储。
Aurora使用Amazon RDS来作为它的控制面板。RDS在数据实例上部署Agent来监控集群的健康状况,是否需要做故障切换,或者实例是否应该被替换掉。每个数据库集群包括一个写副本,0个或者多个度副本。集群中所有的实例都在一个地理上的区域(Region)中,通常会位于不同的可用区,连接到相同区域里面的存储服务。为安全性考虑,我们隔离了数据库,应用以及存储之间的通信。在实际中,每个数据库实例可以与三个Amazon虚拟网络VPC通信:用户应用与数据库引擎交互的用户VPC,数据库引擎与RDS控制面板交互的RDS VPC,数据库与存储服务交互的存储VPC。
存储服务部署在一个EC2虚拟机集群上,集群最少会跨同一个Region的三个可用区AZ,共同为多个用户提供存储,读取或者写入数据,备份或者恢复用户数据。存储节点操作本地的SSD盘,与数据库实例、其他存储节点、备份/恢复服务交互,持续地将数据备份到S3或者从S3恢复数据。存储服务的控制面板用Amazon DynamoDB作为持久存储,存放数据库容量配置、元数据以及备份到S3上的数据的详细信息。为了支持长时间的操作,比如由故障导致的数据库恢复或者复制操作,存储服务的控制面板使用Amazon Simple Workflow Service SWF。为了保证高质量的可用性,需要在用户发现之前积极的、自动的监控和探测真实的和潜在的问题。所有存储服务的关键操作都被持续的监控起来,如果发现性能或者可用性方面的潜在问题会及时告警。
本文系转载,如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,如有侵权,请联系 cloudcommunity@tencent.com 删除。