内容简介:文章作者:温正湖 网易杭州研究院编辑整理:Hoh Xil
文章作者:温正湖 网易杭州研究院
编辑整理:Hoh Xil
内容来源:DTCC 2019
出品社区:DataFun
注:欢迎转载,转载请注明出处
MyRocks 是 Facebook 引入 RocksDB 后的 MySQL 分支版本,在 Facebook 内部得到了大规模使用,得益于 RocksDB 优秀的写入性能和数据压缩等特点,MyRocks 也受到了 MySQL 社区的极大关注,网易杭研在 InnoSQL 5.7.20 版本增加了 MyRocks 存储引擎。本次分享将为大家分析 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 成功落地打下了很好的基础。
1. 大数据量场景
第一个案例是大数据量业务,这可以是用户的行为,动态,历史听歌记录等。业务特点包括写多读少,数据量很大而且都是有价值的,不好基于时间进行删除,需要占用不少 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 。
2. 写密集型业务
下面说下第二个案例。这个案例与第一个案例不同的是,写的压力比第一个大很多,使用 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 。
3. 延迟从库
第三个案例是 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 性能等。
作者介绍:
温正湖,网易杭研资深数据库内核专家。《 MySQL 内核:InnoDB 存储引擎 卷1 》作者之一,申请技术专利 10+ ,已授权 5+ 。曾主导了网易公有云 RDS 、 MongoDB 等数据库云服务建设,现负责网易 MySQL 分支 InnoSQL 开发和维护,专注于数据库内核技术和分布式系统架构。
对作者感兴趣的小伙伴,欢迎点击文末阅读原文,与作者交流。
——END——
文章推荐:
关于 DataFun:
DataFun 定位于最实用的数据智能平台,主要形式为线下的深度沙龙、线上的内容整理。希望将工业界专家在各自场景下的实践经验,通过 DataFun 的平台传播和扩散,对即将或已经开始相关尝试的同学有启发和借鉴。
DataFun 的愿景是:为大数据、人工智能从业者和爱好者打造一个分享、交流、学习、成长的平台,让数据科学领域的知识和经验更好的传播和落地产生价值。
DataFun 成立至今,已经成功在全国范围内举办数十场线下技术沙龙,有超过三百位的业内专家参与分享,聚集了数万大数据、算法相关领域从业者。
您的「在看」,我的动力!:point_down:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用 Spell 实践深度学习,几乎零配置开始使用
- OpenGL 之 帧缓冲 使用实践
- 使用 Docker 安装 Zabbix 实践
- Logback最佳实践和使用指导
- Git子模块功能使用实践
- 接口测试工具Postman使用实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。