优化 RocksDB 的空间放大策略

背景知识

MyRocks 编码方式

在MyRocks中,每个MySQL表的行存储为一个RocksDB键值对:

  • 主键(Primary index)编码在 key 中,并且所有其他行数据编码在 value 中。

    • 主键的编码方式遵循 MySQL 的数据类型规则,例如整数按小端序编码,字符串按字节序编码。
  • 辅助索引编码(Secondary Index),不一定唯一,被以独立的键值对存储;key 是 索引ID + 索引列值 + 主键值 value 留空;

    • 副索引查找转换为RocksDB范围查询
    • 索引 ID 用于区分不同的索引,确保不同索引的键不会冲突。

所有的RocksDB键都前缀了一个4字节的表ID或索引ID以便多个表或索引可以在一个RocksDB键空间共存。最后,一个随每次写入操作递增的世界序列号,被每个键值对存储以支持快照。快照用于实现多版本并发控制,反过来允许我们在RocksDB中实现ACID事务。

解决写入放大的手段

​动态调整 LSM 层级大小
如果为每个级别指定固定大小,那么在实践中,存储在最后一个级别的数据大小不太可能是前一个级别目标大小的10倍。 在最坏的情况下,存储在最后一个级别的数据大小仅略大于前一个级别的目标大小,在这种情况下,空间放大将大于2。 然而,如果我们动态调整每个级别的大小为下一个级别数据大小的1/10,那么空间放大将减少到小于1.111…. 级别大小乘数是LSM树中的一个可调参数。上面,我们假设它是10。级别大小乘数越大,空间放大和读放大越低,但写放大越高。因此,这个选择代表了一种权衡。 对于大多数Facebook生产环境中的RocksDB安装,使用的级别大小乘数为10,尽管有少数实例使用8。
压缩
  • 前缀压缩,前缀编码应用于键,通过不写入与前一个键重复的前缀部分。我们发现这在实践中可以减少3%至17%的空间需求,具体取决于数据工作负载。
  • ​序列ID垃圾回收,
  • 数据压缩,压缩作用在 block 级别,
  • 字典压缩,通常在最后一个级别使用强压缩算法(如zlib或Zstandard),尽管这会带来更高的CPU开销,因为大部分(接近90%)的数据位于该级别,而只有少量的读写操作会访问它。在各种使用场景中,对最后一级应用强压缩算法相比仅使用轻量级压缩,可以额外节省15%至30%的存储空间。 相反地,我们在0-2级不使用任何压缩,以允许更低的读取延迟,尽管这会带来更高的空间和写入放大,因为这些级别只占用总存储空间的一小部分。从第3级到最后一级使用轻量级压缩(如LZ4或Snappy),因为其CPU开销是可接受的,同时它减少了空间和写入放大。对位于前三个级别的数据的读取更可能位于操作系统缓存的(未压缩的)文件块中,因为这些块被频繁访问。然而,对第2级以上级别数据的读取将必须进行解压缩,无论它们是否位于操作系统文件缓存中(除非它们也位于RocksDB块缓存中)。