大多数人追寻永恒的家园(归宿),少数人追寻永恒的航向。算法
----瓦尔特.本雅明数据库
X-Engine是阿里数据库产品事业部自研的OLTP数据库存储引擎,做为自研数据库POLARDB X的存储引擎,已经普遍应用在阿里集团内部诸多业务系统中,其中包括交易历史库,钉钉历史库等核心应用,为业务大幅缩减了成本,同时也做为双十一大促的关键数据库技术,挺过了数百倍平时流量的冲击。缓存
X-Engine团队撰写的论文"X-Engine: An Optimized Storage Engine for Large-scale E-Commerce Transaction Processing",详细讲述了咱们在数据库存储引擎上所作的原创性工做,今年早些时候已经被SIGMOD'19 Industrial Track接收。SIGMOD是数据库领域最重要也是最有影响力的会议之一,今年6月底即将在荷兰阿姆斯特丹举行,届时,X-Engine团队的程训焘博士将会在会议上作一个分享,欢迎你们前来探讨。数据结构
本文目的是对这篇论文作一个前导性分析,帮助读者更好的理解其中的细节。架构
数据库存储引擎是一个有历史的技术,通过数十年的发展,已经出现不少优秀成熟的产品。各式存储引擎已经在索引组织,缓存管理,事务处理,查询优化方方面面都作过细致的研究。(一个数据库存储引擎应该具有什么样的能力,能够参考我以前的文章),即使如此,这个领域的演进仍在持续,每一年都会涌现不少的新技术。近年来,LSM(Log-Structured Merge-Tree)结构受到愈来愈多的关注,虽然这个技术自己出现不少年了,不算什么新事物,不过早先在KV存储系统中被应用的更多一些,近年开始在数据库存储引擎领域崭露头角,RocksDB便是典型表明。并发
LSM之因此变得流行,一是由于其简单,二是特色鲜明。写入模型是简单的追加,不会更新既有的数据,数据组织为简单的逻辑排序,由此带来的特色是写强而读弱,持久化数据只读的特色便于压缩。可是大多数数据库的应用场景其实都是读多写少的,直接使用LSM结构未必合适,想要另辟蹊径,须得扬长辟短。app
X-Engine使用了LSM做为基础架构,目标是做为一个通用的高性能低成本存储引擎,追求读写性能更为均衡,所以在其上作了大量的改进,主要围绕几个方向进行:1. 利用先天优点,持续优化写性能。2. 优化compaction下降对系统性能的冲击,使得系统性能表现趋于平稳。3. 利用持久化数据层只读特色,发挥压缩优点下降成本。4. 利用自然分层结构,结合硬件能力使用冷热分层结构,下降综合成本。5. 利用精细化访问机制和缓存技术,弥补读性能短板。异步
X-Engine的总体架构以下图,根据数据冷热进行分层代替LSM自己的持久化数据分层,热数据层和数据更新使用内存存储,利用了大量内存数据库的技术(Lock-Free index structure/append only)提升事务处理的性能,设计了一套事务处理流水线处理机制,把事务处理的几个阶段并行起来,提高吞吐。而访问频度低的冷(温)数据逐渐淘汰或是合并到持久化的存储层次中,结合当前丰富的存储设备层次体系(NVM/SSD/HDD)进行存储。咱们对性能影响比较大的compaction过程作了大量优化,主要是拆分数据存储粒度,利用数据更新热点较为集中的特征,尽量的在合并过程当中复用数据,精细化控制LSM的形状,减小I/O和计算代价,并同时极大的减小了合并过程当中的空间放大。同时使用更细粒度的访问控制和缓存机制,优化读的性能。性能
既然X-Engine是以LSM为基础架构的,因此一切还要从LSM自己提及。优化
一条数据在LSM结构中的旅程,从写入WAL(Write Ahead Log)开始,而后进入MemTable,这是Ta整个生命周期的第一处落脚点。随后,flush操做将Ta刻在更稳固的介质上,compaction操做将Ta带往更深远的去处,或是在途中丢弃,取决于Ta的继任者什么时候到来。
LSM的本质是,全部写入操做并不作原地更新,而是以追加的方式写入内存。每次写到必定程度,即冻结为一层(Level),写入持久化存储。全部写入的行,都以主键(Key)排序好后存放,不管是在内存中,仍是持久化存储中。在内存中即为一个排序的内存数据结构(Skiplist, B-Tree, etc.),在持久化存储也做为一个只读的全排序持久化存储结构。
普通的存储系统若要支持事务处理,尤为是ACI,须要加入一个时间维度,借此为每一个事务构造出一个不受并发干扰的独立视域。存储引擎会对每一个事务定序并赋予一个全局单调递增的事务版本号(SN),每一个事务中的记录会存储这个SN以判断独立事务之间的可见性,从而实现事务的隔离机制。
若是LSM存储结构持续写入,不作其余的动做,那么最终会成为以下结构:
注意这里每一层的SN范围标识了事务写入的前后顺序,已经持久化的数据再也不会被修改。每一层数据按Key排序,层与层之间的Key range会交叠。
这种结构对于写入是很是友好的,只要追加到最新的内存表中即完成,为实现crash recovery,只需记录WAL(Redo Log),由于新数据不会覆盖旧版本,追加记录会造成自然的多版本结构。
能够想见,如此累积冻结的持久化层次愈来愈多,会对查询会产生不利的影响,对同一个key不一样事务提交产生的多版本记录会散落在各个层次中,不一样的key也会散落在不一样层次中,读操做诸如顺序扫描便须要查找各个层并合并产生最终结果。
LSM引入了一个compaction的操做解决这个问题,这个操做不断的把相邻层次的数据合并,并写入这个更低层次。而合并的过程实际上就是把要合并的相邻两层(或是多层)数据读出来,按key排序,相同的key若是有多个版本,只保留新(比当前正在执行的活跃事务中最小版本号新)的版本,丢掉旧版本数据,而后写入新的层。能够想见这个操做很是耗费资源。
LSM compaction操做,有几种做用,一是为了丢弃再也不被使用的旧版本数据,二是为了控制LSM层次形状,通常的LSM形状都是层次越低,数据量越大(倍数关系),这样放置的目的主要是为了提高读性能。
通常来说,任何存储系统的数据访问都有局部性,大量的访问都集中在少部分数据上,这也是缓存系统能有效工做的基本前提,在LSM存储结构中,若是咱们把访问频率高的数据尽量放在较高的层次上,保持这部分数据量规模,能够存放在快速存储设备中(好比NVM,DRAM),而把访问频率低的数据放在较低层次中,使用廉价慢速存储设备存储。这就是X-Engine的根据冷热分层概念。
要达到这种效果,核心问题是如何挑选合适的数据合并到更低的层次,这是compaction调度策略首先要解决的问题,根据冷热分层的逻辑,就是优先合并冷数据(访问频率相对低)。识别冷数据有不少方法,对于不一样的业务不尽然相同,对于不少流水型业务(如交易,日志系统),新近写入的数据会有更多的几率被读到,冷热按写入时间顺序便可区分,也有不少应用的访问特征跟写入的时间不必定有关系,这个就要根据实际的访问频率去识别冷数据或是热数据。
除了数据热度之外,挑选合并数据还有其余一些维度,会对读性能产生影响,好比数据的更新频率,大量的多版本数据在查询的时候会浪费更多的I/O和CPU,所以须要优先进行合并以减小记录的版本数量,X-Engine综合考虑了各类策略造成本身的compaction调度机制。
上面是LSM宏观逻辑结构,若是具体来论读写操做和compaction如何进行,就须要探讨每一层的数据组织方式,
每一个LSM变种的实现各不相同。
X-Engine的memtable使用了Locked-free SkipList. 求的是简单,并且并发读写的性能都比较高。固然有更高效的数据结构,或者同时使用多种索引技术。这个部分X-Engine没有作过多优化,缘由在事务处理的逻辑比较复杂,写入内存表尚未成为其瓶颈。
持久化层如何组织更显高效,这就须要讨论每层的细微结构。
简单来讲,X-Engine的每层都划分红固定大小的Extent,存放每一个层次中的数据的一个连续片断(Key Range). 为了快速定位Extent,为每层Extents创建了一套索引(Meta Index),全部这些索引,加上全部的memory tables(active/immutable)一块儿组成了一个元数据树(Metadata Tree),root节点为"Metadata Snapshot", 这个树结构相似于B-Tree,固然不尽相同。
须要注意的是,X-Engine中除了当前的正在写入的active memtable之外,其余结构都是只读的,不会被修改。给定某个时间点, 好比LSN=1000, 上图中的"Metadata Snapshot1"引用到的结构即包含了(LSN=1000)时刻的全部的数据的快照(这也是为何这个结构被称为Snapshot的缘由)。
即使是Metadata结构自己,也是一旦生成就不会修改。全部的读都是以这个"Snapshot"结构为入口,这个是X-Engine实现SI隔离级别的基础。以前讲过随着数据写入,累积数据越多,须要对memtable冻结,flush, 以及层与层的compaction. 这些操做都会修改每层的数据存储结构,全部这些操做,都是用copy-on-write来实现,方法就是每次都将修改(switch/flush/compaction)产生的结果写入新的Extent,而后依次生成新的"Meta Index"结构,乃至新的"Metadata Snapshot",以一次compaction操做为例:
能够看到"Metadata Snapshot 2"相对于"Metadata Snapshot 1"并无太多的变化,仅仅修改了发生变动的一些叶子节点以及索引节点。这个技术很有些相似"B-trees, Shadowing, and Clones",若是你读过那篇论文,会对理解这个过程有所帮助。
得益于LSM轻量化写机制,写入操做当然是其明显的优点,可是事务处理远不仅是把更新的数据写入系统那么简单,这里要保证ACID,涉及到一整套复杂的流程。X-Engine将整个事务处理过程分为两个阶段:读写阶段和提交阶段。读写阶段须要校验事务的写写冲突,读写冲突,判断事务是否能够执行或回滚重试,或是等锁。若是事务冲突校验经过,则把修改的全部数据写入"Transaction Buffer", 提交阶段包括写WAL,写内存表,以及提交并返回给用户结果的整个过程,这里面既有I/O操做(写日志,返回消息),也有CPU操做(拷贝日志,写内存表)。
为了提升事务处理吞吐,系统内会有大量事务并发执行,单个I/O操做比较昂贵,大部分存储引擎会倾向于汇集一批事务一块儿提交,称为"Group Commit",可以合并I/O操做,可是一组事务提交的过程当中,仍是有大量等待过程的,好比写入日志到磁盘过程当中,除了等待落盘无所事事。
X-Engine为了进一步提高事务处理的吞吐,采用了一种流水线的技术:把提交阶段分为四个独立的更细的阶段:拷贝日志到缓冲区(Log Buffer), 日志落盘(Log Flush), 写内存表(Write memtable), 提交返回(Commit)。咱们的事务提交线程到了处理阶段,均可以自由选择执行流水线中任意一个阶段,这样每一个阶段均可以并行起来,只要流水线任务的大小划分得当,就能充分并行起来,流水线处于接近满载状态。另外,利用的是事务处理的线程,而非后台线程,每一个线程在执行的时候,要么选择了流水线中的一个阶段干活,要么逛了一圈发现无事可作,干脆回去接收更多的请求,这里没有等待,也无需切换,充分的调动了每一个线程的能力。
LSM在处理多版本数据的方式是新版本数据记录会追加在老版本数据后面,从物理上看,一条记录不一样的版本可能存放在不一样的层,在查询的时候须要找到合适的版本(根据事务的隔离级别定义的可见性规则),通常查询都是查找最新的数据,老是由新的层次(最新写入)往老的层次方向找。
对于单条记录的查找而言,一旦找到即可终止,若是记录还在比较靠上的层次,好比memtable,很快便返回;若是记录不幸已经落入了很低的层次(多是很随机的读),那就得经历逐层查找的漫漫旅途,也许bloomfilter能够跳过某些层次加快这个旅程,但毕竟仍是有更多的I/O操做。X-Engine针对单记录查询引入了Row Cache,在全部持久化的层次的数据之上作了一个缓存,在memtable中没有命中的单行查询,在Row Cache之中也会被捕获。Row Cache须要保证缓存了全部持久化层次中最新版本的记录,而这个记录是可能发生变化的,好比每次flush将只读的memtable写入持久化层次时,就须要恰当的更新Row Cache中的缓存记录,这个操做比较微妙,须要当心的设计。
范围扫描的操做就没这么幸运了。由于无法肯定一个范围的key在哪一个层次中有数据,也许是每层都有,只能扫描全部的层次作合并以后才能返回最终的结果。X-Engine一样采用了一系列的手段:好比Surf(SIGMOD'18 best paper)提供range scan filter减小扫描层数;还有异步I/O与预取对大范围扫描也有显著的提高。
读操做中最核心的是缓存设计,Row Cache来应付单行查询,Block Cache负责Row Cache miss的漏网之鱼,也用来应付scan;因为LSM的compaction操做会一次大批量更新大量的Data Block,致使Block Cache中大量数据短期内失效,带来性能的急剧抖动。X-Engine一样作了不少的处理:1.减小Compaction的粒度, 2. 减小compaction过程当中改动的数据(见稍后章节) 3. compaction过程当中针对已有的cache数据作定点更新。由此能够基本将cache失效带来的抖动降到最低的水平。
X-Engine中的缓存比较多样,memtable也可算作其中一种。以有限的内存,如何恰当的分配给每一种缓存,才能实现价值最大化,是一个还未被妥善解决的问题,X-Engine也在探索当中。
固然,LSM对读带来的也并不是全是坏处,除了memtable之外的只读的结构,在读取路径上能够作到彻底无锁(memtable也可设计成读无锁)。
compaction操做是比较重的。须要把相邻层次交叉的key range数据读出来,合并,而后写到新的位置。这是为前面简单的写入操做不得不付出的代价。X-Engine为优化这个操做从新设计了存储结构。
如前所述,X-Engine将每一层的数据划分为固定大小的"Extent",一个Extent至关于一个小的完整的SSTable, 存储了一个层次中的一个连续片断,其中又会被进一步划分一个个连续的更小的片断"Data Block",至关于传统数据库中的"Page",只不过是只读的,并且是不定长的。
回看数据组织一节中"合并操做对元数据的改变", 对比"Metadata Snapshot2"和"Metadata Snapshot1"的区别,能够发现Extent的设计意图。是的,每次修改对结构的调整并非所有来过,而是只须要修改少部分有交叠的数据,以及涉及到的"Meta Index"节点。两个"Metadata Snapshot"结构实际上共用了大量的数据结构。这个被称为数据复用技术(Data Reuse),而Extent大小正是影响数据复用率的关键,Extent做为一个完整的被复用的物理结构,须要尽量的小,这样与其余Extent数据交叉点会变少,但又不能很是小,不然须要索引过多,管理成本太大。
X-Engine中compaction的数据复用是很是完全的,假设选取两个相邻层次(Level1, Level2)中的交叉的Key Range所涵盖的Extents进行合并,合并算法会逐行进行扫描,只要发现任意的"物理结构"(包括Data Block和Extent)与其余层中的数据没有交叠,则能够进行复用。只不过,Extent的复用能够修改Meta Index,而Data Block的复用只能拷贝,即使如此也能够节省大量的CPU.
一个典型的数据复用在compaction中的过程能够参考下图:
能够看出,对于数据复用的过程是在逐行迭代的过程当中完成的,不过这种精细的数据复用带来另外一个反作用,即数据的碎片化,因此在实际操做的过程当中也须要根据实际状况进行折中。
数据复用不只给compaction操做自己带来了好处,下降操做过程当中的I/O与CPU消耗,更对系统的综合性能产生了一系列的影响。好比compaction过程当中数据不用彻底重写,大大减小了写入空间放大; 更由于大部分数据保持原样,数据缓存不会由于数据更新而失效,减小合并过程当中因缓存失效带来的读性能抖动。
实际上,优化compaction的过程只是X-Engine工做的一部分,还有更重要的,就是优化compaction调度的策略,选什么样的Extent,定义compaction任务的粒度,执行的优先级,都会对整个系统性能产生影响,惋惜并不存在什么完美的策略,X-Engine积累了一些经验,定义了不少规则,而探索如何合理的调度策略是将来一个重要方向。
X-Engine是阿里云智能事业群-数据库产品事业部的重要核心技术之一,做为兼容MySQL的数据库POLARDB X的存储引擎,以前是在服务阿里集团业务中逐渐打磨成熟,今年下半年,咱们将在阿里云平台上推出MySQL(X-Engine)的RDS公有云服务,为阿里云上的公有云客户提供低成本高性能的数据库服务。
附论文下载地址
本文为云栖社区原创内容,未经容许不得转载。