LSM-trees是什么
LSM-trees是一种数据结构,很适合用于key-value数据的高效索引和持久化存储。一些广泛使用的存储系统BigTable、LevelDB、RocksDB(可以充当MySQL的存储引擎)都用到了这项技术。
B+树
- MySQL的InnoDB存储引擎使用了B+树
- 拥有平衡的读写性能
- 单次写入需要改变多个节点,这些节点存在硬盘中不同的位置,可能需要进行多次硬盘随机写入
- 一般来说,硬盘的顺序写入速度远快于随机写入
LSM-trees
- 设计哲学是尽量最大化利用硬盘的顺序写入性能
- 是一种持久化键值存储结构,它能为高频率的键值插入(insert)、更新(update)、删除(delete)操作提供高效率的索引
- 一棵LSM-trees包含从C0到Ck多个组件,这些组件的容量依次增加。其中C0常驻内存之中,是一颗排序树(sorted tree)。其余从C1到Ck组件处于硬盘中,每个组件都是单独的B树(append-only B-tree)
- 对于一次键值插入、更新或者删除操作,有以下步骤:
-
- 第一步,键值对首先以追加方式写入log文件中。这是持久化最重要的一步,保证服务器崩溃时数据不丢失,同时顺序写入也保证了写入性能非常好。
- 第二步,键值对会被追加到的C0,由于在内存之中,这个过程非常快。对于用户来讲,写入操作就结束了。接下来的步骤是后台线程异步执行的。
- 第三部,当C0的大小达到阈值时,它会跟硬盘上的C1进行归并排序操作,这个操作被称为compaction。两者合并生成的新文件会被顺序写入硬盘,取代旧版本的C1。
- 第四步,compaction操作同样发生在C1到Ck组件中。当任意Ci的大小达到阈值,都将触发compaction,与下层Ci+1合并。
- 对于update和delete操作,不实际删除失效的值,而是在compaction过程中清除数据。
- 对于一次查询操作,LSM-trees数据结构可能需要查询多个组件
-
-
查询过程从C0开始,C0包含最新鲜的数据
-
如果在C0中未查到对应键值对,存储引擎将会开始查询C1,以此类推
-
因此,为了查询一对键值对,LSM-trees需要从C0查起,往下查找,直到查到目标键值对所在的Ci
-
对于查询不存在的数据,LSM-trees需要查遍所有组件。因此,一般来说,对于查询操作,LSM-trees相对于B-tree耗时更大
-
工程实践
BigTable
-
谷歌公司在Bigtable论文中第一次将LSM-trees数据结构应用到大规模分布式存储系统中,论文中描述了一种构建在谷歌GFS(Google File System,即谷歌公司的分布式文件系统)上的存储
-
-
顺便提一下Google的大数据三驾马车分别是GFS、MapReduce、BigTable,这三篇论文都非常经典
-
-
以键值对的update操作为例
-
-
数据会被先写入到日志中
-
然后写入memtable,memtable是一种内存中的有序结构
-
随着写入操作增多,memtable中的数据量逐渐增大,当数据量达到设定的阈值时,memtable将会被冻结,然后被写入GFS成为SSTable文件(Static Sorted Table),这个过程被称为minor compaction
-
随着键的update和delete操作的增多,SSTable中失效的键值对需要被及时清理。BigTable会定期进行major compaction,读取多张SSTable中的数据,进行归并排序,清除失效数据,然后创建新的SSTable,并最终将数据写回到GFS中
-
LevelDB
源码 github.com/google/leve…
go语言版本源码 github.com/golang/leve…
leveldb-handbook.readthedocs.io/zh/latest/
- 谷歌公司开源的单机键值存储引擎
- 将SSTable组织成小文件层级结构,小文件可以减少单次compaction消耗
- RocksDB是Facebook维护的LevelDB分支,RocksDB是目前工业界被广泛使用的存储引擎
-
-
我司的多款存储产品的底层存储引擎也是在RocksDB的基础上进行构建或改进的
-
MemTable
跳跃表
- 底层是一个普通的有序链表
- 更高层都充当下面链表的索引。有点像实现了二分查找法的链表
- 以查找节点5为例子,只需查找1-4-5。插入和查找时间复杂度都是log(n)
- Redis的有序列表也用到了跳跃表结构
SStable
物理结构
-
每4kb进行压缩,默认使用snappy算法
-
我们公司去年在推用RocksDB取代innodb,主要原因是能够节省70%的存储空间
逻辑结构
-
data block: 用来存储key value数据对;
-
filter block: 用来存储一些过滤器相关的数据(布隆过滤器),用于快速判断某个key是否在这个sstable当中,但是有一定误判率;
-
meta Index block: 用来存储filter block的索引信息(索引信息指在该sstable文件中的偏移量以及数据长度);
-
index block:index block中用来存储每个data block的索引信息;
-
footer: 用来存储meta index block及index block的索引信息;
data block
-
data block中存储的数据是leveldb中的key-value键值对。
-
由于sstable中所有的keyvalue对都是严格按序存储的,用了节省存储空间,leveldb并不会为每一对key-value对都存储完整的key值,而是存储与上一个key非共享的部分,避免了key重复内容的存储。
-
每间隔若干个keyvalue对,将为该条记录重新存储一个完整的key。重复该过程(默认间隔值为16),每个重新存储完整key的点称之为Restart point。
一个entry分为5部分内容:
-
与前一条记录key共享部分的长度;
-
与前一条记录key不共享部分的长度;
-
value长度;
-
与前一条记录key非共享的内容;
-
value内容;
log文件
-
log文件跟memory db是一一对应的,数据同时写入memory db和log,不仅迅速插入内存中的有序结构,而且写入了磁盘,保证数据不丢失。
-
Memory db写入到一定大小时,会被锁定,然后转变成SSTable,这个过程被称之为minor compaction
写入流程
Minor Compaction
-
一次minor compaction非常简单,其本质就是将一个immutable的所有数据持久化到一个磁盘文件中
-
minor compaction的优先级高于major compaction
-
当进行minor compaction的时候有major compaction正在进行,则会首先暂停major compaction
Major Compaction
三种情况触发:
-
当0层文件数超过预定的上限(默认为4个)
-
当level i层文件的总大小超过(10 ^ i) MB
-
当某个文件无效读取的次数过多
-
- Leveldb的作者认为,一个文件一次查询的开销为10ms, 若某个文件的查询次数过多,且查询在该文件中不命中, 那么这种行为就可以视为无效的查询开销,这种文件需要被compaction。
- 但是compaction本身代价也很昂贵。对于一个1MB的文件,其合并开销为
-
-
source层1MB的文件读取
-
source+1层 10-12MB的文件读取
-
source+1层10-12MB的文件写入
-
-
-
compaction1MB文件有25MB的文件IO开销,除以100MB/s的文件IO速度,估计开销为25ms。
-
因此当一个1MB的文件无效查询超过25次时,便可以对其进行合并。
-
读取流程
-
依次尝试从MemTable-Immutable Memtable-SSTable中读取数据
-
Level0层sstable直接从memtable转化,key有重叠,需要读取所有sstable
-
下层level每层sstable都不会重叠,最多只需要读取一个sstable
读写放大问题
什么是读写放大
写(读)放大指的是,当用户向存储引擎写入(读取)数据,存储引擎向硬盘写入(读取)的数据总量大于用户写入(读取)的数据总量。比如,用户写入LevelDB的数据量为10GB,而LevelDB写入硬盘的数据量为100GB以上,这造成了10倍的写放大。
levelDB的读写放大
写放大
-
在LevelDB中,Li层级的SSTable跟Li+1层级进行合并时,需要先读取对应所有的SSTable文件,在内存中进行归并排序,然后生成新的SSTable,并全量写回硬盘。
-
LevelDB中Li+1层级的SSTable容量是Li层级的10倍。在最坏的情况下,Li+1层级的SSTable需要跟10个Li层级的文件进行compaction,从而相同的数据需要重写10次。因此,当数据在两层之间的移动最多会造成10倍的写放大。
-
而LevelDB共有6层,从L0层级转移到L6层级需要转移5次,最坏情况下的写放大倍数达到了50倍。
读放大
读放大问题来源于两方面:
- 第一,为了寻找一对键值对,LevelDB需要查询多个SSTable文件。最差情况下,LevelDB需要查询4个L0层级SSTable,以及L1到L6每层一个文件,共10个文件。
- 第二,为了在一个SSTable文件中寻找一对键值对,LevelDB需要读取多个数据块。如上面章节介绍的,SSTable文件由数据块(data block),布隆过滤器块(filter block),元数据块(meta index block)和索引块(index block)构成。具体来讲,从SSTable中寻找1KB大小的键值对,需要首先读取16KB大小的索引块,判断该数据所在的位置,然后读取4KB大小的布隆过滤器数据块,判断该数据是否存在,最后读取4KB大小的数据块,最多24KB数据。
- 综上,10个SSTable文件乘以每个文件读取24KB,读放大最终可能达到240倍。
几种优化思路
PebblesDB
-
levelDB同一level的sstable严格有序,key不重叠,导致compaction代价极大
-
PebblesDB引入guard概念,guard之内的sstable可以无序,且key可以重叠
-
compaction不需要读取下层数据。(最底层除外)
-
文件在不同level之间移动时只需要重写一次
-
优化了写放大,但是读放大更严重了
LSM-trie
LSM-trie : an lSM-tree-based ultra-large key-value store for small data items
-
将每一个Level均由多个sub-levels替代,以减少写放大。
-
之后在合并的过程中将这些sub-level中的数据进行合并,并将合并后的内容作为下一个level。
-
通过这种方法,数据在不同level间移动时只需要重写一次,所以也就很好的减少了写放大。
-
跟pebblesDB相同,优化了写放大,但是读放大更严重了
WiscKey
WiscKey: Separating Keys from Values in SSD-Conscious Storage
工程实践
pingcap.com/blog-cn/tit…
-
一般情况下键比较小,值比较大
-
WiscKey将键值分开存储,lsm-trees只存键和索引,值存在单独value log里面。
-
写放大优化
-
-
假设写入一对键值,键的大小为8字节,值的大小为1024字节,LSM-trees数据结构的写放大倍数为10倍,由于我们只将键存在LSM-trees,那么实际的写放大倍数仅仅为1.07倍((8 * 10 + 1024 ) / (8 + 1024) = 1.07)。
-
-
读放大优化
-
-
对于一次读取操作,存储引擎需要先从键仓库读取地址,然后再从值仓库获取真实的值。表面上看来这个过程比传统的LSM-trees算法需要多一次磁盘I/O操作,但是,由于在LSM-trees中并不需要存储值,数据量显著减少,存储引擎中LSM-trees的大小显著小于LevelDB,一次查询操作所需要查询的level数量将减少,因此减少了读放大,读取性能将获得提升。
-
如何进行垃圾回收
-
遍历LSM-trees代价太大,遍历value log更高效
-
Value log中记录键信息,去lsm-trees查键值是否失效
-
从尾部开始垃圾回收,失效直接丢弃,有效将数据移到头部
-
如果update/delete操作频繁,就会导致反复GC,数据反复从尾部移动到头部,反而增加了写放大问题
HashKV
HashKV: Enabling Efficient Updates in KV Storage via Hashing
-
将lsm trees本身的log文件和value log文件合二为一
-
根据key的hash值,将value log分成非常多的segment group
-
垃圾回收以segment group为单位,哪个segment group最近update多,就回收哪个
-
check k-v 的有效性
-
-
不需要查找 LSM-tree,segment group 中的 value 都是以 log-structed 形式存在的,那么从 tail 开始,最后面的某个 key 对应的肯定就是 lastest version,前面的就是旧的 可以删除的版本。由于segment group较小,完全可以全量扫描一遍这个 segment,注意从后往前扫描,就可以获取所有有效的value。
-
-
GC性能提升,但是写入的log分成了太多segment,把原来顺序写入的优势给丢掉了
参考链接
- levelDB源码 code.google.com/p/leveldb
- levelDB源码 go语言版 github.com/golang/leve…
- leveldb-handbook.readthedocs.io/zh/latest/
- 字节跳动在 RocksDB 存储引擎上的改进实践
相关论文
- The log-structured merge-tree (LSM-tree)
- Bigtable: A Distributed Storage System for Structured Data
- Skip lists: a probabilistic alternative to balanced trees
- PebblesDB: Simultaneously Increasing Write Throughput and Decreasing Write Amplification in Key-Value Stores
- LSM-trie: An LSM-tree-based Ultra-Large Key-Value Store for Small Data Items
- WiscKey: Separating Keys from Values in SSD-Conscious Storage
- HashKV: Enabling Efficient Updates in KV Storage via Hashing