这是我参与「第四届青训营 」笔记创作活动的第20天
本次笔记重点内容
- 介绍 LSMT 与存储引擎
- 分析 LSMT 存储引擎的优势与实现
- LSMT 模型理论分析
- LSMT 存储引擎调优以及案例介绍
Log-Structured Merge-Tree——LSMT
通过 Append-only Write + 择机 Compact 来维护索引树的结构。数据先写入 MemTable,MemTable 是内存中的索引,当 MemTable 写到一定阈值后,冻结成为 ImmemTable,任何修改只会在 MemTable 上,所以 ImmemTable 可以被转交给 Flush 线程进行写盘操作而不用担心并发问题。Flush 线程收到 ImmemTable ,在真正执行写盘前会进一步从 ImmemTable 生成 SST(Sorted String Table),其实也就是存储在硬盘上的索引,逻辑上和 ImmemTable 无差别。
为什么采用LSMT模型
HDD VS SSD 时代
HDD时代是顺序操作和随机操作的不对称性,顺序操作远快于随机操作;SSD时代是顺序写和随机写的不对称性,顺序写操作远快于随机写操作。
LSMT存储引擎的优势
- 顺序写模型对于 SSD 设备更友好
- SST 不可修改的特性使得其能使用更加紧凑的数据排列和加上压缩
- 后台延迟 Compact 能更好利用 CPU 多核处理能力,降低前台请求延迟
LSMT存储引擎实现
写实现
RocksDB 在真正执行修改之前会先将变更写入 WAL(Write Ahead Log),WAL 写成功则写入成功,因为如果程序出问题或者重启,可以回放WAL来恢复或者继续之前的写入。多个写入者会选出一个 Leader,由这个 Leader 来一次性写入,这样可以批量聚合请求,避免频繁提交小 IO,批量提交时,Leader 可以同时唤醒其余 Writer,但如果没有批量提交就只能链式唤醒了。写完 WAL 实际还要写 MemTable,RocksDB 在 LevelDB 的基础上又添加了并发 MemTable 写入的优化,由最后一个完成 MemTable 写入的 Writer 执行收尾工作,这样省时,而 RocksDB 为了保证线性一致性,安排了一个 Leader 分配时间戳,每条修改记录都会带着分配到的时间戳,也必须安排一个 Leader 推进当前可见的时间戳。
Get实现
为了加速点查,一般 LSMT 引擎都会在 SST 中嵌入 BloomFilter,BloomFilter 可以 100% 断言一个元素不在集合内,但只能大概率判定一个元素在集合内。【1,10】表示索引块存储数据的区间在1-10之间,顺着绿色的块层层向下查询,如果索引块是 SST,就先查询 BloomFilter,看数据是否有可能在这个 SST 中,有的话则进行进一步查询。
Compact实现
Compact 在 LSMT 中是将 Key 区间有重叠或无效数据较多的 SST 进行合并,以此来加速读取或者回收空间
Level策略
每一个层不允许有 SST 的 Key 区间重合。由于在 LSMT 中,每下一层都会比上一层大 T 倍,每次上下层的合并都一定是一个小 SST 和一个大 SST 进行 Compact,这个从算法的角度来说是低效的,增加了写放大问题。
Tier策略
允许 LSMT 每层有多个区间重合的 SST。当本层区间重合的 SST 到达上限或者本层大小到达阈值时,一次性选择多个 SST 合并推向下层。但是每层的区间内重合的 SST 越多,那么读取的时候需要查询的 SST 就越多。也就是说,Tier 策略是用读放大的增加换取了写放大的减小。
LSMT模型
T: size ratio,每层 LSMT 比上一层大多少,L0 大小为 1,则 L1 大小为 T,L2 为 T^2...
L: level num,预期 LSMT 层数
B: 每个最小的 IO 单位能装载多少条记录
M: 每个 BloomFilter 有多少 bits
N: 每个 BloomFilter 生成时用了多少条 Key
S:区间查询的记录数量
LSMT 引擎调优案例
TerarkDB aka LavaKV 是字节跳动内部基于 RocksDB 深度定制优化的自研 LSMT 存储引擎,其中有完全自研的 KV 分离功能,存储引擎其实存了两类数据,一类是索引,一类是用户输入的数据,对于索引来说,随着记录不断变更,需要维护索引的拓扑结构,因此要不断 Compact,但对于用户存储的数据来说,只要用户没删除,可以一直放着,能读就行,不需要经常跟着 Compact。