MyTopling 中的 iterator 缓存

(一)背景

MyTopling 是基于  ToplingDB 的  MySQL,分叉自 MyRocks,ToplingDB 则分叉自 RocksDB,兼容 RocksDB 接口,从而 MyTopling 可以复用 MyRocks 的大部分成果。

ToplingDB 早已开源,MyTopling 也会于近期开源。

(二)sysbench 基准测试

sysbench是一个模块化的、跨平台、多线程基准测试工具,主要用于评估测试各种不同系统参数下的数据库负载情况。关于这个项目的详细介绍请看: github.com/akopytov/sys  。
它主要包括以下几种方式的测试:

  1. CPU 性能
  2. 磁盘 IO 性能
  3. 调度程序性能
  4. 内存分配及传输速度
  5. POSIX 线程性能
  6. 数据库性能(OLTP 基准测试)

sysbench 的数据库 OLTP 测试支持  MySQL、PostgreSQL、Oracle,目前主要用于 Linux 操作系统,开源社区已经将 sysbench 移植到了Windows,并支持 SQL Server 的基准测试。

所以,我们用 sysbench 对 MyTopling 进行基准测试,并与主流的 MySQL 变体(原版 MySQL,MyRocks,PolarDB)进行对比。

(三)火焰图

火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的,和所有其他的 profiling 方法不同的是,火焰图以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能导致性能瓶颈的调用栈。

火焰图 svg 文件可以通过浏览器打开, 它对于调用图的优点是:可以通过点击每个方块来分析它上面的内容。

火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短。火焰图的配色并没有特殊的意义,默认的红、黄配色是为了更像火焰而已。

火焰图特征

  • 每一列代表一个调用栈,每一个格子代表一个函数
  • 纵轴展示了栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,正在占用 cpu 的函数。
  • 横轴的意义是指:火焰图将采集的多个调用栈信息,通过按字母横向排序的方式将众多信息聚合在一起。需要注意的是它并不代表时间。
  • 横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。
  • 火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。
  • 其他的采样方式也可以使用火焰图, on-cpu 火焰图横轴是指 cpu 占用时间,off-cpu 火焰图横轴则代表阻塞时间。
  • 采样可以是单线程、多线程、多进程甚至是多 host

火焰的每一层都会标注函数名,鼠标悬浮时会显示完整的函数名、抽样抽中的次数、占据总抽样次数的百分比。下面是一个例子。

在某一层点击,火焰图会水平放大,该层会占据所有宽度,显示详细信息。

按下  Ctrl + F 会显示一个搜索框,用户可以输入关键词或正则表达式,所有符合条件的函数名会高亮显示。

总的来说

  • 颜色本身没有什么意义
  • 纵向表示调用栈的深度
  • 横向表示消耗的时间

(四)MyTopling sysbench 火焰图

这个火焰图是运行 sysbench select_random_range 时 MyTopling 服务器进程的。图中我加了一个绿色和蓝色的方框,绿色是  handler::read_range_first,蓝色是  myrocks...setup_scan_iterator

可以看到,read_range_first 耗时主要包含 iterator 的 setup 和 seek,seek 是必不可少的,但是 setup 占那么多,太过于浪费了!

在 MyTopling 使用的 ToplingDB 中,SST 数据存储是 ToplingZipTable,ToplingZipTable 使用了可检索内存压缩技术,select_random_range 测试命中的 ToplingZipTable 索引是 Topling NestLoudsTrie,这是一种 嵌套的Succinct 数据结构,众所周知,Succinct 因为实现难度高,内存访问又比较随机,实际工程中很少使用,但是,ToplingZipTable 中使用的 Succinct,经过了我多年的持续优化,性能已经逼近硬件极限,所以,在这个火焰图中,原以为会是性能热点的 NestLoudsTrie,其 CPU 占比却 极低!大量的 CPU 浪费在了不必要的计算上……

(五)性能分析

通过火焰图进行性能分析,重点是发现 意料之外的时间消耗,然后找到相应的代码,看到底干了什么事。

在这个案例中,我们通过看火焰图知道 setup_scan_iterator 中会创建 iterator,再看代码就发现,这些 iterator 是在  ha_rocksdb::index_init 中创建的,然后在  ha_rocksdb::index_end 中销毁。

所以我们自然而然就会想:只要在第一次创建好之后,把这些 iterator 保存起来,后面直接使用,不再创建,就可以了啊!

(六)问题没有那么简单

创建 iterator 时指定了一个快照(snapshot),每次创建 iterator 时,都是在不同的 snapshot 上创建的,单单保存 iterator 是没用的!MyRocks 的作者们不是傻子,问题真这么简单,他们早就解决了!

