网易MyRocks使用和优化实践

网易云社区2019-10-30 14:42

本文是在DTCC2019第十届中国数据库大会上所做的“网易MyRocks使用和优化实践”PPT讲稿。由于不少同学看过PPT后,又线下联系希望获取更多的信息。于是将讲稿放出来。可评论进一步交流。如有疑问或错误之处,敬请指正。


MyRocks 是 Facebook 引入 RocksDB 后的 MySQL 分支版本,在 Facebook 内部得到了大规模使用,得益于 RocksDB 优秀的写入性能和数据压缩等特点,MyRocks 也受到了 MySQL 社区的极大关注,网易杭研在 InnoSQL 5.7.20 版本增加了 MyRocks 存储引擎。本次分享将为大家分析 MyRocks 的优点,介绍其在网易互联网核心产品的不同业务场景上的使用、参数调优、问题定位和功能优化。


本次分享主要从3个方面着手,分别是MyRocks/RocksDB实现和优点分析,MyRocks在网易的使用案例分析。

MyRocks技术实现



MyRocks是使用RocksDB替换InnoDB作为存储引擎的MySQL版本。下面简单介绍下RocksDB的技术实现。

RocksDB是一个kv存储引擎,基于Log Structured Merge Tree作为数据存储方式。这是跟InnoDB最大的不同。在RocksDB中,LSM-Tree默认是多层的,每层都会独立进行compaction操作。

RocksDB还有个概念是列族,column famliy。一个列族可以是一个表,也可以是表的一个索引。一个列族里面可以包括多个表的数据。每个CF单独有一颗LSM-Tree和多个MemTable。

Memtable是内存中的写buffer,缓存着已经提交但还未持久化的事务数据。其包括active和immutable 2种。Memtable可通过参数配置大小和个数。

在RocksDB中,所有的列族共用write ahead log文件。


下面看下RocksDB的写流程。

每个事务都有一个writebatch对象,用于缓存该事务在提交前修改的所有数据。在事务提交时,其修改的数据先写入WAL日志buffer,根据配置参数选择是否持久化。然后将修改的数据有序写入到active memtable中。当active memtable达到阈值后会变为immutable,再新产生一个active memtable。

数据写入memtable后就意味着事务已经提交。数据的持久化和compaction都是异步进行的。当immutable memtable个数达到所设置的参数阈值后,会被回刷成L0 SST文件。在L0文件个数达到阈值后,合并到L1上并依次往下刷。RocksDB中可以配置多个线程用于对每层数据文件进行compaction。


读操作分为当前读和快照读。这里以当前读为例。

首先查找本事务对应的writebatch中是否存在请求的数据。接着查找memtable,包括active和immutable,然后基于sst文件元数据查找所需的数据对应的文件是否缓存在block cache中。如果没有被缓存,那么需将对应的SST文件加载到block cache中。

在整个查找过程中,为了提高查找效率,会借助布隆过滤器。可以避免无效的数据IO和遍历操作。


上面3页ppt简单介绍了RocksDB及其读写流程。接着我们看看基于RocksDB的MyRocks都有哪些特性,有哪些不足。

首先,在特性方面,支持最常使用的RC和RR隔离级别,与InnoDB的RR级别解决了幻读问题不同,MyRocks的RR是标准的,存在幻读;实现了记录级别的锁粒度,支持MVCC提高读效率;通过WAL日志实现crash-safe;有相比InnoDB强大很多的压缩性能。

我们的MyRocks支持多种高效率的物理备份方式。包括myrocks_hotbackup和mariabackup等;MyRocks对主从复制机制也进行了优化,提高回放效率。除此之外,还有一些优秀的特性,比如更加高效的TTL等,在此不一一举例。

不足和限制方面:相比InnoDB,MyRocks在功能和稳定性上还存在较大差距。比如在线DDL,所支持的索引类型等功能。Bug也明显更多,尤其在XA事务、TTL、分区表等方面。

MyRocks优点分析


