“ 本文作者为中国移动云能力中心大数据团队软件开发工程师周翔宇,文章首先分析B+树磁盘随机写问题,引出LSM树并分析其结构、读写流程、Compaction策略以及在HBase中的具体实现。其次,分析LSM树读写放大的根本原因,以及学术界如何通过KV分离技术来优化Compact导致的写放大。最后,分析KV分离技术在HBase MOB和TiDB Titan项目中的具体落地。”
01
—
背景
LSM树(Log-Structured Merge-Tree),因其独特的数据组织方式(Log-Structured)和需要在后台不断合并(Merge)的维护方式而得名。又因其磁盘顺序写的模式,从而取代B+Tree,被广泛应用于HBase、Cassandra、LevelDB、RocksDB和TiDB等写密集型KV数据库和存储引擎上。
02
—
LSM树和HBase中的应用
LSM树结构上是一种横跨内存和磁盘,且包含多颗子树的索引结构,并在内存达到阈值时启动树合并。LSM树中C0(MemTable)是一颗常驻内存中还未刷盘、原地更新的查找树,MemTable内部KV对有序存储,便于快速持久化到SSTable上。C1 - CX(SSTable)是磁盘中只追加(append-only)的 B-Tree,每个SSTable包含多个存储数据的文件segment,每个 segment 内部都是有序的,但不同 segment 之间没有顺序关系。随着数据的不断写入,SSTable和segment数量不断增加,当Ci超过固定大小后,采用Merge Sort的方式写入到Ci+1层,该过程也称为Merge或者Compact。
LSM树根据SSTable中每层Run数量的不同,将Compaction策略划分为两大类:Tiering和Leveling。论文中对于Run的概念比较抽象,可以理解每个Run里KV对具有key有序(sorted)和key不重叠(non-overlapping)两个特点。
Tiering:每层可能有N个重叠的sorted Runs,当上一层的sorted Runs 要merge下来时,不会和当前层重叠的sorted Runs马上合并,而是等到当前层空间不足时,才会合并,然后再merge到下一层。Tiering这种策略适用于写密集型(Write-Intensive)应用,具有较低的写放大,但是读放大和空间放大较高。由于所有上层数据直接追加写入下层,写放大比较低。但是每层都有多个Run,且多个Run中的数据可以重叠,点查询需要由上到下遍历每一个Run,读放大较严重。业界采用Tiering作为Compaction策略的有BigTable、HBase、Cassandra和InfluxDB等KV数据库。
Leveling:每层有且只有一个sorted Run,当上一层的sorted Run 要merge下来时,会马上和当前层的sorted Run合并。Leveling这种策略适用于读密集型(Read-Intensive应用),具有较低的读放大和空间放大,但写放大较高。由于每层只有一个Run,这个Run中的KV对不存在Key重叠,因此降低了读放大和空间放大,但是点查询仍然有可能会遍历所有的Run,而范围查询必然会遍历所有的Run。Leveling上层与下层merge的过程有可能涉及到上下层所有的数据,造成上层Run全量重写到下层中,导致写放大较高,但是由于每个Run可以分为多个partition(SSTable),因此可以节约部分临时空间。业界采用Leveling作为Compaction策略的有LevelDB、RocksDB等KV数据库。
LSM树在HBase的实现中,MemStore和StoreFile分别对应C0和C1 - CX。数据写入时,首先会写到内存中的MemStore,当达到一定阀值之后,flush(顺序写)到磁盘,形成新的StoreFile,最后多个StoreFile又会进行Compact。HBase MemStore内部维护了一个ConcurrentSkipListMap的数据结构,跳表是一种查找、插入和删除高效的内存数据结构,其底层采用有序链表存储,方便数据插入。在底层节点之上构建多层不同的稀疏索引,加速节点的查询定位。当内存数据源源不断flush到磁盘后,为了减少读数据seek操作次数,HBase支持Major Compaction和Minor Compaction策略,所有的HFile或者选择多个HFile进行异步多路归并成一个HFile文件。
为了提升LSM树SSTable的随机读性能,HBase在HFile中引入了Bloom Filter,对应HFile中的Bloom index block。通过Bloom Filter,HBase以牺牲少量的空间代价,换来读取数据时非常快速地确定是否存在某条数据,从而进一步提升磁盘随机读效率。
03
—
LSM树读写放大问题分析及放大优化
LSM树的读放大主要来源于读操作需要从C0~Ck(自顶向下)一层一层查找,直到找到目标数据。这个过程可能需要不止一次 I/O,特别是对于范围查询,读放大非常明显。LSM树的空间放大主要是由于所有数据写入采用非原地更新的追加方式,过期或者删除的数据不会马上从磁盘上清理掉。因此,采用LSM树思想的KV数据库的实现中,通常需要启用后台线程周期检查或者手动flush等方式触发Compaction 来减少读放大(减少SSTable文件数量)和空间放大(清理过期数据),但也因此带来了严重的写放大问题。下图是PebblesDB论文中测试了几款主流KV数据库的写放大系数对比:
04
—
HBase和TiDB中KV分离技术实现
学术界的顶会顶刊发表了很多优秀的数据结构和KV分离方案来优化写放大问题,实验场景下数据对比也较为出色。然而,由于业界负载和实际业务数据更为复杂,业界KV分离技术成熟产品并不多。下面简单分析一下应用较为广泛的开源产品HBase MOB(Medium Object)和TiDB Titan如何实现了KV分离技术的应用。
HBase 2.0版本引入了列族级别的MOB功能,改善了对中等大小值的低延迟读写能力,支持MOB阈值可配,对客户端完全透明,为HBase对象存储提供了一体化解决方案。由于HBase最理想场景是存储Metadata,对于MOB(1~10MB)、LOB(>10MB)写入时,触发频繁的flush、Compaction和Region分裂,导致HBase集群读写延迟和长期处于RIT状态。
HBase MOB方案中Metadata和MOB数据同时写入Memstore,当Memstore中数据flush到磁盘时,将Metadata存放到常规的StoreFile形式,MOB数据则以HFile格式直接存储到HDFS上,使得MOB移出普通Region的管理来避免Region Split和Compaction操作。然而,数据仍然先写入Memstore,决定了写入数据量不能太大,否则一次写入可能会撑爆Memstore,造成OOM或者Full GC。此外,为了避免HDFS上存在许多MOB小文件,HBase MOB支持更为简单的Compaction方式,默认按天合并压缩。HBase MOB Compaction只对小文件进行压缩,而对大文件不进行严格的压缩,避免对大文件的无限压缩使得写放大更具可预测性。
05
—
总结
LSM 树的 KV 分离技术有效缓解了Compaction操作对value的不断重写,减小了写放大。但于此同时,带来其他的开销,比如空间放大、读路径变长等。LSM树读放大、写放大和空间放大,三者就像 CAP 定理一样,需要根据用户实际业务负载,做好权衡和取舍。
附:参考文献
The Log-Structured Merge-Tree (LSM-Tree)
LSM based Storage Techniques: A Survey
PebblesDB: Building Key-Value Stores using Fragmented Log-Structured Merge Trees
WiscKey:Separating Keys from Values in SSD-conscious Storage
HashKV: Enabling Efficient Updates in KV Storage via Hashing
HBase MOB官方设计文档
tikv/titan: A RocksDB plugin for key-value separation, inspired by WiscKey
Titan的设计与实现
- END -
关于本周文章如果大家有疑问,或者有其他感兴趣的大数据内容,欢迎大家在评论区留言,后续会持续分享大数据领域的干货知识~
评论