所以,我们得继续分析:snapshot 本质上只是一个 SeqNum,在 snapshot 上创建 iterator 时,snapshot 的作用就只是给 iterator 提供一个 SeqNum。那么,我们只要改变 iterator 中保存的 SeqNum 不就可以了?

继续看代码,很快发现,RocksDB 确实提供了一个接口: Iterator::Refresh ,从字面上看,好像有点符合我们的期望,但是该函数没有参数,所以它必然不能满足我们改变 Iterator 中 SeqNum 的需求。所以,我很快给 上游 RocksDB 提了一个  Feature Request: Iterator::Refresh() with a snapshot

(七)缓存 SST 的 Iterator

既然要复用 Iterator,那我们就看看都有哪些东西需要复用,在上面那个火焰图中,我们聚焦到 Seek:

然后发现,蓝色方框中的部分,这是创建单个 SST 的 Iterator,对 DBIter 的 Seek,会落到具体的 SST 上,每个 Level(严格讲是每个 Sorted Run,L0 包含多个 Sorted Run),这样,对于多次 Seek,如果落到不同的 SST 上,就需要销毁旧 SST 的 Iterator,创建新 SST 的 Iterator,在大部分情况下,这部分都不算大热点,但是如果复用 DBIter 之后,这一块必然会成为新热点。

这个小热点可以轻松消除,并且解决思路很清晰:增加新选项  ReadOptions::cache_sst_file_iter,据此决定是否缓存 SST 文件的 Iterator。此类修改,改动不大,风险低,也容易理解,容易测试,所以我改完也就  Pull Request 给上游 RocksDB 了。

(八)自己动手,尽在掌握

RocksDB 官方只是把这个问题标记为 待解决,所以我就先做其它事情(MyTopling 一个导致 MySQL Query Plan 出错的 Bug)。

这期间还实现了一个重大优化: MultiGet IO 并发在 ToplingDB 中的协程实现,以及在 MyTopling 中的落地应用,实现过程很顺利,从确定决策开始动手,到完成在 MyTopling 中的整合及测试,一共花了三天时间。

手边紧要的事情都做完了, Feature Request: Iterator::Refresh() with a snapshot 还没有动静,我们等不起,尝试一下自己解决吧:

Iterator::Refresh 的具体实现是  ArenaWrappedDBIter::Refresh代码面前, 了无秘密,在该函数中,如果发现  SuperVersionNumber 发生了改变,就原地析构旧的 DBIter 并原地构造新的 DBIter,否则的话,把 Iterator 中保存的 SeqNum(相当于一个 snapshot)更新到最新。

所以,我的修改就很简单直接了:给 Refresh 增加一个 snapshot 参数,如果 snapshot 为 null,那就是旧的行为,否则的话,把 SeqNum 更新到 snapshot 的 SeqNum。再加上其它配套修改,总共也没有动多少代码。

这个修改我也  Pull Request 给上游 RocksDB 了,因为这个修改可以连带解决另一个问题: MyTopling 解决的 MyRocks Bug: create index 引发的内存问题。这个问题,我也向 RocksDB 和 MyRocks 提交了相应的 issue 和 pull request( RocksDB Issue #10536MyRocks Issue #1211MyRocks PR #1212)。

create index 引发的内存问题,该问题我们在 MyTopling 中的解决方案是 Workaround( MyRocks PR #1212):间歇性地(目前是每前进/后退 10万次)销毁旧 Iterator,创建新 Iterator。
  因为 Iterator 引用了一个 Version 对象,而该 Version 对象会将 MemTable 和 SST pin 住,并且因为 RocksDB 实现的原因,后续新的 MemTable 对象也会被 pin 住,从而在一个长命百岁的 Iterator 活着时,持续向 DB 写入数据会导致 OOM 内存爆炸。
  我们之前的 workaround 解决方案就是缩短 Iterator 对象的生命周期,从而间接地解决问题。我们现在增加了 Refresh(snapshot) 之后,只需要间歇性地 Refresh(snapshot) 就可以了。理论上这个间歇性的Refresh(snapshot) 可以放在 Iterator 本身的实现之内,不过这有点越俎代庖,就维持原样吧。

(九)整合进 MyTopling

我们增加了新的 MySQL 变量: rocksdb_reuse_iter(为了兼容性,仍然用 rocksdb 前缀)。解决问题的思路很简单,但实现及测试、debug 还是费了好些力气。

最终效果非常振奋人心,sysbench select_random_ranges 性能提高了 70%!

(十)最后

MyTopling 即将开源,开源的同时,我们会推出 DBaaS 内测版,欢迎大家体验, 进入官网,扫码加入 Topling 用户群!


请使用浏览器的分享功能分享到微信等