分析了实现和特性后,下面说下我们认为MyRocks相比InnoDB的核心竞争力,因为InnoDB已经足够成熟和强大,为什么还需要MyRocks,下面我们要讲的就是MyRocks存在的价值。


InnoDB是典型的B/B+树存取方式,即使是顺序插入场景,也会在一个page还未完全填满时触发分裂。变为2个半空的page。或者说InnoDB的page一定存在页内碎片。

而RocksDB由于是Append方式,都是文件顺序写行为。每写满一个memtable就flush到磁盘成为独立的sst文件,sst文件和sst文件的merge操作也是顺序读写,整个过程均不会产生内部碎片。


即使在非压缩模式下,RocksDB记录也进行了前缀编码,默认每16条记录才有一条完整的,这意味着节省了随后15条记录的共同前缀所需的空间。

此外,RocksDB每个索引占用7+1 bytes(sequence number + flag)的元数据开销,InnoDB是每记录6+7 bytes (trx_id + undo ptr)空间开销,似乎存在多个索引的场景下RocksDB更加费空间,但Ln SST文件seq id可置0,经过压缩等操作大部分元数据开销是可节省的。


在压缩效率方面,显然也是RocksDB更占据优势。

首先说下InnoDB压缩,包括两种,一种是使用最广泛的记录压缩,也就是通过在create table或alter table时设置key_block_size或compressed来启用。这种压缩并不是记录透明的,压缩前它会提取每条记录的索引信息。相对来说压缩比会降低。另一种是透明页压缩,在5.7版本中支持,使用较少,其压缩操作时在InnoDB的文件IO层实现,对InnoDB上层是透明的。该压缩需要依赖稀疏文件(sparse file)和操作系统的(hole punching)特性。不管是哪一种,都以page/block为单位来独立保持压缩后的数据,而文件系统是需要块对齐的,一般都是4KB,这就意味着一个16KB的页,即使压缩为5KB,也需要使用2个文件block,即8K保存。其中的3KB空间填空。

RocksDB很好得解决了这个问题。每个block压缩后,先组合成一个SST文件,保存时只需要SST文件对齐到4KB即可。


与HDD不同,SSD基于NAND Flash介质存储数据,其可擦写的次数有限,每个NAND Flash块写数据前都需要先擦除置位后才能写入新数据,因此写放大越小有利于延长SSD盘的使用寿命

InnoDB在随机写时,每个page写入或更新一条记录意味着需要完整写至少一个page,而且由于存在doublewrite,还需要再写一遍page。

相对来说,RocksDB会好一些。可以通过公式:1 + 1 + fanout * ( n – 2) / 2(n为LSM层数,fanout为每层存储增长倍数))计算出大概的写入放大。

1:从memtable回刷到L0 (1,20)

1:从L0到L1 (1,20)

从L1到L2开始,每层有fanout(10)倍放大。那就意味着 L2上面的数据有可能因为L1数据compaction的时候参与了compaction。参与次数最少0次,最多有fanout次。平均下来是fanout/2次。

而L2到L4一共3层。总层数是5层。那么假设层数是n,就是n-2层有 fanout/2次放大

最少0次的案例是:如果写入是顺序的,那么就不需要参与合并。比如每个sst文件2条记录。那么第一次写1,2,第二次写3,4,第三次写5,6。这样基于sst文件的campaction时,每层的数据都直接往下写就行了,不需要跟其他sst文件合并。

最多fanout次的案例是:如果第一次写入(1,40),第二次写入(20,21),那么第一次和第二次就得先merge然后拆为2个新文件(1,20)和(21,40),如果第三次写入是(10,11),那么(1,20)这个文件还得继续参与merge和拆分。一直下去,直到该层的数据达到了fanout阈值,不能再合并上层的数据了,就被写到下一层去。这样子,1就被重复写了fanout次了

相应地,RocksDB也提供了较为精确的写入放大统计可供实时查看,在MyRocks中可以通过show engine rocksdb status查看。

MyRocks应用案例


目前网易互联网这块有不少业务在使用MyRocks,包括云音乐、云计算等。接下来介绍几个在这些业务上的使用案例。除了MyRocks本身的优势外,在MyRocks落地过程中,我们跟DBA和业务三方进行了非常良好地配合,这也为MyRocks成功落地打下了很好的基础。

