《LSMT 存储引擎浅析》|青训营笔记

96 阅读9分钟

这是我参与「第四届青训营」笔记创作活动的第17天

本节课程目录:

  1. LSMT 与存储引擎介绍
  2. LSMT 存储引擎的优势与实现
  3. LSMT 模型理论分析
  4. 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 来维护索引树的结构。

bb13d49d1ee247caa0fbf7eea2dd829a~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

1.3 存储引擎是什么?

以单机数据库 MySQL 为例,大致可以分为

  • 计算层
  • 存储层(存储引擎层)

计算层主要负责 SQL 解析/查询优化/计划执行,数据库著名的 ACID 特性,在 MySQL 中全部强依赖于存储引擎。

1034e4f2e54648a9890e2b2ef4c864a2~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • 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 在发生不平衡或者节点容量到达阙值后,必须立即进行分裂来平衡

5632b3c874e64b61aeb04b6048fa429a~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • LSMT 与 B+Tree 可以用同一模型描述
  • 从高层次的数据结构角度来看,二者没有本质的不同,可以互相转化
  • 工程实践上还是用 LSMT 来表示一个 Append-only 和 Lazy Compact 的索引树,B+Tree 来表示一个 Inplace-Update 和 Instant Compact 的索引树
  • Append-only 和 Lazy Compact 这两个特性更符合现代计算机设备的特性

666264aeadd84680a3a0bff6529e956d~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

2.2 为什么要采用 LSMT 模型

  • 在计算机存储乃至整个工程界都在利用 Indirection 处理资源的不对称性
  • 存储引擎面对的资源不对称性在不同时期是不同的

HDD 时代

顺序操作和随机操作的不对称性

23395f9ae51b4e378d5c50bea403d6c6~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png 由于机械硬盘需要磁盘旋转和机械臂移动来进行读写,顺序写吞吐是随机读的 25 倍

SSD 时代

顺序写和随机写的不对称性

7ce537e8f3f64c46a173c7945b68b4c6~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png 由于 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 写成功则写入成功

image.png

  • 多个写入者会选出一个 Leader,由这个 Leader 来一次性写入 WAL,避免小 IO
  • 不要求 WAL 强制落盘时,批量提交亦有好处,Leader 可以同时唤醒其余 Writer,降低了系统线程调度开销

3473f28671864e5d8324bc3aa358b9b7~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • 没有批量提交就只能链式唤醒
  • 链式唤醒加大前台延迟

cd6ecc5322e64fe5a8d5569800c2071d~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • 写完 WAL 还要写 MemTable
  • RocksDB 在继承 LevelDB 的基础上又添加了并发 MemTable 写入的优化

image-2.png

  • WAL 一次性写入完成后,唤醒所有 Writer 并行写入 MemTable
  • 由最后一个完成 MemTable 写入的 Writer 执行收尾工作

38d54d139f5d4458bb2bd3b0953b6aa6~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

2.3.2 LSMT 存储引擎的实现 - Snapshot & SuperVision

  • RocksDB 的数据由 3 部分组成,MemTable / ImmemTable / SST。直接持有这三部分数据并且提供快照功能的组件叫做 SuperVersion。
  • RocksDB 的 MemTable 和 SST 的释放与删除都依赖于引用计数,SuperVersion 不释放,对应的 MemTable 和 SST 就不会释放。对于读取操作来说,只要拿着这个 SuperVersion,从 MemTable 开始一级一级向下,就能查到记录。拿着 SuperVersion 不释放,等于是拿到了快照。

fb0f27aad2594ce4a9b26b9f1af17889~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • 如果所有读者都给 SuperVersion 的计数 +1,读完后再 -1,那么这个原子引用计数器就会成为热点。CPU 在多核之间同步缓存是有开销的,核越多开销越大

  • 为了让读操作更好的 scale,RocksDB 做了一个优化是 Thread Local SuperVersion Cache

  • 没有 Thread Local 缓存时,读取操作要频繁 Acquire 和 Release SuperVersion

  • CPU 缓存不友好 6cda12f62f604228be8ac269bca68e81~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

  • 有 Thread Local 缓存时,读取只需要检查一下 SuperVersion 并标记缓存正在使用即可,可以看出多核之间的交互就仅剩检查 SuperVersion 缓存是否过期了

  • CPU 缓存友好 35f6629620e649fcbd9dacfe199a8e4b~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

2.3.3 LSMT 存储引擎的实现 - Get & BloomFilter

  • RocksDB 的读取在大框架上和 B+ Tree 类似,就是层层向下
  • 相对于 B+Tree,LSMT 点查需要访问的数据块更多。为了加速点查,一般 LSMT 引擎都会在 SST 中嵌入 BloomFilter

9246ba0872984fb6874f83b568fb2ce7~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

2.3.4 LSMT 存储引擎的实现Compact - Level

Compact 在 LSMT 中是将 Key 区间有重叠或无效数据较多的 SST 进行合并,以此来加速读取或者回收空间。Compact 策略可以分为两大类,Level 和 Tier。

Level

a6aae5dad71543e190fb7d3caf9026fc~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

Level 策略直接来自于 LevelDB,也是 RocksDB 的默认策略。每一个层不允许有 SST 的 Key 区间重合。当用户写入的 SST 加入 L0 的时候会和 L0 里区间重叠的 SST 进行合并。当 L0 的总大小到达一定阈值时,又会从 L0 挑出 SST,推到 L1,和 L1 里 Key 区间重叠的 SST 进行合并。Ln 同理。

Tier

b9c44e306a8747418c44ca2bf18c6aaa~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

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。

ed763b2bff2e4903a10fb40b8b06d1e6~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

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:区间查询的记录数量

11fded4b459d436992f076706bb481dc~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

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 单独存储,

image-3.png

4.2 LSMT 存储引擎调优案例 - TerarkDB & Abase & ByteGraph

  • 图存储场景描述
    • Key size :20B ~ 30B
    • Value size:数十 KB 级别
    • 写多读少
  • 收益结论:
    • 延迟大幅度降低,长尾消失,扛住了比 RocksDB 高 50% 的负载。

4.3 LSMT 存储引擎调优案例 - TerarkDB & Flink

  • 收益结论:
  1. 平均 CPU 开销在 3个作业上降低了 26%~39%
  2. 峰值 CPU 开销在广告作业上有明显的收益,降低了 67%
    1. live_feed_head 作业上峰值 CPU 开销降低 43%
    2. multi_trigger 受限于分配的CPU 资源,没有观察到峰值 CPU 收益( 平均 CPU 开销降低 39% )
  3. 平均容量开销在 3 个作业上降低了17%~31.2%
  4. 直播业务某集群容量不收缩,TerarkDB 的 schedule TTL GC 彻底解决了该问题
  • 收益说明:
  1. 平均 CPU 收益主要来自于,开启 KV 分离,减少写放大
  2. 容量收益主要来自于 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