卫孝贤 刘文欣 蔡鹏
摘要:随着云计算的盛行,用户对云数据库的需求越发复杂,而当下基于共享存储的一写多读的云数据库系统并不能支持写性能的动态扩展,多个主节点同时提供写服务,会引起跨节点的读写冲突,进而导致多主节点缓存不一致,对于这个问题,基于全局有序的事务日志的乐观冲突检测可以检测出跨节点事务冲突,并回滚冲突的事务,维持整个系统的隔离级别与一致性,另外,通过广播和回放全局有序的事务日志,可以将主节点的修改同步到其余节点,保证每个节点的独立服务能力,这一基于事务日志的多主缓存一致性解决方案已实现在开源数据库MySQL上,并通过实验验证了该解决方案对系统性能的影响。
关键词:云数据库:多主缓存:事务日志
中图分类号:TP392 文献标志码:A DOI:10.3969/j/i8811.1000-5641.202091002
0引言
时至今日,随着大数据技术的发展,涌现出了多种多样的新型数据库,但是传统的关系型数据库仍然占据着主导地位,这是因为关系型数据库采用了sQL(structured Query Language)标准,这种高级的非过程化的数据库编程语言将编程逻辑和关系型数据的管理方式完美地衔接起来,其普适性目前仍然难以超越。
如果说关系型数据库是IT时代的产物,那么互联网时代的产物就是云计算,在蓬勃发展的云计算2.0时代,关系型数据库在云托管的环境下存在一些问题:①传统数据库的I(Input)/O(Output)“瓶颈”在云场景下发生了变化,在多租户场景下I/O分布到多个节点和多个磁盘,单个磁盘的I/O压力不会太大,性能的“瓶颈”转移到了数据包的发送速度和网络带宽上;②事务的提交是另一个问题,例如某个客户端提交的事务中包含了数据A的修改,要想将A的修改同步到其余的服务节点,让其余的客户端可以读到A的值,这需要多阶段的同步协议,但这些协议会带来较高的事务延迟;③多节点云数据库的故障恢复与传统的关系型数据库的故障恢复也是有区别的,如何恢复整个系统的状态或是故障后切换到没有故障的节点需要另外设计一套解决方案。
作为云计算时代的先行者,亚马逊在2014年发布了云托管的关系型数据库Aurora,并在2017年发布了论文Amazon aurora:Design considerations for high throughput cloud native relationaldatabases,解释了基于云环境的关系型数据库应该如何设计,2018年,阿里巴巴紧随其后发布了商用云数据库PolarDB,随后,2019年微软发布了云数据库Socrates,2020年华为发布了云数据库Taurus,这些商业云数据库无一例外地遵循了存储计算分离的原则,存储计算分离是将关系型数据库的逻辑分为存储和计算两个部分:存储部分负责数据的持久化;计算部分负责SQL的解析、事务处理等计算逻辑,存储部分由多个存储节点共同组成的共享存储系统实现,而计算节点则提供独立的数据库请求服务能力,在这一架构下,云数据库可以通过增减存储节点或者计算节点,动态地扩展系统的存储能力或计算能力,实现云场景下的按需分配、动态扩展。
到目前为止,除了Aurora之外,所有的商用云数据库只提供了一写多读的计算节点配置,即计算节点中只有一个节点可以执行写操作,其余的节点只提供读服务,这种配置的云数据库集群只能提供读性能的扩展,而整个系统的写性能无法通过增减节点进行扩展。
云场景下,用户的需求是复杂且难以预测的,仅仅是读性能的扩展并不能满足用户的所有需求,为了实现写性能的扩展,云数据库系统需要增减具有写能力的计算节点,由于系统的存储是众多计算节点共享的,而每个计算节点有自己的缓存,写操作首先作用于缓存,缓存满时换出到共享存储中,每个计算节点的缓存都包含该节点近一段时间内读写的数据,不同节点的缓存中可能包含相同的数据,另外,每个计算节点都独立地提供读写服务,因此,在多主的场景下,不同主节点的缓存中相同数据可能会出现版本不一致的现象,这些不同版本的数据副本是不可控的,在换出到共享存储时会产生读写冲突。
现今,主流的基于共享存储的多主解决方案有两种——Oracle RAC和DB2 pureScale,这两种方案在实现方式上有所不同,但思路都是通过多节点协调的远程物理页锁保证页的写权限只会被一个节点拥有,因为多节点的读写会产生锁冲突,拥有写权限的节点完成写操作后,将新版本的页在集群中传输,更新其余节点缓存中页的副本,这一方案会带来较多的网络通信用于申请锁和传输页,另外,跨节点死锁的处理也较为复杂,需要多次的网络通信,频繁的网络通信和粗粒度的锁会限制整个系统的擴展性和吞吐量。
综上所述,为了在共享存储的云数据库系统中提供可动态扩展的写性能,解决多主场景下缓存不一致的问题,本文基于MySQL设计并实现了多主缓存一致性维护插件,本文的主要贡献如下,
(1)基于redo日志生成事务日志,并通过广播和筛选回放事务日志更新其他节点的缓存,设计并实现了异步日志广播机制,减少了事务延时中的网络等待,
(2)通过全局有序的事务日志检测跨节点的事务冲突,维护了整个系统的数据一致性。
(3)通过实验论证了Paxos提议和切片ID这两种全局事务ID分配方式对多主数据库系统性能的影响。
本文后续安排如下:第1章描述基于MySQL的多主数据库架构;第2章描述事务日志的生成方式和优缺点;第3章介绍异步日志广播和两种全局事务ID分配方式,以及基于全局有序事务日志的冲突检测方法;第4章通过实验验证本文方案对系统性能的影响;第5章总结全文。
1基于MySQL的多主数据库架构
为了减少应用从传统关系型数据库向云数据库迁移的代价,当下的云数据库大多基于MySQL开发,保证了与MySQL客户端的兼容,如阿里巴巴的PolarDB,其计算节点是MySQL的服务层与部分存储引擎,而存储层则是自主研发的分布式文件系统PolarFS,通过远程文件I/O接口将日志和数据持久化到分布式存储上。
仿照PolarDB的架构,本文设计了如图1所示的多主数据库原型:多个MySQL进程组成计算节点集群,这些计算节点将日志和数据写到共享的远程存储上,以此组成基于共享存储的多主数据库架构,
由图1可知,每个计算节点都可以独立地提供读写能力,并通过多主插件协调不同节点的事务,以维护各节点缓存中的缓冲池一致性,多主插件对于MySQL本身逻辑的影响较小,可以较便捷地变更实现方式,以此验证多种解决方案对于系统的影响,另外,MySQL成熟的事务机制和缓冲池机制为解决方案的设计提供了重要的研究背景参考,提高了解决方案的实用性,
2事务日志的生成
2.1全局事务日志的必要性
为了维护多主场景下的缓存一致性,本文设计了基于日志的解决方案,然而,在MySQL中存在着多种类型的日志,其中最为重要的就是binlog和redo log,这两种日志维护了MySQL事务的持久性,binlog有更好的通用性,而redo log有更好的性能,但是在多主场景下,这两种日志并不能符合跨节点冲突检测和日志同步的需求,因此,本文基于性能更好的redo log生成了全局事务日志,用于维护多主缓存的一致性。
通常,数据库的日志会被分为逻辑日志与物理日志:逻辑日志记录的是数据库的逻辑操作;而物理日志记录的是数据库的物理更改,在MySQL中,逻辑日志的体现是binlog,物理日志的体现是redolog。
MySQL的binlog是一种独立于存储引擎的日志,由MySQL服务层管理,主要应用于数据库的主从复制和故障恢复,binlog中记录了每一个数据库操作(Insert、Update、Delete,不包括Select),相比于redo log,binlog有更好的可读性,通过解析binlog可以在不同的存储引擎或异构数据库中回放数据,binlog的缺点是存在性能上的缺陷:①binlog记录的是逻辑操作,回放过程中解析日志的代价更高;②开启binlog时,MySQL需要确认binlog和redo log两份日志都成功刷盘才能提交事务,这会影响到事务的响应时间和吞吐量,
redo log尽管没有binlog那么高的通用性,但是却有着性能上的优势,在跨节点场景下直接通过redo log检测冲突时,则会遇到一些问题:①redo log中仅能检测出对于页的更新,并无事务的信息,尽管可以检测出页级别的并行寫冲突,却无法判断是否存在事务级别冲突,可能会使本无冲突的事务回滚;②由于无法确定事务对应的所有日志记录,即使在redo log中检测出了冲突,应该回滚的事务日志依然需要回放,在缓冲池中回放出事务的数据页和undo页后,才可以执行事务的回滚,这无疑是多余的回放。
若是日志能以事务为单位组织,将会为冲突检测和日志回放带来极大的便利。
2.2全局事务日志生成
InnoDB存储引擎的redo log对于日志记录来说是有序的,但是对于事务来说是无序的,由于日志记录中并不包含事务信息,无法通过扫描和解析redo log直接获取到事务对应的所有日志记录,事务提交时也仅仅会记录最后一条日志记录的日志序列号,来确保之前产生的日志记录已经刷盘,因此,本文设计了如图2所示的全局事务日志采集模块。
MySQL默认的事务模型是“一连接一线程”,即MySQL接收到连接请求时,会分配一个线程专门服务该连接,接收该连接发来的SQL语句,并在解析后执行,该线程接收到事务开始请求后,会启动一个事务,事务提交后清空事务对象的状态,为接收下一个事务做准备,事务的执行在单个连接上是串行的,因此,本文可以通过线程ID唯一的标识正在执行的事务,并据此收集以事务为单位产生的日志记录,整个流程如下。
(1)当事务开始时,记录下执行事务的线程ID,并在线程ID-事务日志映射表上创建新的日志链表,标志着该线程已启动一个事务。
(2)迷你事务执行读写操作后会产生相应的日志记录,此时触发日志生成事件,在日志被存入日志栈之前收集日志记录和线程ID,在线程ID-事务日志映射表中找到对应线程的日志链表,将日志记录插入链表。
(3)当事务提交时,检查当前线程对应的日志链表,若为空,则说明该事务属于只读事务,
事务日志可以回避原生redo log在跨节点冲突检测场景的问题:①日志以为事务为单位组织起来,执行冲突检测时可以检测出事务之间的冲突,避免误判;②当判定某一事务由于冲突应该回滚时,可以丢弃其对应的日志,不需要再做无效的回放和传输,加快了日志回放和传输的速度。
2.3日志的提前解析
一般来说,MySQL日志回放的过程是,先扫描日志文件,解析每个日志块的头部信息,进而在扫描分析出每条日志记录的偏移量,解析每条日志记录头部记录的页号后,将其按照页号存放到一个线程ID-事务日志映射表中,之后再从线程ID-事务日志映射表中以页为单位提取对应的日志记录链表,再次解析日志记录头部,解析出日志的类型和一些辅助信息后,传人对应的函数中执行回放。
多主日志同步的场景下,将内存中的日志记录传输到多个副本后,每个副本都需要经过两次解析才能获取到真正需要回放的内容,从系统的角度来看,这就产生了重复的解析,增加了整个系统的CPU占用,影响了日志回放的速度。
因此,应该对日志进行提前解析,在不增加日志记录占用空间的情况下,将日志记录中的有效信息提取出来,通过解析头部信息,可以较为简单地得出日志类型、页号、页中的偏移量和实际修改内容,部分在回放中没有意义的特殊类型的日志记录可以在提前解析完成后舍弃。
日志的提前解析可以减少同步日志时的网络传输数据量,减少整个系统的资源消耗,提高日志回放速度,在一写多读场景下,日志的提前解析会给唯一的主节点带来额外的负担,从而降低整个系统的写性能,但在多主场景下,日志由多个主节点分别产生,提前解析的压力由多个主节点共同负担,故对系统的性能影响较小。
3基于全局事务日志的冲突检测
3.1异步日志广播
事务日志生成之后,需要将其广播到集群中的其余节点,通过回放事务日志中与缓存内容有关的日志记录,更新其余节点的缓存内容,这一过程与MySQL的主从复制、组复制思路相似:通过日志更新数据库内容,实现多副本数据一致,MySQL的组复制采用了同步日志广播的形式,在主节点提交事务时将binlog经过一次Paxos提议确定全局顺序,并确保binlog被大多数节点收到,收到binlog的节点依据之前收到的全局有序日志判断是否存在跨节点冲突,然后将检测结果返回给主节点,主节点收到大多数节点无冲突的回复后才会提交本地事务。
在跨节点冲突检测的场景下,日志的生命周期并不需要太长,冲突检测只会利用到从事务启动到提交前这段时间提交的事务日志,因为仅有这段时间提交的事务会改变数据库的状态,可能与执行冲突检测的事务产生冲突,另外,由于所有节点的全局事务日志副本都是一致的,跨节点冲突检测在所有节点执行的结果都是一致的,因此,没有必要所有节点都执行冲突检测,当确定本地事务在全局事务序列中的位置后,只需要结合之前的事务日志执行冲突检测,即可确定事务是否可以提交,事务提交后,日志可以通过异步的方式发送到其余节点,而不用等待日志成功发送到大多数节点后再提交事务,同步日志广播与异步日志广播的时间轴分析如图3所示。
异步日志广播可以减少事务中的网络等待时延,避免由于网络阻塞带来的事务提交长等待,另外,异步的日志广播方案使得更新自身缓存的时机与本地事务的提交无关,缓存更新的延迟会导致其余节点读取的数据版本较旧,但这在分布式系统中是允许的。
3.2全局事务ID分配
前面多有提到“全局事务日志”的概念,但是目前为止,本文只讨论了事务日志的生成和广播,所有节点都拥有全局事务日志的副本,前提条件是所有节点对于任意一事务日志在这一全局事务序列中的顺序达成共识,实现全局有序,最直接的方法就是为每一个事务分配一个全局的事务ID,这个ID标识了事务在全局事务序列中的顺序,只要所有节点对该ID的分配达成共识,即可保证全局事务序列的一致。
目标是共识,那么首先想到的就是共识协议Paxos或raft,本文在3.1节中也有提到过,组复制通过将binlog打包发送到一个Paxos组中来保证消息的全局有序,這种基于Paxos提议的全局有序协调机制可以保证系统的高可用,是一种简单有效的方案,落实到多主场景中,基于Paxos提议的全局事务ID分配机制的实现方式如图4所示,其使用了微信开源的PhxPxaos库。
尽管简单有效,但基于Paxos提议的全局事务ID分配机制在多主场景下还是存在一些问题:①每次事务提交都需要经过一次Paxos提议才能确定全局事务ID,事务需要至少等待一次所有节点参与的协调结束后才能提交,网络等待的时间过长;②高并发的数据库会同时执行多个事务,事务提交时会发起Paxos提议,以线程为单位的Paxos提议发送会引起较为剧烈的提议冲突,协调提议冲突的网络代价会降低整个系统的吞吐量;③Paxos协议适用于3节点到5节点的场景,随着参与节点数量的增加,系统的吞吐量会急剧下降,无法支持良好的扩展。
为了保证冲突检测的有效性和日志回放的有序性,事务在提交前需要确认自己拥有的全局事务ID,高并发环境下单节点上会有多个事务同时提交,以单个事务为单位申请全局事务ID并不是一个好的选择,会导致频繁地发出申请或是单节点上的多个申请排队,等待所有线程上的事务都进入提交状态后,再去做批量申请的操作,又可能会由于某一个或几个线程上的长事务长时间执行而导致整个节点的提交阻塞,更为糟糕的是,无法完成提交的事务不会释放本地的行锁,而其余线程上的事务可能在等待被持有的行锁而无法进入提交状态,这就导致了额外的死锁。
比较好的选择是提前为节点划分一些事务ID,在事务提交时将这些ID分配给事务,使得事务可以快速提交,而不会影响到同一节点其余线程上事务的提交。
为了减少事务提交过程中的网络等待,本文提出了基于切片的全局事务ID分配机制,如图5所示,全局事务ID是连续而单调递增的自然数序列,但逻辑上可以划分为多个切片,每个切片是一组与节点数量等量的全局事务ID集合,在切片中,按照节点的编号顺序将ID分配给对应的节点,事务提交时,节点可通过简单的计算将切片ID和局部ID的组合转换成可用的全局事务ID,之后即可执行本地提交,广播事务日志更新其余节点的缓存,当节点增减时,如同图5中的节点3.需要将这一节点的变更信息发送到所有节点上,并收到确认回复,才能保证所有节点后续的切片中会增加新节点需要的槽位,
基于切片的全局事务ID分配机制不需要在提交时等待网络传输,只要经过本地的计算即可获取全局有序的事务ID,相比基于Paxos提议的分配方式有着更好的扩展性和更低的事务时延。
3.3冲突检测
在每个节点都拥有一份全局事务日志的副本之后,还需要解决的一个问题是跨节点的事务冲突处理,每个节点都有独立的服务读写请求的能力,本地多个线程执行的事务由MySQL的事务锁机制解决冲突,跨节点的事务由于不共享事务模块而缺少处理冲突的手段,全局事务日志基于redo log生成,其中包含了对于数据页的修改,分配了全局事务ID的事务日志形成的全局事务日志包含了所有节点上所有事务的提交顺序和数据修改操作,通过扫描这一日志可以检测出跨节点事务之间的写冲突,但这样是不够的,仅检测跨节点事务的写冲突,可能会破坏节点本地的事务隔离级别,
所以,在事务执行的过程中除了读写操作生成的日志记录,还需要收集读写操作之前的数据版本,通过数据版本形成的读集合与日志记录组成的写集合可以检测出跨节点事务的读写冲突,冲突检测的方案参考了分布式数据库中的乐观并发控制方法。
算法1是基于事务日志的跨节点冲突检测算法,其中的输入certification info是一个以页号和页中行偏移量为索引组织的版本集合,版本通过全局事务ID描述,验证数据库记录了全局已提交事务对于页中行的修改,当前事务的写集合是基于事务日志解析出的事务修改的行集合,而快照版本集合是事务执行读写操作前记录的页号、行偏移量和版本号。
4实验
4.1实验环境
本文基于开源数据库MySQL实现了多主插件,实验服务器配置信息:Intel(R)Xeon(R)Silver 4 110CPU,168 GB内存;10 GB/8网卡;CentOS Linux release 7.7.1908(core)操作系统;实验选用TPC-c负载,仓库数量为50.每个计算节点有16个客户端连接,每个计算节点都作为主节点执行写事务。
4.2全局事务ID分配性能测试
第一组实验是,多个节点执行TPC-C写负载,测试不同的ID分配方式对于系统吞吐量的影响,如表1所示,表1中,纵坐标是参与多主集群的节点数量,横坐标是整个事务的吞吐量,用每秒钟传输的request/事务数量(Transactions Per Second,TPS)表示,单个写节点时,系统不需要网络传输,本地生成的连续ID即可作为全局ID使用,此时两种分配方式的吞吐量相同,这一数据可以作为扩展性的参考。
基于Paxos提议的全局ID分配参考MySQL的组复制机制实现,从表1可以看出,3个节点时系统的吞吐量是单节点的1.65倍左右,扩展到3个节点时系统性能较低,但还在可接受的范围;而扩展到5个节点时,系统的吞吐量下降至单节点的9.38%,这是不可接受的性能衰减,基于切片的全局ID分配按照预先分配的切片单位计算全局ID,无论是3个节点还是5个节点,都可以保持接近线性扩展的扩展性,3个节点时吞吐量是单节点的3倍,5个节点时接近单节点的5倍。
4.3事务性能测试
从表1可以看出基于Paxos提议的全局ID分配扩展性十分糟糕,第二组实验的目的是探究产生这一现象的原因,表2是不同的分配方式单次分配全局ID的平均分配时延,从表2可以看到,基于切片的全局ID分配方式表现十分稳定,无论是3个节点还是5个节点,单次分配只需要0.16us左右,对于事务执行时延的影响较小,而基于Paxos提议的分配方式由于需要网络沟通达成共识,随着节点数量的增加,单次提议的时延会越来越高,单个节点时并不需要通过Paxos提議生成ID,而是通过本地计算直接生成有序ID,故此时基于Pxaos提议的分配方案平均时延为0.在无冲突的情况下,5个节点单次提议所需的平均时延是3个节点的381倍,当发生冲突时,5个节点单次提议的平均时延是3个节点的1.4倍,看上去似乎扩展性的表现好一些,但实际并不是这样,3个节点的情况下,发生冲突的提议平均时延是无冲突时延的900倍;5个节点时,冲突提议的平均时延是无冲突时延的3.4倍,产生这一现象的原因是,随着节点数量的增加,达成共识需要的网络代价越来越高,单位时间里可以发出的提议数量更少,产生的冲突也会更少;而3个节点时完成一次成功的提议可能需要经过多次连续的冲突,无论如何,可以看出冲突对于提议的平均时延影响极大。
表3是两种分配方式中单次事务执行的平均时延,从表3可以看出,基于切片分配ID的事务执行时延比较稳定,随着节点数量的增加,事务时延的变化很小,基于Paxos提议分配ID的事务随着节点数量的增加,由于网络代价的增加和冲突的影响,事务的时延变化很大,3个节点事务平均时延是单节点的1.88倍,5个节点事务平均时延是3个节点的29.75倍。
5结语
随着云计算的发展,用户对于云数据库的需求越来越复杂,当下一写多读的商业云数据库产品并不能满足用户对于写性能动态扩展的需求,在存储计算分离的架构下,动态扩展写性能需要支持多个计算节点同时执行写操作,并且不破坏多个计算节点缓存中数据的一致性,本文基于开源数据库MysQL设计了一种基于全局事务日志同步的缓存一致性维护机制,通过收集和提前解析redo日志生成以事务为单位的日志,为事务日志申请一个全局有序且连续的事务ID,以此全局有序的事务日志为基础进行跨节点事务的冲突检测,回滚冲突的事务,以此维护多主缓存的一致性,通过实验证明,基于Paxos提议的全局事务ID分配扩展性较差,而切片ID的方案在理想情况下可达到线性扩展。