背景
PegaDB2.0 是基于 RocksDB 构建的兼容 Redis 协议的 NoSQL 存储服务,在介绍性能优化之前,简单介绍一下 Kvrocks 是如何与 RocksDB 交互的。
从实现上来看,Kvrocks 会将 Redis 数据类型编码成 Key-Value 数据写入 RocksDB 的不同 Column Family (以下简称 CF)中。目前主要有以下几种 CF:
Metadata CF:主要存储String 类型的数据,以及 Hash/Set/List 这些复杂类型的元数据(长度,过期时间等),不存储具体元素内容
Subkey CF:存储复杂类型元素内容
ZSetScore CF:存储 Zset Score 信息
Pub/Sub CF:Pub/Sub 相关信息
Propagated CF: 作为主从同步和存放数据之外的内容,比如 Lua 脚本
交互逻辑如图所示:
优化细节
引擎升级
PegaDB2.0 此前使用的是 RocksDB 6.4.6,版本较老,无法使用某些优化后的新特性。RocksDB 的版本迭代非常快,我们调研了业界比较有名的使用 RocksDB 的项目,比如 MyRocks,ArangoDB 使用的是最新的 6.26.1 版本,StarDog 使用的是 6.20 版本。
PegaDB2.0 选择较新的 6.26.1 版本,并提供多种存储引擎版本以满足不同的需求,我们也在考虑引入存储引擎热拔插的机制,在不影响线上服务的前提下切换存储引擎。我们团队将持续关注 RocksDB 社区进展,及时反馈、解决 RocksDB 使用中的问题,并回馈社区。
Memtable 优化
PegaDB2.0 使用 SkipList Memtable,相比 HashSkipList Memtable 拥有更好的跨多个前缀查找性能,并且也更节省内存。同时针对 SkipList Memtable 打开 whole_key_filtering 选项,该选项会为 Memtable 中的 key 创建 Bloom Filter,这可以减少在跳表中的比较次数,降低查询时的CPU使用率。
相关配置:
metadata_opts.memtable_whole_key_filtering = true
metadata_opts.memtable_prefix_bloom_size_ratio = 0.1
SST 优化
Data Block
PegaDB2.0 在 Data Block 上使用 Hash 索引,提升点查询的效率。 查找 Data Block 时候,会使用二分查找,二分查找会导致 CPU Cache Miss,提高 CPU 使用率。如果在 Data Block 上使用 Hash 索引,将会避免二分查找,在点查询场景下降低 CPU 利用率。官方的测试数据显示该特性会降低 21.8% CPU 利用率,提升 10% 吞吐,但是会增加 4.6% 的空间占用。相比磁盘资源,CPU 资源更加昂贵,权衡之下,我们选择打开 Hash 索引。
相关配置:
BlockBasedTableOptions::data_block_index_type = DataBlockIndexType::kDataBlockBinaryAndHash
BlockBasedTableOptions::data_block_hash_table_util_ratio = 0.75 // 如果此值小的话,说明 Hash 桶多,冲突就比较小。
Filter/Index Block
旧版本的 RocksDB 默认使用的 BloomFilter 是 BlockBasedFilter,这也是 LevelDB 的方式。基本原理是每 2KB 的 KV 数据产生一个 Filter,最后组成一个 Filter 数组。在查找的时候,先查找 Index Block, 对于可能存在该 Key 的 Data Block,再查找对应的 Filter Block 判断 Key 是否存在。
RocksDB 对 Filter 进行了优化,引入 Full Filter。每个 SST 都有一个 Filter,这样可以检查 Key 是否存在于 SST 中,避免读取 Index Block ,但是如果 SST 中 key 较多的话,会导致 Filter Block 和 Index Block 较大,对于 256MB 的 SST,Index/Filter Block 的大小大约为 0.5MB 和 5MB,这比 Data Block(通常4-32KB)大得多。当 Index/Filter Block 完全存于内存时,每个 SST 生命周期只会读取一次,这是最理想的情况,但当它们与 Data Block 竞争 Block Cache 时,很可能从磁盘上重新读取很多次,导致读放大非常严重。
PegaDB2.0 以前的做法是动态的调节 SST 相关的配置,使得 SST 文件不会过大,从而避免 Index/Filter Block 过大,但是这种机制存在的问题是当数据量非常大时,SST 文件过多,占有过多系统资源,也会带来性能的衰减。新版本 PegaDB2.0 对此进行了优化,打开 Partitioned Block 相关的配置, Partitioned Block 的原理是在 Index/Filter Block 基础上加一层二级索引,当读 Index 或者 Filter 的时候,先将二级索引读入内存,然后根据二级索引,找到所需的分区 Index Block,将其加载进 Block Cache 。Partitioned Block 机制带来的优点如下:
增加缓存命中率:大 Index /Filter Block 会污染缓存空间,将大的 Block 进行分区,允许以更细的粒度加载 Index/Filter Block,从而有效地利用缓存空间
提高缓存效率:分区 Index/Filter Block 将变得更小,在 Cache 中锁的竞争将进一步降低,提升了高并发下的效率
降低 IO 利用率:当索引/过滤器分区的缓存 Miss 时,只需要从磁盘加载一个小的分区,与读取整个 SST 文件的 Index/Filter Block 相比,这会使磁盘上的负载更小
相关配置:
format_version = 5
index_type = IndexType::kTwoLevelIndexSearch: 使用 partition index
NewBloomFilterPolicy(BITS, false) : 使用 Full Filter
BlockBasedTableOptions::partition_filters = true:使用partition filter, index_type 必须为 kTwoLevelIndexSearch
cache_index_and_filter_blocks = true
pin_top_level_index_and_filter = true
cache_index_and_filter_blocks_with_high_priority = true
pin_l0_filter_and_index_blocks_in_cache = true
optimize_filters_for_memory = true
压缩策略优化
RocksDB 在数据落盘时会对数据进行压缩。下图是不同压缩算法压缩速度和压缩比的测试数据。我们在 PegaDB2.0 上对不同的压缩算法进行了对比测试,发现不同的压缩算法对性能影响非常大,特别是 CPU 资源紧张的情况下,会显著增加长尾延迟。
在 PegaDB2.0 中对 L0 层和 L1 层的 SST 不设置压缩,因为这两层数据量较少,压缩这些层级的数据并不能减少很多磁盘空间,但是可以通过不压缩这些层级的数据来节省 CPU。每个 L0 到 L1 的Compaction 都需要访问 L1 中的所有文件,另外,范围扫描不能使用 Bloom Filter,需要查找 L0 中的所有文件。如果读L0和L1中的数据时不需要解压缩,写 L0 和 L1 中的数据不需要压缩,那么这两种频繁的CPU密集型操作会占用更少的 CPU,相比压缩获得的磁盘空间,收益更大。
对于其他层级,我们使用 LZ4。对于大数据量、低QPS的场景,我们还会将最后一层设置为 ZSTD,进一步降低空间放大,减少成本。
Cache 优化
PegaDB2.0 涉及多个Column Family(以下简称CF)。对于简单数据类型(字符串操作),将数据存到 Metadata CF;复杂数据类型会将元数据存到 Metadata CF中,实际数据存在 Subkey CF 中。默认情况下,不同的CF使用不同的 Block Cache。线上场景复杂,无法预知用户使用的数据类型,也就无法提前给每个 CF 分配合适的 Block Cache 大小。PegaDB2.0 默认为这两个 CF 分配了相同大小的 Block Cache,如果用户使用简单类型和使用复杂类型比例不相当时,会使得 Block Cache 使用效率降低。PegaDB2.0 让不同的CF共享同一个大的 Block Cache,这提高了 Block Cache 的利用率,也提升了 30% 的性能。
方法:不同 BlockBasedTableOptions 使用同一个 Block Cache 的指针。
此外,PegaDB2.0 还引入 Row Cache 应对热点 Key 问题。RocksDB 先检查 Row Cache,再检查 Block Cache,对于存在热点的场景,数据会优先存放在 Row Cache 中,进一步提高 Cache 利用率。
Key-Value 分离
LSM 型的存储引擎会将 Key 和 Value 存放在一起,在 Compaction 的过程中,Key 和 Value 都会被重写一遍,当 Value 较大时,会带来非常严重的写放大。针对此问题,
WiscKey(FAST'16)这篇论文提出了 KV 分离的方案,业界也基于此论文实现了 LSM 型存储引擎的 KV 分离,比如: RocksDB 的 BlobDB、PingCAP 的 Titan 引擎、厂内的 UNDB 所使用的
Quantum 引擎。
RocksDB 6.18 版本重新实现了 BlobDB(RocksDB的KV分离方案),将其集成到 RocksDB 的主逻辑中,并且也一直在完善、优化 BlobDB 相关特性。PegaDB2.0 引入此特性,应对大 Value 的场景。测试显示,当 PegaDB2.0 打开 KV 分离开关时,针对 Value 为10KB 的场景,写性能提升 2.5 倍,读性能并没有衰减; Value 越大,写性能提升幅度越大,Value 为 50KB 场景下,写性能提升了 5 倍。
具体配置:
ColumnFamilyOptions.enable_blob_files = config_->RocksDB.enable_blob_files;
min_blob_size = 4096
blob_file_size = 128M
blob_compression_type = lz4
enable_blob_garbage_collection = true
blob_garbage_collection_age_cutoff = 0.25
blob_garbage_collection_force_threshold = 0.8
未来工作
目前 PegaDB2.0 使用 RocksDB 作为底层 Key Value 存储引擎。未来,我们将抽象出底层存储,使其更加方便的适配其他存储引擎,也便于应用新的硬件、新的协议栈、新的架构。
持久内存的应用
英特尔傲腾持久内存(Optane PMem)弥补了传统 SSD 和 DRAM 之间的空白,以创新的技术提供独特的操作模式,满足各种工作负载的需求。随着高性能存储硬件的发展,传统的存储架构逐渐无法发挥新硬件的性能,传统引擎的架构需要在 PMem上重新调整。
我们团队正在积极跟进新存储硬件的发展,与英特尔团队深入合作,探索在 RocksDB 以及 Redis 场景下如何更好的使用 PMem,进而降低用户成本、提升访问性能。
用户态协议栈
采用用户态协议栈,可以减少系统调用以及处理数据包的开销,降低数据包的频繁内存分配、降低内存拷贝,提高内存使用效率。PegaDB 后续将在底层引入用户态协议栈,进一步提升性能。
计算存储分离
在 Rocksdb Virtual Meetup 2021 上, RocksDB 团队分享了 RocksDB 的计算存储分离方案,其将 SST 文件存储到远程分布式文件系统之上,其团队负责人也透露未来将高优开发计算存储分离相关特性,尽快将此特性在 Facebook 内部落地。
LSM 型数据库需要进行 Compaction 来减少空间放大,Compaction 是 CPU 密集型的操作,传统架构下 Compaction 和上层服务耦合在一起,相互影响,甚至会阻塞上层写入(称为 Write Stall)。RocksDB 引入 Remote Compaction 特性将 CPU 密集型的 Compaction 转移到远程去执行,实现服务计算与存储引擎计算剥离,达到计算资源的极致弹性。为了降低访问远程存储的延迟 RocksDB 引入 Secondary Cache(可以是内存、PMem、NVMe-SSD 组成的多级 Cache),将 Block Cache 淘汰的数据缓存在本地,减少访问远程存储的次数。
未来我们也会持续跟踪 RocksDB 的进展,自研与拥抱开源,双管齐下,尽快推出计算存储分离架构的新版本 PegaDB,积极拥抱云原生,实现计算、存储极致的弹性,进一步降低用户云上成本。 总结
本文重点介绍了基于 RocksDB 的磁盘 Redis 服务是如何使用 RocksDB 高级特性提升性能的。未来,BDRP 团队将持续优化 PegaDB2.0,为大家带来更卓越的性能体验。