大数据量场景



第一个案例是大数据量业务,这可以是用户的行为,动态,历史听歌记录等。业务特点包括写多读少,数据量很大而且都是有价值的,不好基于时间进行删除,需要占用不少SSD盘。而且由于用户基数很大且在增长,所以数据增长也很快。导致需要频繁进行实例扩容/拆分。

目前采用DDB+MySQL/InnoDB的方式,这里举其中一个场景,该场景是一个DDB集群,下面挂着16个MySQL主从高可用实例,通过设置key_block_size对数据进行压缩。每个实例算1TB数据。总数据大概32TB。

写比较多,数据量大。这看起来是一个比较适合MyRocks的场景。


那么最简单的测试方式就是搭个MyRocks节点作为高可用实例的slave了。测试比较顺利,我们采用snappy压缩算法,1TB左右的InnoDB压缩数据,在MyRocks下只有300G多一点。换算到32个mysqld节点,可以节省20TB的SSD盘空间。而且由于盘空下来了,计算资源本来就比较空,那么本来一个服务器部署2个mysqld节点,现在有能力部署3~4个节点。同时,使用MyRocks后,数据的增长幅度也变低,对实例进行拆分/扩容的频率也就相应减低了。


当然,在使用MyRocks的时候也需要关注其与InnoDB的不同之处,比如内存使用方面。

RocksDB会比InnoDB占用更多的内存空间。这是因为RocksDB除了有与InnoDB buffer pool相对应的block cache外。我们在前面提到过他还有write buffer,就是memtable。而且每个column family都有好几个memtable。如果column family比较多的话,这部分占据的内存空间是很可观的。

目前RocksDB本身已经提供了实例总的memtable内存大小和未flush部分的大小,为了能够更加直观得了解每个column family的memtable内存情况,我们增加了一些指标,比如每个column family下memtable使用的总内存等。

另外,我们也发现在MyRocks上使用tcmalloc和jemalloc比使用glibc中默认的内存分配器高效很多。刚开始使用默认分配器,在写入压力很大的情况下,内存极有可能爆掉。

在block cache配置方面,需要了解rocksdb_cache_index_and_filter_blocks设置为0和1的不同之处,设置为1会将index和filter block保存在block cache里面。这样比较好控制内存的实际使用量。

最后,需要合理规划一个实例下column family的个数。每个cf的memtable个数也需要根据实际写入场景来配置,主要有图中所列的参数再加上每个memtable的大小write_buffer_size。

写密集型业务



下面说下第二个案例。这个案例与第一个案例不同的是,写的压力比第一个大很多,使用InnoDB根本就扛不住。而且读也很多,且读延迟比较敏感。这种业务场景,目前一般使用redis。但用redis也会遇到不少问题,比如持久化之类的,这里不展开。在本案例中主要还是关心成本。因为数据量比较大,需要大量的内存,而且随着推荐的实时化改进和推荐场景的增多,内存使用成本急剧增加,甚至可能出现内存采购来不及的情况。。。

显然,单个MyRocks肯定扛不住全部的压力,于是仍采用DDB+MyRocks的方式将压力拆分到多个实例上。但马上就发现新的问题。那就是MyRocks实例的从库复制跟不上。我们这个场景下,tps达到5000以上就不行了。为此也做了下调优,比如启用slave端跳过事务api等,虽然有点效果,但解决不了问题。 另外也尝试过在DDB层将库拆细,这样每个实例会有数十个DB,然后将复制模式改为基于DATABASE,效果是比较好的。但这样会导致DDB这层出现瓶颈,所以看起来也不是好的方案。

不过好在业务对数据一致性有一定的容忍度,所以,最终落地的方案是业务层双写。


这是双写部署框图。图中把算法相关的单元用Flink来表示。

算法单元从DDB1读取上一周期的推荐数据,跟当前实时变化的新数据相作用产生本周期的推荐数据,然后将其分别写入DDB1和DDB2。推荐使用方从DDB2上读取所需的推荐数据


