出处:https://zhuanlan.zhihu.com/p/45652076
作者:温正湖(任职于网易杭州研究院,从事数据库内核开发)
MyRocks是一种经过空间和写性能优化的MySQL数据库,为您业务的数据库选型提供一种靠谱的选择。本文主要介绍什么是MyRocks,包括其功能特性,重点讲解MyRocks相比InnoDB的优势,详细分析MyRocks适用的各种场景。
RocksDB是FaceBook基于Google开源的LevelDB实现的,使用LSM(Log-Structure Merge)树来存储数据。Facebook开发工程师对RocksDB进行了大量的开发,使其符合MySQL的插件式存储引擎框架的要求,移植到了MySQL上,并称之为MyRocks。MyRocks支持基于SQL的数据读写、锁机制、MVCC、事务、主从复制等MySQL绝大部分功能特性。从使用习惯考虑,使用MyRocks还是使用MySQL/InnoDB并没有多大区别。
经过4年多的发展,MyRocks已经成熟,开源的MySQL分支版本Percona和MariaDB已将MyRocks迁移到自己的MySQL分支中,InnoSQL作为网易的MySQL分支,目前也已支持MyRocks,具体版本为InnoSQL 5.7.20-v4,在开源的MyRocks代码基础上,我们对其做了功能优化增强、bugfix,并支持对其进行本地和远程在线物理备份。下面先简要介绍MyRocks特性,让大家对其有个基本认识。由于MyRocks只是将InnoDB替换为RocksDB,所以MySQL Server层的逻辑并没有多大变化,包括SQL解析和执行计划,基于Binlog的多线程复制机制等。我们讨论的焦点主要是存储引擎层,也就是RocksDB上。
本文主要包括3个部分:首先是通过RocksDB读写流程来介绍其整体框架、存储后端和功能特性;接着分多维度分析其与InnoDB的不同点,这些差别所带来的的好处;最后分析RocksDB的这些优势能够用在哪些业务场景上。文章较长,大家可以挑自己感兴趣的部分食用。
RocksDB读写流程
写流程
上图所示为RocksDB的写请求示意图,一个事务的修改在提交前先写入事务线程自身的WriteBatch中(在上图示例中事务仅执行一个Put操作,那么WriteBatch中仅有该Put),在提交时被写入RocksDB位于内存中的MemTable中,MemTable本质上是一个SkipList,里面缓存的记录是有序的。和InnoDB一样,事务更改的数据(WriteBatch)在提交前也会先写Write Ahead Log(WAL),事务提交后,只需保证WAL已经持久化即可,MemTable中数据不需要写入磁盘上的数据文件中。当MemTable大小达到阈值后(比如32MB),RocksDB会产生新的MemTable,原来的MemTable会变为只读状态(Immutable),不再接收新的写入操作。Immutable MemTable会被后台的Flush线程dump成一个sst文件。在磁盘上,RocksDB通过一个个sst文件来保存数据,一个个log文件保存WAL日志。在磁盘上,sst文件是分层的,每层多有一到多个sst文件,文件大小基本固定,层级越大,该层的文件数量越多,意味着该层允许的总大小越大,如下图所示。
一般情况下,从内存中dump出来的文件放在Level0,Level0层的各个sst文件其保存的记录区间是可能重合的,比如sst1保存了1.4.6.9,sst2保存了5.6.10.20。由于采用LSM树技术存储数据,所以一条记录会有多个版本,比如sst1和sst2都有记录6,只不过sst2中的版本更新。同样的,不同层级间也会存在相同记录的不同版本。跟Level0不同,Level1及更高层级的sst文件,同层的sst文件相互间不会有相同的记录。
Compaction机制
既然存在多个不同的记录版本,那么就需要有个机制进行版本合并,这个机制就是Compaction。
上图就是一个Level0的Compaction,将一到多个Level0的文件跟Level1的文件进行compaction的过程。不管是将内存的MemTable dump到sst文件,还是sst文件之间的Compaction,从IO角度都是顺序读写,这不管在SSD还是HDD上都是有利的,对于HDD可以发挥顺序性能远高于随机性能的特点,对于SSD,可以避免随机写带来的Flash介质写放大效应。
读流程
聊完了RocksDB写流程,我们再来看下跟读相关的组件。如下所示:
数据库中的读可分为当前读和快照读,所谓当前读,就是读取记录的最新版本数据,而快照读就是读指定版本的数据。在此我们仅讨论当前读,快照读可做类似的分析。由于采用LSM树存储结构,所以RocksDB的读操作跟InnoDB有较大的不同,这是由于LSM可能存在多个记录的版本(且不像InnoDB那样前后版本有指针相连),且无法通过(严格意义上)的二分查找。因此,在RocksDB中引入Bloom Filter(布隆过滤器)来进行读路径优化,在RocksDB中Bloom Filter可以选择三种不同的方式,分别是基于data block的、基于partition的和基于sst文件的,Bloom Filter可以用来判断所需查找的key一定不在某个block/partition/sst中。RocksDB默认基于data block,其粒度最小。
接下来结合上面2张图简要分析RocksDB读流程。一个Get(key=bbb)请求首先在当前MemTable中通过Bloom Filter查找,若未命中,在进一步到只读MemTable,如果还未命中,说明该key-vaule或者在磁盘sst文件中,或者不存在。所以需要搜索每个sst文件的元数据信息,找出所有key区间包含所请求key值的sst文件。并根据层级从小到大进行查询。对于每个sst文件,通过Bloom Filter进一步查找,若命中,则将sst文件中的data block读入BlockCache,通过二分法在block内部进行遍历查找,最后返回对应key或NotFound,如下图所示。
RocksDB列族
在RocksDB中列族(Column Family)就是在逻辑上独立的一棵LSM树,每个列族都有自己独立的MemTable,所有列族共享一份WAL日志。sst文件的Compaction是以列族为粒度进行的。
默认情况下一个MyRocks实例包括2个列族,分别为用于存放系统元数据的_system_和用于存放所有用户创建的表数据的default。当然,用户在定义表的时候,可以通过在索引后面加备注(comment)来声明该索引使用的列族名,下面的例子即将rdbtable的主键和唯一索引都放在独立的列族cf_pk和cf_uid上。
CREATE TABLE "rdbtable" (
"id" bigint(11) NOT NULL COMMENT '主键',
"userId" bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
PRIMARY KEY ("id") COMMENT 'cf_pk',
UNIQUE KEY "uid" ("userId") COMMENT 'cf_uid',
) ENGINE=ROCKSDB DEFAULT CHARSET=utf8
MyRocks主要功能特性
并发控制
MyRocks基于行锁(row locking)实现事务并发控制,锁信息都保存在内存中。MyRocks支持shared和exclusive行锁,MyRocks在事务中执行更新时使用RocksDB库进行锁管理。可通过设置unique_check=0来屏蔽行锁和唯一键检查,这样在批量导入数据时会提高性能,但使用时要注意数据key是否有重复,所以一般的高可用实例的从库关闭唯一性检查以加快Binlog回放速度。目前MyRocks还没有实现gap锁,存在幻读问题(phantom read),这与标准的RR隔离级别一样,但弱于InnoDB的RR。
事务隔离级别
MyRocks目前支持2种事务隔离级别:read committed(RC)、repeatable reads(RR)。MyRocks使用快照(snapshot)实现这两种隔离级别,在repeatable reads中,snapshot在整个事务中持有,事务中的语句将看到一致的数据。在read committed隔离级别中,snapshot将被每个语句持有,因此SQL语句可以看见该语句执行前的对数据库的修改。与绝大多数数据库实现一样,在RR隔离级别下snapshot是在事务执行第一条sql时获取而不是事务开始时(begin/start)获取。
与InnoDB相同,MyRocks支持基于MVCC的快照读,快照读无需加锁。MVCC通过RocksDB快照实现,方法类似于InnoDB的read view。
备份与恢复
与InnoDB一样,MyRocks支持进行在线物理备份和逻辑备份。逻辑备份通过mysqldump或mydumper等现有MySQL备份工具。物理备份则通过MyRocks实现的myrocks_hotbackup工具进行远程备份,或者使用mariadb提供的mariabackup工具进行本地备份。
与InnoDB的比较优势
熟悉MySQL的同学们都知道,InnoDB目前是在MySQL上占统治地位的存储引擎。其具备了一个关系型数据库存储引擎应该拥有的绝大部分特性,如强大而完整的事务机制等,MySQL官方已经将InnoDB作为MySQL不可分割的一部分,新加入的MySQL系统表均使用InnoDB而不是MyISAM。那么为什么Facebook不使用InnoDB而另起炉灶基于RocksDB开发MyRocks呢。显然,RocksDB肯定有他过人之处,下面将从多个维度进行对比分析。
更小的存储空间
先来看看InnoDB在存储空间利用上存在的问题,我们知道InnoDB是基于B+树的,避免不了树节点的SMO操作,下面是个叶子节点分裂示意图。
叶子节点Block1在插入user_id=31后触发了节点分裂条件,被从中间拆分为2个Block,每个块占用原Block1约一半的空间,显然每块的填充率不到50%,也就是说此时有一半内碎片。
对于顺序插入的场景,块的填充率较高。但对于随机场景,每个块的空间利用率就急剧下降了。反映到整体上就是一个表占用的存储空间远大于实际数据所需空间。
但基于LSM树的RocksDB不会有该问题,其每次数据插入、更新和删除都是在一个新的sst文件中追加写入,只需在文件内部保证有序即可,不需要通过检索找到B+树的全局有序的某个迁移位置插入或更新,这样就解决了B+树节点的填充率问题,提高了空间利用率。
更进一步,RocksDB的sst文件是分层的,上下层总大小比值最大了10,在大数据量情况下,最坏也只有约10%的空间放大,这相比InnoDB是个很大的提升。
此外,如上图所示,RocksDB在存储时对记录列采用前缀编码。对每行的元数据也采取类似的处理方式。这更进一步减小的所需的存储空间。
更高效的压缩方式
在之前的文章中我们介绍过InnoDB基于记录的压缩机制,大概的实现方式是将16KB页(Page)中每条记录的部分字段进行压缩,再将压缩后的所有记录按照指定的页大小进行存放。比如设置的key_block_size为8,即压缩后按照8KB进行存放,若压缩后页大小为5KB,则浪费了3KB的存储空间。InnoDB在MySQL 5.7版本引入透明页压缩,但仍存在上述的问题。
RocksDB在记录压缩时不是基于页的,无需按key_block_size进行对齐,只需每个sst文件在压缩后按照文件系统块大小(一般为4KB)对齐即可,每个数MB的sst文件对齐开销不超过4KB,远远小于InnoDB压缩的对齐开销。
综合比较,MyRocks相比InnoDB能够节省一半以上的存储空间。
旧版本回收优化
在对记录进行频繁更新的场景下,若存在长时间的一致性快照读,InnoDB会因为记录旧版本无法purge导致undo空间急剧增大。但RocksDB可以有效缓解还问题。下面通过一个示例进行说明。
假设对MySQL进行一致性逻辑备份,开启事务但还未对表t执行select操作前对该表主键为1值为0的记录进行100万次增一操作。根据原理,本次备份需要读到值为0的原始记录。
对于InnoDB,由于备份事务id小于更新100万次增一的事务id,因此,这100万个旧版本记录(即undo)都不会被purge,这意味着在对该记录进行备份时,需要执行100万次版本回溯,每次都是基于记录上的undo指针对undo页进行随机读,效率很低。
RocksDB针对InnoDB存在的旧版本记录purge问题进行了优化,假设原始记录的sequence number为2,该版本即为备份事务可见版本,对于比它更大的版本,在RocksDB将MemTable dump为sst文件,或对sst文件进行Compaction时会删除中间版本,仅保留当前活跃事务可见版本和记录最新的版本。这样既满足MVCC要求,又提高了快照读效率,同时也减少了需占用的存储空间。
更小的写放大
在InnoDB上,一次记录更新操作需要先将当时记录版本写到undo日志中用于进行事务回滚和MVCC(写undo页前也需要先写undo的redo),再写一份更新后记录的redo用于进行宕机恢复,然后才能将更新操作写到对应的数据页上(可能会触发B+树节点分裂),为了避免在刷盘时宕机导致数据页损坏,还需要再写一份到Doublewrite磁盘缓存中。
可以看出,一次更新需要写的东西非常多,特别的,如果是随机更新场景,在写数据页和Doublewrite时,写放大的比率是页大小/记录大小,非常惊人。
RocksDB写放大与其sst文件总层级相关,最坏的写放大情况约为(n-2)*10,其中n为总层数。显然,相比InnoDB会好很多。
写放大变小了,意味着有限的存储写能力能够得到更高效的发挥,可以说在达到存储IO性能瓶颈时,RocksDB能够写更多记录。
另一方面,RocksDB每次数据插入、更新和删除都是追加写入而不是原地更新。这样表现在存储后端上就全都是顺序写,没有随机写。对基于NAND Flash实现的SSD,在不考虑SSD内部对写放大优化的前提下,同样一块SSD,在RocksDB下能够比在InnoDB下用得更久。
更快的写入性能
前面已经提到,InnoDB对记录是原地更新的这意味着在随机DML场景下对每条记录操作都是随机写(即使对二级索引的先删除再写入新记录的情况,也是随机的),如下图所示。
而RocksDB不同,将随机写转换为顺序写,后台进行记录新旧版本合并的多线程Compaction也是批量的顺序写操作。对于批量插入场景,RocksDB也可以关闭记录唯一性检查来进一步加速数据导入速度。
在HDD上,这样的优化能够发挥机械盘顺序读写性能远优于随机读写的特点。即使在SSD上,这样的优化对数据库的性能也是有帮助的。
更小的主从延迟
相比InnoDB,RocksDB还提供了更多的从库DML优化选择。
由于在从库上能够并行回放的事务肯定是没有冲突的,也就是说不存在事务间的锁等待关系,所以,RocksDB引入了一个优化参数rpl_skip_tx_api用来调过对记录加锁等保障事务隔离性的操作,加快了事务回放速度。
类似的,针对从库上事务特点,可以跳过记录插入操作的唯一键约束检查,对于更新和删除操作,可以跳过记录查找操作,因为只要没有实现上的Bug,所操作的记录肯定是满足事务约束的。
其他InnoDB没有的特性
MyRocks在MySQL 5.6/5.7就实现了逆序索引,基于逆序的列族实现,显然,逆序索引不能使用默认的default列族。基于LSM特性,MyRocks还以很低的成本实现了TTL索引,类似于HBase。相比MongoDB遍历记录进行批量删除的TTL实现方式,LSM存储下的TTL特性除了需要保存时间戳外,没有额外的维护性能损耗代价,直接在Compaction时合并处理即可。
MyRocks适用场景
根据上面的描述,可以总结出MyRocks适用的业务场景,包括:
大数据量业务
相比InnoDB,RocksDB占用更少的存储空间,还具备更高的压缩效率,非常适合大数据量的业务。下图为Facebook公开的RocksDB与InnoDB空间占用对比。
下图为网上的RocksDB和InnoDB、TokuDB压缩对比数据
结合上图可以发现,RocksDB所需的存储空间远小于InnoDB,甚至比以高压缩比著称的TokuDB还要好一点。
在网易内部的业务测试中也得到了验证,某个热门业务的DDB实例由于数据量增长很快,DBA不得不频繁进行分表扩容操作。使用MyRocks替换InnoDB发现,启用压缩(key_block_size=8)的165GB的InnoDB单表,在MyRocks压缩下仅为51GB,该DDB实例一共有8个MySQL高可用实例,每个DBN包含10个InnoDB表,统计下来,替换MyRocks后实例所需存储空间从26TB降为不到9TB。这一方面节省了三分之二(约17TB)的存储开销,同时也延长了DBA需要分表扩容的周期,假设DBA之前需要每个季度进行一次扩容操作,现在只需要每三个季度扩容一次即可。
写密集型业务
MyRocks采用追加的方式记录DML操作,将随机写变为顺序写,非常适合用在有批量插入和更新频繁的业务场景。下图为阿里云发布的批量插入场景下的性能对比图,相比基于InnoDB的AliSQL,MyRocks获得了近一倍的性能提升。
在网易内部的某更新密集型业务场景下,也获得了较好的性能表现,除了有不弱于KV存储系统的写入性能外,在读性能上还占据了一定的优势。对比如下:
上图是在只读,1:1和2:1混合读写情况下,测试10分钟获取的结果,可以发现MyRocks在性能和延迟两个方面均有较好的表现。
上图是1:1混合读写和只写场景下,写性能和延迟情况。可以发现在20写并发情况下,MyRocks也有上佳的表现。
缓存持久化方案
由于MyRocks具有高效的空间利用率,相比InnoDB,同样大小的内存可缓存更多的数据量;相比pika等Redis替代方案,具有成熟的故障恢复机制和主从复制架构;此外其更低的复制延迟有利进行读能力扩展。因此,MyRocks也是较合适的Redis缓存替代方案。
替换TokuDB
相比TokuDB,RocksDB/LevelDB拥有毫不逊色的写入性能和压缩比,具有更好的读性能;作为存储引擎被MySQL、MongoDB、Kudu和TiDB等主流数据库系统所使用,有更好的开源社区支持,更快的问题定位和BugFix可能性,更具可读性的源码。在TokuDB越来越不被看好的情况下,MyRocks可用于替换目前线上的TokuDB实例。
低成本低延迟从库
MyRocks的较好的写入性能,再配合从库针对性参数优化,可实现比InnoDB更低的复制延迟。再加上更小的存储空间占用优势,适合用于搭建特殊用途的从库,比如防止线上数据误删除的延迟从库,用于进行大数据统计和分析的从库等。
总结
总的来说,相比InnoDB,MyRocks占用更少的存储空间,能够降低存储成本,提高热点缓存效率;具备更小的写放大比,能够更高效利用存储IO带宽;将随机写变为顺序写,提高了写入性能,延长SSD使用寿命;通过参数优化降低了主从复制延迟。因此,在数据量大、写密集型等业务场景下非常适用。此外,作为同样的MySQL写和空间优化方案,MyRocks具有更好的社区生态,适合用于替换TokuDB实例。MyRocks高效的缓存利用率,成熟的故障恢复和主从复制机制,使得其也可以作为Redis的持久化方案。
参考资料:
1、RocksDB实现分析 http://ks.netease.com/blog?id=10818
2、RocksDB wiki https://github.com/facebook/rocksdb/wiki
3、Facebook、Percona、Alibaba公开的RocksDB相关文档和PPT
全文完。
Enjoy Linux & MySQL :)
「MySQL核心优化」课已升级到MySQL 8.0,扫码开启MySQL修行之旅