MyTopling
MyTopling 是拓扑岭(topling.cn)开发的 MySQL 兼容数据库。
MyTopling fork 自 facebook 的 myrocks-8.0.23,进行了大幅修改:
- 将存储引擎从 RocksDB 替换为 ToplingDB
- 对 myrocks 的接入层进行了深度优化
- 对 ToplingDB Transaction 进行了深度优化
- Transaction 代码虽然在 ToplingDB/RocksDB 中,但其主要是为 MyTopling/MyRocks 服务的
- 支持 ToplingDB 的 分布式 Compact
- 支持 ToplingDB 的 CSPP_WBWI(CSPP WriteBatchWithIndex)
- 支持 ToplingDB 的 CSPP MemTable
- 支持 ToplingDB 的 ToplingFastTable
- 支持 ToplingDB 的 ToplingZipTable
- 支持 ToplingDB 的 引擎级监控(grafana)
- 支持 ToplingDB 的 引擎观测
MyTopling 的 创建索引 及 bulk_load 加速
在 MyRocks 中,bulk_load 和 创建索引走的是同一套逻辑,其处理方式大致为:
- 如果输入数据有序,直接使用 RocksDB 的 SstWriter 创建 SST 文件,然后将创建出来的 SST 文件注入到 LSM Tree 中
- 如果输入数据无序,先对数据排序(内排+外排),然后再执行 1 中的步骤
在 sysbench 性能测试中,我们发现 prepare 阶段创建索引的速度很慢,进一步定位到是在内存中的排序很慢,其排序方式非常原始粗暴,使用了 std::set
,并且在应该给 std::set
指定自定义 KeyComparator 的地方,把 RocksDB 中的 Comparator 指针放到每个 set 元素中。所以,即便使用最普通的优化方式,也能对性能有所提升,并降低内存用量。但我们最终进行了非常彻底的优化:
- 在 ToplingDB SstWriter 中,允许乱序输入
- 实现 AutoSortTable,以支持乱序输入
- 修改 MyRocks 相关代码,支持乱序输入
这中间的核心是 AutoSortTable,我们知道,RocksDB 的 SST 是 Static Sorted Table(也有称 Sorted String Table),创建 SST 时,输入数据必须是按 Key 有序的。支持乱序输入,其本质就是把排序从 SST 外部移到内部,貌似没啥可圈可点的。
然而,这里核心中的核心是:AutoSortTable 使用了 CSPP Trie,将 Key 逐个插入 CSPP Trie,比在内存中排序还要快,并且 CSPP Trie 只使用一整块内存,这意味着创建好的 CSPP Trie 数据结构可以通过一次 write 操作写到 SST 文件中。
最终效果是,优化后,索引创建的速度提升了 30 倍以上!
ToplingDB 的 Transaction 优化
- 在 MyRocks 的 Transaction 中,原本其性能热点是 SkipList (SkipList MemTable 和 SkipList WBWI),我们将这两个都用 CSPP Trie 替换之后,这个性能热点就消失了
- SkipList 性能热点消除后,原本不是性能热点的 Lock 管理,成了新的性能热点,这一块,主要通过 topling 的 hash_strmap 和 gold_hash_map 解决,并且,把调用链上的 std::string,改成了无需内存拷贝的 Slice
MyTopling 的 分布式 Compact
分布式 Compact 的核心是把 CompactionFilter 和 TablePropertiesCollector 需要的数据字典转移到 Compact Worker 结点上,这个,我们只需要仔细研读相关代码,将数据字典进行序列化/反序列化即可,并且,ToplingDB 提供了完善 SidePlugin 机制,使得实现这一点变得相对容易。
Compaction Worker 多进程化
MyRocks 中 CompactionFilter 和 TablePropertiesCollector 使用了一些全局变量,而我们的 Compact Worker 是多线程为多个 DB 实例服务的,全局变量在多线程中是共享的,这样就会导致来自多个 DB 的数据发生混淆。所以,我们将 Compact Worker 改成多进程的,将执行 Compact Job 的代码放到 fork 出来的新进程中,关于这一点,我们在知乎上有个回答
对上游 RocksDB 的 Pull Request
我们的诸多修改,有一些代码使用了 topling-core,对 RocksDB 的改动也较大,但还有一些修改没有外部依赖,我们将没有外部依赖的代码,都向上游官方 RocksDB 发起了 Pull Request
性能测试
time sysbench --db-driver=mysql --threads=50 --report-interval=1 \ --mysql-host=192.168.100.10 --mysql-port=3306 \ --mysql-user=test --mysql-password=1234 --mysql-db=test2 \ --tables=1000 --table_size=5000000 oltp_read_write \ --db-ps-mode=disable --mysql_storage_engine=rocksdb prepare
复制
硬件配置
DB 结点(1 个结点)
CPU | 双路 E5-2682 v4 共 32 核 64 线程 |
---|---|
内存 | 768G, DDR4 2133 |
SSD | 6 个 nvme SSD(三星 970 pro) |
Compact Worker 结点(共 8 个结点)
CPU | 双路 E5-2682 v4 共 32 核 64 线程 |
---|---|
内存 | 128G, DDR4 2400 |
SSD | 无(无盘服务器) |
DB 配置
rocksdb_write_disable_wal | ON |
---|---|
rocksdb_flush_log_at_trx_commit | 0 |
disable-log-bin | |
gtid_mode | OFF |
enforce_gtid_consistency | OFF |
DBOptions.allow_fdatasync | false |
在这样的软硬件配置上,上述命令花了 7067.903 秒,平均每秒钟写入超 70万行,538MB!
最终瓶颈还是在 IO 上,然而,按照 nvme SSD 的性能,6 块 SSD 的聚合带宽超过 15GB/sec,我们在运行过程中发现实际的 IO 写入只有 2~3GB/sec,操作系统并没有完全发挥硬件的 IO 性能。
所有结点(DB 结点和 Compact 结点)全程 CPU 占用率小于 50%,因为瓶颈在 IO。
MyTopling 更多地为随机写优化
这个 sysbench 命令其实更适合 InnoDB 和原版 MyRocks,因为 prepare 是按照自增 id 顺序写数据!
- 对于 InnoDB,顺序写对 BTree 是非常友好的操作,对 1000 个表中的单独每个表顺序写,表面上看并不是真正的顺序写,但实际上,到 BTree 的下层时,相当于是对 1000 个 range 的顺序写
- 如果 table 数量为 1,对于原版 MyRocks,顺序写数据,SST flush 之后,只需要 trivial move,直到压缩选项发生变化的 level
在 MyTopling 中,因为有分布式 Compact 加持,我们并没有针对顺序写去做优化,我们不允许把 SST 从 L0 trivial move 到 L1,也不允许从 L1 trivial move 到更下层,而是:
- 使用较大的 MemTable,生成较大的 L0 file
- 设置较小的 L1 SST file size
- 在 L0->L1 compact 时,少数 L0 SST 文件会 Compact 成很多个 L1 SST
- 在 L1->L2 compact 时,因为 L1 SST 文件数量众多,可以启动多个 compact 线程,利用分布式 compact,充分并行化
我们的原则是“充分并行化”,从而“为顺序写入而优化”就没有那么重要了,因为在实际业务中,绝大部分写都是随机写入!