lsm-tree的背景、定义和适用场景本文不再详述。本文意在论述LSM的存储引擎、工业取舍以及发展现状。
read amplification
):一次读取操作需要访问磁盘的次数,主要由compaction策略引入。可以引入cache和bloom filter优化。write amplification
):一次写操作带来的数据落盘的次数。在LSM-Tree中可由某个key数据在磁盘上存储的数量这个指标体现,主要是由compaction策略引入的。space amplification
):实际空间占用/理论数据量
,LSM-Tree的架构导致空间放大必然存在run
:借用wiki中的解释:
The on-disk data is organized into sorted runs of data. Each run contains data sorted by the index key. A run can be represented on disk as a single file, or alternatively as a collection of files with non-overlapping key ranges. To perform a query on a particular key to get its associated value, one must search in the Level 0 tree and each run.
LSM-Tree存储引擎的工作流程如下:
从上面的存储流程可以看出,LSM-Tree的性能主要取决于Compaction策略,它主要分为两类,tiered compaction和leveled compaction
tiered适合write-intensive workload,有较低的写放大,缺点是读放大和空间放大较高。
tiered compaction的策略是:memtable达到阈值之后刷入下一层(落盘)作为一个run,每层的run达到一定数量之后,将当前层所以的run进行compaction并刷入下一层,循环往复。这种策略给人直观的感受是,越下层的run越大,当然实际的策越要比描述的复杂,因为run与run之间重叠的部分是不确定的,有可能完全重叠,也有可能完全不重叠。
STCS
最严重的问题了。Scylla的文章通过实验论证了这个问题time-series data
。key是时间,几乎是恒定递增的,因此越是下层,run越是不重叠,写入和查询的策略都可以做对应的优化。leveled compaction
和上面的STCS相比,降低了空间放大和读放大,比较适合read-intensive workload,当然这个也只是相对而言,如果真的是读很多,不如用B+ Tree。
leveled compaction
每层只维护一个run,按照key排序可以分为多个partition(SSTable file)。每层容量达到一定限制后,选择某个sstable与上层merge。
leveled compaction
上层与下层merge的过程有可能涉及到上下层所有的数据,可能造成上层run全量重写,导致写入放大,但是由于每个run可以分为多个partition(sstable),因此可以节约部分临时空间。不同于leveled,Leveled-N允许每层多个Sorted run。Dostoevsky paper
引入了一种Fluid LSM
:允许最高层只有一个run,而其它层可以有多个run。
这里讨论scylladb实现的Hybrid模式。主要体现在这篇文章,其希望推出一种模式,可以吸纳tiered和leveled两者的优点。scylladb引入Hybrid主要为了优化tiered糟糕的空间放大。我推测主要有以下几点优化:
这里主要讲scylladb/Cassandra针对时序数据引入的Time-Window compaction,时序数据有以下特点:
这种数据状况下,通过将不同的时间分配在不同的Time-Window中,将SSTable分配成了一个个time bucket,实现形式上采用tiered compaction,不同的time bucket不会合并(优化读操作)。由于overlap并不多,所以compaction压力不大,空间放大也不大。由于数据分布较好,查询也较方便。
scylladb的文章总结了以下表格:
workload | Size-Tiered | Leveled | Hybrid | Time-Window |
---|---|---|---|---|
Write-only | 2x peak space | 2x writes | Best | - |
Overwrite(指同一个key多次update) | Huge peak space | write amplification | high peak space bug not like size-tiered | - |
Read-mostly, few updates | read amplification | Best | read amplification | - |
Read-mostly, but a lot of updates | read and space amplification | write amplification may overwhelm | read amplification | - |
Time series | write,read,and space amplification | write and space amplification | write and read amplification | best |
顾名思义,levelDB使用leveled compaction
,其CRUD流程如下:
l0
是tiered
,对于每个run,key的范围有重叠。其它层是leveled
Version::Get
,逐层向下搜索。搜索方法的策略是并不是遍历每一个sstable,而是先看需要查找的key是否落在sstable的范围内,如果落在,对sstable搜索。O(1)
,反向遍历时间复杂度是O(log(N))
Prev()
都要重新定位MaybeScheduleCompaction()
函数判断。levelDB代码清晰,代码量少,阅读方便,不过缺少大规模工程应用场景。
LSM-Tre工业级实现,和levelDB相比,参数更多,优化更多,使用更灵活,Features Not in LevelDB这篇文章介绍了RocksDB相对于levelDB所做的优化,我选择一些优点列举如下:
Options.prefix_extractor
read-only
模式以优化读速度Column Families
,可以设置某个kv归并为一个column family列族,如{cf1, key1, value1}
,列族之间可以相互隔离(不共享memtable和sstable)RocksDB会在某种情况下限制前台写请求的速度,称为Write Stalls。产生的原因不外乎因为某种原因,compaction的速率赶不上前台write的速率了。这种现象如果持续下去,会产生以下结果:
这种情况下需要将前台写请求降速,直到compaction执行到某种可以继续高效处理前台读写请求的程度。
一般情况下,Write Stall产生的原因有以下几种可能:
scyllaDB/cassandra默认采用tiered,可以配置为levled、Hybrid或Time-Window。只有Hybrid是ScyllaDB独有。实现细节如上所述
HBase是一个分布式,版本化,面向列的开源数据库(面向列族Column Family
,在)、分布式持久化KV数据库。HDFS(仅支持追加写)为Hbase提供可靠的底层数据存储服务,MapReduce为Hbase提供高性能的计算能力,Zookeeper为Hbase提供稳定服务和Failover机制,因此我们说Hbase是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。Hbase适合存储PB级别的海量数据,虽然当个请求是时延不低,但是可以通过并行请求增大吞吐,并且单个IO的时延下降并不多。
架构和bigtable比较类似,通过key-range,将数据水平切分到多台服务器上,数据持久化在HDFS中。这点和现代的应用如tidb略有不同,其是将数据切分之后通过一致性协议存储在多个RSM上,每个RSM的数据引擎为LSM-Tree
Hbase 技术细节笔记(上)
hbase在实现中,是把整个内存在一定阈值后,flush到disk中,形成一个file,这个file的存储也就是一个小的B+树,因为hbase一般是部署在hdfs上,hdfs不支持对文件的update操作,所以hbase这么整体内存flush,而不是和磁盘中的小树merge update。内存flush到磁盘上的小树,定期也会合并成一个大树。整体上hbase就是用了lsm tree的思路。
因为小树先写到内存中,为了防止内存数据丢失,写内存的同时需要暂时持久化到磁盘,对应了HBase的HLog(WAL)和MemStore
MemStore上的树达到一定大小之后,需要flush到HRegion磁盘中(一般是Hadoop DataNode),这样MemStore就变成了DataNode上的磁盘文件StoreFile,定期HRegionServer对DataNode的数据做merge操作,彻底删除无效空间,多棵小树在这个时机合并成大树,来增强读性能。
TiKV使用RocksDB作为存储引擎,并且实现了Titan作为RocksDB 的高性能单机 key-value 存储引擎插件。以支持大value(Blob),其核心特性在于支持将 value 从 LSM-tree 中分离出来单独存储,以降低写放大。
其主要设计灵感来源于 USENIX FAST 2016 上发表的一篇论文 WiscKey。WiscKey 提出了一种高度基于 SSD 优化的设计,利用 SSD 高效的随机读写性能,通过将 value 分离出 LSM-tree 的方法来达到降低写放大的目的。
Titan 适合在以下场景中使用:
这篇论文详细地分析了各种操作在tired和leveled不同策略下的i/o复杂度,使用了level num、相邻level size比、entry数量、buffer size等一系列相关变量推导出不同操作具体的I/O代价公式和空间放大,对于point read还考虑了bloom filter带来的影响,range query分了short和long两种来分析。并且提出了优化策略lazying leveling,是tiering和leveling的结合体,最大一层使用leveling其它层使用tiering,lazying适合包含update、point lookup和long range lookup的混合负载,tiering适合大多为update的负载,leveling适合大多为lookup的负载。通过控制merge频率在tiering、leveling和lazying leveling几种不同策略下转换以适应不同的workload,称作fluid LSM((类似rocksdb的leveled-n)),并提出Dostoevsky,在执行期间动态计算最优配置以到达最大吞吐。
从作者的分析中可以看出compaction这个机制的复杂程度,要使用一套通用机制在不同场景下消耗最少的资源带来最好的性能基本是不可能的,实际工业界实现中需要考虑更多的因素(cache、delete、任务拆分粒度、复用技术等),因此大部分系统使用多种策略多个参数用以适配不同场景。