事务处理
得益于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
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生态体系下大数据体量通用存储引擎,我们还在持续优化存储结构,压缩算法,读写性能,稳定性,以期达到最好的性价比。X-Engine仍然有他最擅长的方向,可以作为在线历史数据一体化的数据库,在不损失读写性能的情况下充分压缩数据表,根据我们的测试结果,在标准TPC-C测试场景下,X-Engine的tpmC性能与InnoDB基本持平。如果你的应用使用MySQL数据库,有大量写入,希望降低存储成本,并且有一定查询需求的应用,例如日志、消息归档,订单流水存储等等,便非常适合使用X-Engine.
X-Engine不只是一个为研究设计的系统,从一开始就是为了实现用户价值,在阿里集团内部大规模使用已经有两年多,并且在最为核心的交易,钉钉消息历史库上都全面替代了原有的系统,达到了预期中的良好效果,为交易历史库(原来使用HBase)节省33%成本,为钉钉消息历史库(原来使用MySQL with InnoDB)节省了60%的成本。
后续方向
作为MySQL的存储引擎,持续的提升MySQL系统的兼容能力是一个重要目标,后续会根据需求的迫切程度逐步加强原本取消的一些功能,比如外键,对一些数据结构,索引类型的支持。
X-Engine作为存储引擎,核心的价值还在于性价比,持续提升性能降低成本,是一个长期的根本目标,X-Engine还在compaction调度,缓存管理与优化,数据压缩,事务处理等方向上一直进行深层次的探索。
X-Engine不仅仅局限在一个单机的数据库存储引擎,未来还将作为自研分布式数据库PolarDB-X(PolarDB分布式版本)的核心,提供企业级数据库服务。




