这是我参与「第四届青训营 」笔记创作活动的第15天。
上节了解了数据如何在文件层面组织起来的。
在文件之上的单存储层面这些数据文件又是如何存储的?
课程目录
-
介绍 LSMT 与存储引擎
-
分析 LSMT 存储引擎的优势与实现,LSMT是目前最适合现代计算机硬件特性的索引结构
-
LSMT 模型理论分析,从实践完善了理论模型
-
LSMT 存储引擎调优以及案例介绍
LSMT 与存储引擎
LSMT的历史
LSMT
通过Append-only Write + 择机Compact来维护结构的索引树
存储引擎
以单机sql为例
大致可以分为:
- 计算层
- 存储层(存储引擎层)
计算层主要负责SQL解析/查询优化/计划执行
数据库的ACID特性,在mYSQL全部依赖与存储引擎
ACID是什么/存储引擎那些组件保证了这些特性
- Atomicity
原子性依赖于存储引擎 WAL(Redo Log)
- Consistency (Correctness)
一致性需要数据库整体来保证
- Isolation
隔离性依赖于存储引擎提供 Snapshot(有时候会直接说 MVCC)能力。如果上层没有单独的事务引擎的话,也会由存储引擎提供事务能力。一般的是实现是 2PL(2 Phase Lock) + MVCC。2PL 可以简单理解为对所有需要修改的资源上锁。
- Durability
持久性依赖于存储引擎确保在 Transaction Commit 后通过操作系统 fsync 之类的接口确保落盘了
存储引擎还有很多特性
LSMT 与B+Tree的异同
先简单回顾下经典 B+Tree 写入流程
有一 Order 为 5 的 B+Tree,目前存有 (10, 20, 30, 40),继续插入 15,节点大小到达分裂阈值 5,提取中位数 20 放入新的内部节点,比 20 大的 (30, 40) 移入新的叶节点。这个例子虽然简单,但是涉及了 B+Tree 最核心的两个变化,插入与分裂。
在 B+Tree 中,数据插入是原地更新的,装有 (10, 20, 30, 40) 的节点在插入和分裂后,原节点覆写成 (10, 15)。此外,B+Tree 在发生不平衡或者节点容量到达阈值后,必须立即进行分裂来平衡。
反观 LSMT,数据的插入是追加的(Append-only),当树不平衡或者垃圾过多时,有专门 Compact 线程进行 Compact,可以称之为延迟(Lazy)的。
思考一个问题,B+Tree 能不能把部分数据采用追加写,然后让后台线程去 Compact 维护树结构呢?或者 LSMT 能不能只有一层 L0,ImmemTable 给 Flush 线程之后,立马 Compact 呢?
答案是都可以。前者的做法叫做 Fractal tree(分型树)应用在了 TokuDB 中。后者的做法在 OceanBase 或者类似对延迟有严格要求的在线数据库中得到了应用,因为 LSMT 层数越少,读取越快。
所以从高层次的数据结构角度来看,B+Tree 和 LSMT 并没有本质的不同,可以统一到一个模型里,根据 Workload 的不同互相转换。
为什么LSMT模型
LSMT 存储引擎的优势
- 相对于 B+Tree 的优势
我们在前文已经阐述了 LSMT 与 B+Tree 的异同,在这里总结下 LSMT 的优势。
-
顺序写模型对于 SSD 设备更友好
-
SST 不可修改的特性使得其能使用更加紧凑的数据排列和加上压缩
-
后台延迟 Compact 能更好利用 CPU 多核处理能力,降低前台请求延迟
- 相对于 HashTable 的优势
LSMT 存储引擎是有序索引抽象,HashTable 是无序索引抽象。无序索引是有序索引的真子集。LSMT 相比于 HashTable 更加通用。HashTable 能处理点查请求,LSMT 也能,但 LSMT 能处理 TopK 请求,但 HashTable 就不行了。为了避免维护多套存储引擎,绝大多数数据库都直接采用一套有序的存储引擎而非针对点查和顺序读取分别维护两个引擎。
LSMT存储引擎的实现,以RocksDB为例
RocksDB写入流程主要有两个优化,批量WAL写入(集成自LevelDB)与并发MemTable更新
RocksDB在真正执行修改之前会先将变更写入WAL,WAL写成功则写入成功
RocksDB WAL 写入流程继承自 LevelDB。LevelDB 在 WAL 写入主要做的一个优化是多个写入者会选出一个 Leader,由这个 Leader 来一次性写入。这样的好处在于可以批量聚合请求,避免频繁提交小 IO。
但很多业务其实不会要求每次 WAL 写入必须落盘,而是写到 Kernel 的 Page Cache 就可以,Kernel 自身是会聚合小 IO 再下刷的。这时候,批量提交的好处就在于降低了操作系统调度线程的开销。
批量提交时,Leader 可以同时唤醒其余 Writer。
如果没有批量提交就只能链式唤醒了,这样造成延迟较高。
LSMT存储引擎的实现-Write
WAL一次性写入完成之后,唤醒所有Writer并行写入MenTable,
最后一个完成MenTable写入的Writer执行收尾工作
LSMT存储引擎的实现-Snapshot&SuperVision
RocksDB 的数据由 3 部分组成,MemTable / ImmemTable / SST。直接持有这三部分数据并且提供快照功能的组件叫做 SuperVersion。
RocksDB 的 MemTable 和 SST 的释放与删除都依赖于引用计数,SuperVersion 不释放,对应的 MemTable 和 SST 就不会释放。对于读取操作来说,只要拿着这个 SuperVersion,从 MemTable 开始一级一级向下,就能查询到记录。那么拿着 SuperVersion 不释放,等于是拿到了快照。
如果所有读者开始操作前都给 SuperVersion 的计数加 1,读完后再减 1,那么这个原子引用计数器就会成为热点。
LSMT存储引擎的实现-Get&BloomFilter
由于 LSMT 是延迟 Compact 的且 SST 尺寸(MB 级别)比 B+Tree Node (KB 级别)大得多。所以相对而言,LSMT 点查需要访问的数据块更多。为了加速点查,一般 LSMT 引擎都会在 SST 中嵌入 BloomFilter,例如 RocksDB 默认的 BlockBasedTable。BloomFilter 可以 100% 断言一个元素不在集合内,但只能大概率判定一个元素在集合内。
RocksDB 的读取在大框架上和 B+ Tree 类似,就是层层向下。[1, 10] 表示这个索引块存储数据的区间在 1 - 10 之间。索引块可以是 MemTable / ImmemTable / SST,它们抽象上是一样的。查询 2,就是顺着标绿色的块往下。如果索引块是 SST,就先查询 BloomFilter,看数据是否有可能在这个 SST 中,有的话则进行进一步查询。
LSMT存储引擎的实现- Compact-Level
Compact 在 LSMT 中是将 Key 区间有重叠或无效数据较多的 SST 进行合并,以此来加速读取或者回收空间。Compact 策略可以分为两大类。
Level 策略直接来自于 LevelDB,也是 RocksDB 的默认策略。每一个层不允许有 SST 的 Key 区间重合。当用户写入的 SST 加入 L0 的时候会和 L0 里区间重叠的 SST 进行合并。当 L0 的总大小到达一定阈值时,又会从 L0 挑出 SST,推到 L1,和 L1 里 Key 区间重叠的 SST 进行合并。Ln 同理。
由于在 LSMT 中,每下一层都会比上一层大 T 倍(可配置),那么假设用户的输入是均匀分布的,每次上下层的合并都一定是一个小 SST 和一个大 SST 进行 Compact。这个从算法的角度来说是低效的,增加了写放大,具体理论分析会在之后阐述,这里可以想象一下 Merge Sort。Merge Sort 要效率最高,就要每次 Merge 的时候,左右两边的数组都是一样大。
实际上,RocksDB 和 LevelDB 都不是纯粹的 Level 策略,它们将 L0 作为例外,允许有 SST Key 区间重叠来降低写放大。
Tier 策略允许 LSMT 每层有多个区间重合的 SST,当本层区间重合的 SST 到达上限或者本层大小到达阈值时,一次性选择多个 SST 合并推向下层。Tier 策略理论上 Compact 效率更高,因为参与 Compact 的 SST 大小预期都差不多大,更接近于完美的 Merge Sort。
Tier 策略的问题在于每层的区间内重合的 SST 越多,那么读取的时候需要查询的 SST 就越多。Tier 策略是用读放大的增加换取了写放大的减小。
LSMT模型理论分析
RocksDB 是单机存储引擎,那么现在都说云原生,HBase 比 RocksDB 就更「云」一些,SST 直接存储于 HDFS 上,Meta 信息 RocksDB 自己管理维护于 Manifest 文件,HBase 放置于 ZK。二者在理论存储模型上都是 LSMT。
算法复杂度研究,这里就不写了,以后再学吧
LSMT 引擎调优案例
LSMT#存储引擎调优案例 - TerarkDB
TerarkDB aka LavaKV 是字节跳动内部基于 RocksDB 深度定制优化的自研 LSMT 存储引擎,其中完全自研的 KV 分离功能,上线后取得了巨大的收益。
KV 分离受启发于论文 WiscKey: Separating Keys from Values in SSD-conscious Storage, 较长的记录的 Value 单独存储,避免 Compact 过程中频繁挪动这些数据。做法虽然简单,但背后的原理却十分深刻。存储引擎其实存了两类数据,一类是索引,一类是用户输入的数据。对于索引来说,随着记录不断变更,需要维护索引的拓扑结构,因此要不断 Compact,但对于用户存储的数据来说,只要用户没删除,可以一直放着,放哪里不重要,能读就行,不需要经常跟着 Compact。只要 Value 足够长,更少 Compact 的收益就能覆盖 KV 分离后,额外维护映射关系的开销。
Abase 图存储场景使用 TerarkDB
-
图存储场景描述
- Key size :20B ~ 30B
- Value size:数十 KB 级别
- 写多读少
-
收益结论:
延迟大幅度降低,长尾消失,扛住了比 RocksDB 高 50% 的负载。
Flink 流计算场景使用 TerarkDB
- 收益结论:
-
平均 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 形态调整回收空间
存储引擎最新发展趋势
新硬件
在新的硬件(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
###3 新模型
在现有模型上添加新的扩展,
e.g.
KV 分离,WiscKey: Separating Keys from Values in SSD-conscious Storage
REMIX: Efficient Range Query for LSM-trees
新参数 / 新工况
发现现有模型在某些工况中表现不够好,并调整现有参数,
e.g.
The Log-Structured Merge-Bush & the Wacky Continuum
标题:LSMT 存储引擎浅析 | 青训营笔记
网址:juejin.cn/