这是我参与「第四届青训营」笔记创作活动的第17天
本节课程目录:
- LSMT 与存储引擎介绍
- LSMT 存储引擎的优势与实现
- LSMT 模型理论分析
- LSMT 存储引擎调优案例与展望
1. LSMT 与存储引擎介绍
1.1 LSMT 的历史
- LSMT 是 Log-Structured Merge-Tree 的缩写,在 1996 年由 Patrick O‘Neil etc. 提出
- 相较而言 B-Tree 就早得多了,在 1970 年由 Bayer,R; McCreight,E. 提出
- 较早的数据库产品,如 MySQL,PostgresQL 默认均采用 B+Tree(B-Tree 变种)索引。
- 较新的数据库产品,如 TiDB,CockroachDB,默认均采用 LSMT 存储引擎(RocksDB / Pebble)
1.2 LSMT 是什么?
一言以蔽之,通过 Append-only Write + 择机 Compact 来维护索引树的结构。
1.3 存储引擎是什么?
以单机数据库 MySQL 为例,大致可以分为
- 计算层
- 存储层(存储引擎层)
计算层主要负责 SQL 解析/查询优化/计划执行,数据库著名的 ACID 特性,在 MySQL 中全部强依赖于存储引擎。
- Atomicity 原子性
- 依赖于存储引擎 WAL(Redo Log)
- Consistency (Correctness) 一致性
- 需要数据库整体来保证
- Isolation 隔离性
- 依赖于存储引擎提供 Snapshot(有时候会直接说 MVCC)能力。如果上层没有单独的事务引擎的话,也会由存储引擎提供事务能力。一般的是实现是 2PL(2 Phase Lock) + MVCC。
- Durability 持久性
- 依赖于存储引擎确保在 Transaction Commit 后通过操作系统 fsync 之类的接口确保落盘了
除了保障 ACID 以外,存储引擎还要负责:
- 屏蔽 IO 细节提供更好的抽象
- 提供统计信息与 Predicate Push Down 能力
存储引擎不掌控 IO 细节,让操作系统接管,例如使用 mmap,会有如下问题:
- 落盘时机不确定造成的事务不安全
- IO Stall
- 错误处理繁琐
- 无法完全发挥硬件性能
2. LSMT 存储引擎的优势与实现
2.1 LSMT 与 B+Tree 的异同
- 在 B+Tree 中,数据插入是原地更新的
- B+Tree 在发生不平衡或者节点容量到达阙值后,必须立即进行分裂来平衡
- LSMT 与 B+Tree 可以用同一模型描述
- 从高层次的数据结构角度来看,二者没有本质的不同,可以互相转化
- 工程实践上还是用 LSMT 来表示一个 Append-only 和 Lazy Compact 的索引树,B+Tree 来表示一个 Inplace-Update 和 Instant Compact 的索引树
- Append-only 和 Lazy Compact 这两个特性更符合现代计算机设备的特性
2.2 为什么要采用 LSMT 模型
- 在计算机存储乃至整个工程界都在利用 Indirection 处理资源的不对称性
- 存储引擎面对的资源不对称性在不同时期是不同的
HDD 时代
顺序操作和随机操作的不对称性
由于机械硬盘需要磁盘旋转和机械臂移动来进行读写,顺序写吞吐是随机读的 25 倍
SSD 时代
顺序写和随机写的不对称性
由于 SSD 随机写会给主控带来 GC 压力,顺序写吞吐是随机写的 6 倍
- HDD 时代,顺序操作远快于随机操作
- SSD 时代,顺序写操作远快于随机写操作
无论对于 HDD 还是 SSD,顺序写都是一个很好的特质,LSMT 符合这一点,B+Tree 则依赖原地更新,会导致随机写。
2.3 LSMT 存储引擎的实现,以 RocksDB 为例
RocksDB 是一款十分流行的开源 LSMT 存储引擎,最早来自 Facebook(Meta),应用于 MyRocks,TiDB 等数据库。
2.3.1 LSMT 存储引擎的实现 - Write
- RocksDB 写入流程主要有两个优化,批量 WAL 写入(继承自 LevelDB)与并发 MemTable 更新
- RocksDB 在真正执行修改之前会先将变更写入 WAL,WAL 写成功则写入成功
- 多个写入者会选出一个 Leader,由这个 Leader 来一次性写入 WAL,避免小 IO
- 不要求 WAL 强制落盘时,批量提交亦有好处,Leader 可以同时唤醒其余 Writer,降低了系统线程调度开销
- 没有批量提交就只能链式唤醒
- 链式唤醒加大前台延迟
- 写完 WAL 还要写 MemTable
- RocksDB 在继承 LevelDB 的基础上又添加了并发 MemTable 写入的优化
- WAL 一次性写入完成后,唤醒所有 Writer 并行写入 MemTable
- 由最后一个完成 MemTable 写入的 Writer 执行收尾工作
2.3.2 LSMT 存储引擎的实现 - Snapshot & SuperVision
- RocksDB 的数据由 3 部分组成,MemTable / ImmemTable / SST。直接持有这三部分数据并且提供快照功能的组件叫做 SuperVersion。
- RocksDB 的 MemTable 和 SST 的释放与删除都依赖于引用计数,SuperVersion 不释放,对应的 MemTable 和 SST 就不会释放。对于读取操作来说,只要拿着这个 SuperVersion,从 MemTable 开始一级一级向下,就能查到记录。拿着 SuperVersion 不释放,等于是拿到了快照。
-
如果所有读者都给 SuperVersion 的计数 +1,读完后再 -1,那么这个原子引用计数器就会成为热点。CPU 在多核之间同步缓存是有开销的,核越多开销越大
-
为了让读操作更好的 scale,RocksDB 做了一个优化是 Thread Local SuperVersion Cache
-
没有 Thread Local 缓存时,读取操作要频繁 Acquire 和 Release SuperVersion
-
CPU 缓存不友好
-
有 Thread Local 缓存时,读取只需要检查一下 SuperVersion 并标记缓存正在使用即可,可以看出多核之间的交互就仅剩检查 SuperVersion 缓存是否过期了
-
CPU 缓存友好
2.3.3 LSMT 存储引擎的实现 - Get & BloomFilter
- RocksDB 的读取在大框架上和 B+ Tree 类似,就是层层向下
- 相对于 B+Tree,LSMT 点查需要访问的数据块更多。为了加速点查,一般 LSMT 引擎都会在 SST 中嵌入 BloomFilter
2.3.4 LSMT 存储引擎的实现Compact - Level
Compact 在 LSMT 中是将 Key 区间有重叠或无效数据较多的 SST 进行合并,以此来加速读取或者回收空间。Compact 策略可以分为两大类,Level 和 Tier。
Level
Level 策略直接来自于 LevelDB,也是 RocksDB 的默认策略。每一个层不允许有 SST 的 Key 区间重合。当用户写入的 SST 加入 L0 的时候会和 L0 里区间重叠的 SST 进行合并。当 L0 的总大小到达一定阈值时,又会从 L0 挑出 SST,推到 L1,和 L1 里 Key 区间重叠的 SST 进行合并。Ln 同理。
Tier
Tier 策略允许 LSMT 每层有多个区间重合的 SST,当本层区间重合的 SST 到达上限或者本层大小到达阈值时,一次性选择多个 SST 合并推向下层。Tier 策略理论上 Compact 效率更高,因为参与 Compact 的 SST 大小预期都差不多大,更接近于完美的 Merge Sort。
3. LSMT 模型理论分析
3.1 Cloud-Native LSMT Storage Engine - HBase
RocksDB 是单机存储引擎,那么现在都说云原生,HBase 比 RocksDB 就更「云」一些,SST 直接存储于 HDFS 上,Meta 信息 RocksDB 自己管理维护于 Manifest 文件,HBase 放置于 ZK。二者在理论存储模型上都是 LSMT。
3.2 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:区间查询的记录数量
Level
-
Write: 每条记录抵达最底层需要经过 L 次 Compact,每次 Compact Ln 的一个小 SST 和 Ln+1 的一个大 SST。设小 SST 的大小为 1,那么大 SST 的大小则为 T,合并开销是 1+T,换言之将 1 单位的 Ln 的 SST 推到 Ln+1 要耗费 1+T 的 IO,单次 Compact 写放大为 T。每条记录的写入成本为 1/B 次最小单位 IO。
-
O(Write_Level) = L * T * 1/B = T * L/B
-
Point Lookup: 对于每条 Key,最多有 L 个重叠的区间,每个区间都有 BloomFilter,失效率为 e^(-M/N),只有当 BloomFilter 失效时才会访问下一层。
-
O(PointLookup_Level) = L * c^(-M/N)
Tier
-
Write: 每条记录抵达最底层前同样要经过 L 次 Compact,每次 Compact Ln 中 T 个相同尺寸的 SST 放到 Ln+1。设 SST 大小为 1,那么 T 个 SST Compact 的合并开销是 T,换言之将 T 单位的 Ln 的 SST 推到 Ln+1 要耗费 T 的 IO,单次 Compact 的写放大为 T / T = 1。每条记录的写入成本为 1/B 次最小单位 IO。
-
O(Write_Tier) = L * 1 * 1/B = L/B
-
Point Lookup:对于每条 Key,有 L 层,每层最多有 T 个重叠区间的 SST,对于整个 SST 来说有 T * L 个可能命中的 SST,乘上 BloomFilter 的失效率即可得结果。
-
O(PointLoopup_Tier) = L * T * e^(-M/N)
Tier 策略降低了写放大,增加了读放大和空间放大
Level 策略增加了写放大,降低了读和空间放大。
4. LSMT 存储引擎调优案例与展望
4.1 LSMT 存储引擎调优案例 - TerarkDB
- TerarkDB aka LavaKV 是字节跳动内部基于 RocksDB 深度定制优化的自研 LSMT 存储引擎,其中完全自研的 KV 分离功能,上线后取得了巨大的收益。
- KV 分离受启发于论文 WiscKey: Separating Keys from Values in SSD-conscious Storage,较长的记录的 Value 单独存储,
4.2 LSMT 存储引擎调优案例 - TerarkDB & Abase & ByteGraph
- 图存储场景描述
- Key size :20B ~ 30B
- Value size:数十 KB 级别
- 写多读少
- 收益结论:
- 延迟大幅度降低,长尾消失,扛住了比 RocksDB 高 50% 的负载。
4.3 LSMT 存储引擎调优案例 - TerarkDB & Flink
- 收益结论:
- 平均 CPU 开销在 3个作业上降低了 26%~39%
- 峰值 CPU 开销在广告作业上有明显的收益,降低了 67%
- live_feed_head 作业上峰值 CPU 开销降低 43%
- multi_trigger 受限于分配的CPU 资源,没有观察到峰值 CPU 收益( 平均 CPU 开销降低 39% )
- 平均容量开销在 3 个作业上降低了17%~31.2%
- 直播业务某集群容量不收缩,TerarkDB 的 schedule TTL GC 彻底解决了该问题
- 收益说明:
- 平均 CPU 收益主要来自于,开启 KV 分离,减少写放大
- 容量收益主要来自于 schedule TTL GC,该功能可以根据 SST 的过期时间主动发起Compaction,而不需要被动的跟随 LSM-tree 形态调整回收空间
4.4 存储引擎最新发展趋势 - 新硬件
在新的硬件(SMR HDD,Zoned SSD,PMem)上设计存储引擎
e.g.
MatrixKV: Reducing Write Stalls and Write Amplification in LSM-tree Based KV Stores with Matrix Container in NVM
4.5 存储引擎最新发展趋势 - 新模型
在现有模型上添加新的扩展
e.g.
KV 分离,WiscKey: Separating Keys from Values in SSD-conscious Storage
REMIX: Efficient Range Query for LSM-trees
4.6 存储引擎最新发展趋势 - 新参数/新工况
发现现有模型在某些工况中表现不够好,并调整现有参数
e.g.
The Log-Structured Merge-Bush & the Wacky Continuum