这个系统灰度上线后效果还挺不错,业务方也较满意。但随着不断往上面加推荐场景,遇到了急需解决的问题是如何在高效利用服务器资源的情况下扛住写入压力。

也就是说,均衡利用现有服务器的cpu、内存和IO资源。不要出现IO满了但CPU很空,或者CPU爆了但IO还有不少剩余。而且在IO和CPU负载处于合理区间时得保证mysqld不会OOM。

在不断加推荐场景的过程中,首先出现的是数据compaction来不及。写性能出现大幅度波动。因为刚开始配置的compaction线程是8个,所以很自然得将其翻倍。效果还是不错的。但受cpu和io能力影响,再往上增加线程数就没什么效果了。 根据RocksDB暴露的一些统计信息,我们将触发write stall的阈值调高。


先是增加了触发write stall的等待compaction数据量阈值。Write stall触发周期明显变长。而且并不是数据量,而是l0层的文件个数阈值被触发。


于是又将l0层的文件阈值调高。瓶颈有回到了等待compaction的数据量。

最终在这几组参数上找到了一个平衡。讲到这里,可能有些同学有疑问,随着数据不断写入,如果compaction速度跟不上,设置的write stall阈值总是会被触发的。这就跟具体的业务场景有关了。因为这个场景的写入压力每天都有很强的周期性。到了凌晨时非常明显的低谷期。那么只要确保在低谷期compaction线程能够把累计的数据merge掉就不会有问题。如此循环。

当然,除了这组参数调优,其实还可以通过调节memtable的大小和个数。Memtable越大,意味着可以合并更多对同一条记录的DML操作,memtable越多意味着在flush时可以合并更多写操作。但这个调优需要考虑服务器的内存使用情况。如果内存本来就比较紧张,那就不可取了。


对于我们这个业务,在上一组参数调优下,效果比较好。服务器资源使用均衡,同时性能上也支撑住了业务的要求。

目前MyRocks已经成功得替换了好几个业务场景的Redis服务。用SSD盘换内存。再借助MyRocks高效的压缩能力,进一步减少SSD盘消耗。
当然,这并不是说所有的Redis都可用MyRocks取代。只能说对于哪些写入压力明显超过InnoDB能力但还没有到非用全内存不可而且数据量又相对较大的业务来说。可以尝试使用MyRocks。

延迟从库


第三个案例是MyRocks在解决数据误删除方面的尝试。数据误删除是个非常头痛的问题,现有的主从复制或基于paxos/raft的高可用方案都无法解决这个问题。目前潜在可选的方案包括基于全量+增量备份向前恢复,基于当前数据+flashback向后回退,基于延迟从库+待回放relay-log来恢复。

基于flashback的方案一般来说是最快的。对于DML操作,如果复制是基于row格式的,那么可以通过delete改insert,insert改delete,update操作改变前后项的方式来回滚。但对于DDL操作,目前MySQL官方版本是做不到的,社区里面也有开源的DDL flashback方案,但存在兼容性等问题。我们网易杭研这边维护的MySQL版本目前已经在不影响binlog兼容性的情况下支持DDL的flashback,在此不展开。

基于延迟从库的方案跟基于全量+增量的方式是类似的,但由于MySQL原生提供了延迟复制,而且是基于运行中的实例进行数据恢复,所以可靠性更高,相对来说性能也更好。但存储开销比较大,而且需要一定的计算资源。而MyRocks可以缓解存储成本的问题。

目前一些业务的核心库已经部署了基于MyRocks延迟从。在落地过程遇到过一些问题。主要是XA事务相关的,包括XA事务的回放问题,从InnoDB迁移数据到RocksDB等。


首先说聊下第一个问题。MySQL在5.7版本之前有个著名的xa prepare bug。就是说,按照正常的语义,xa prepare成功后,其数据虽然不可见,但它是持久化的。而在5.7之前,xa prepare的数据是不持久化的,如果xa commit前session退出,或者mysqld crash了,那么数据也就没了。

为了解决这个问题,5.7版本增加了一个新的binlog event类型,用来记录xa prepare操作。并配套修改了slave端xa事务回放的逻辑。详细的分析可以查看mysql worklog 6860 ( https://dev.mysql.com/worklog/task/?id=6860)。这里简单说明下:

大家知道,一个session执行了xa prepare后,是不能执行其他事务语句的,只能执行xa commit或xa rollback。但在slave端,mysql使用固定的几个worker线程来回放事务的,必须需要解决xa prepare后无法执行其他事务的问题。那么为什么无法执行其他事务呢,就是因为xa prepare后,session中该xa事务对应的上下文得保持到xa commit或rollback。很显然,只要将对应的事务上下文保存起来就可以了,在5.7中,mysql在server层引入了全局事务对象,通过在执行xa prepare前后进行detach和attach的操作将xa prepare事务上下文缓存起来。

但不仅仅是mysql server层有事务上下文,对于事务引擎也有上下文。而MyRocks的问题就在于并没有实现这套xa事务回放框架要求的API接口。比如在session退出时的引擎层面处理接口close_connection,将xa prepare事务的引擎层事务对象从当前worker线程detach掉的接口replace_native_transactioni_in_thd。

当然,上面只是解决了大框架的问题。在我们实现了所欠缺的接口开始用的时候,还是碰到xa事务回放的不少问题。包括因为回放xa事务导致更新gtid_executed和rpl_slave_info后未释放innodb事务对象而导致内存泄露问题。Xa prepare后因为rocksdb redolog刷新机制原因导致crash后数据丢失。解决与引擎无关的rocksdb和innodb都存在的因为mysql xa prepare操作先记录binlog在进行引擎层prepare导致数据丢失问题。等等。

由于时间关系,这里都不展开来讲了。感兴趣的同学可以线下交流。总之,如果没有较强的mysql源码修改能力。那就不要把myrocks用在有xa事务的业务场景上。


那么,除了myrocks代码本身的问题外,第二个问题是如何将innodb的数据迁到myrocks从库上。可能大家会疑问这有什么难的。做个备份不就行了吗。但这个问题确实给我们带来了一些麻烦。首先是因为物理备份然后通过alter table转成rocksdb存储引擎这种方法不行。因为rocksdb的ddl效率太差。所以只能通过逻辑备份了。网易内部有个数据迁移服务叫NDC,类似阿里的DTS,可以进行全量迁移,然后再通过解析binlog进行增量迁移。在迁移了全量数据然后基于gtid_executed起复制后很快就会报复制出错。提示xa commit对应的xid找不到。有2个原因,一是xa prepare的数据是不可见的,无法通过select出来导致的。二是NDC在停止增量迁移时,不会将binlog中的xa prepare执行掉。

所以,正确的方式应该是NDC在开始全量迁移前,获取gtid_executed,然后执行下xa recover,等待这些xa事务都commit掉后再开始迁移数据。结束增量迁移时,获取此时源节点的gtid_executed,并执行掉xa prepare后再起复制。

第三个问题相对来说好办,就是需要在复制时的ddl建表语句从innodb转为rocksdb。可采用的方案是周期性查询mysql系统表,将所有新产生的InnoDB业务表转为RocksDB。第二种是设置myrocks节点只能创建rocksdb引擎的表。这样会导致复制失败,通过脚本来将建表sql改为使用rocksdb。


延迟从库这个项目效果还是不错的。基本上一个物理服务器就搞定一个业务所有的核心库。花比较小的代价来提高了业务核心库的数据安全性,达到了预期的效果。


截止目前,通过将一些业务场景的InnoDB或Redis实例替换为MyRocks已经为业务节省了超过100w+的成本,但这还只是在小部分业务场景上使用。在网易内部还有很大的潜在使用空间。

接下来,我们在MyRocks上的计划是将MyRocks上到网易云数据库服务RDS上,可以大大提高MyRocks运维自动化,接入更多的业务场景。在MySQL 8.0上支持MyRocks,改造优化MyRocks非常糟糕的在线DDL性能等。