WiscKey: Separating Keys from Values in SSD-conscious Storage

WiscKey: Separating Keys from Values in SSD-conscious Storage

说明

目前越来越多的KV存储的引擎采用了LSM-tree的结构,如LevelDB(HBase存储引擎)、RocksDB(Facebook基于LevelDB优化的引擎)、Riak等,LSM-tree的主要架构如下图所示:

LSM-tree以及基于其实现的存储引擎有以下特点:

  1. 小的随机写IO被顺序化为大的连续IO;
  2. 磁盘上的物理文件(sstable)被分层为多个level,且随着系统的运行,层级低的数据会向层级高的数据合并。

因为LSM-tree将大量的随机IO变成了顺序IO,因此对HDD此类存储介质有着比较明显的性能提升。但同时,此架构也会带来如下一些问题,最主要的便是IO放大以及IO放大带来的副作用。

LSM-tree结构之所以带来IO放大,是因为:

写入时,除了将数据顺序写入日志外,还考虑将memtable写入level 0 的sstable以及由此带来的sstable合并问题,根据论文作者的计算,在极端情况下,可能带来的放大系数为10*Level;

读取时,首先需要查找文件所在的sstable,由于sstable分层,因此该过程要由低向高level逐渐查找,在极端情况下,需要遍历所有sstable的元数据块,也带来了相当程度的读放大。

而这种IO放大会带来怎样的副作用呢?

显而易见的是IO效率的降低;

对于SSD存储器件,这种放大还会导致存储硬件的寿命缩减。

因为SSD优异的随机IO性能,随机IO和顺序IO的性能比没有HDD的那样突出,因此,这种LSM-tree结构带来的顺序IO好处无法弥补IO放大带来的劣势。那如何在SSD存储环境中设计高效的存储引擎?这是本篇论文关注的重点。

WiscKey

WsicKey便是为了解决该问题而设计的存储引擎,其主要设计思路是:

对象的key与value分离

其中,元数据包括对象的key以及对象数据的位置,就是一个<key, addr>的二元组;数据则是对象的实际数据,被存储在value log区域,实际上就是write-ahead log了。

对象的数据是被按照顺序追加的方式存储在value log中,依然满足顺序写的特点;而对象的元数据则依然使用LSM-tree来存储,与LevelDB的存储别无二致,只是只存储元数据罢了,数据量小了好几个数量级。

接下来我们分析如此设计有什么好处?

数据只写日志Value Log,避免了数据日志和sstable的双写,提高IO效率;

只将元数据写入LSM-tree,虽然也会存在IO放大,但是由于数据量很小,总体的放大效果明显降低。

对于好处2,论文中有一个大概的计算,假设对象的元数据为16B,对象数据大小为1KB,按照LSM的放大系数为10来计算,采用WiscKey设计,写入的总体放大率约为:

(16 * 10 + 1024)/(1024 + 16) = 1.14

对于读操作,由于LSM-tree的数据量已经显著减少,总的sstable文件数量也相应减少,元数据的查找过程也会加快,只是不太好评估这个效率。而且,LSM-tree数据量的减少还能带来另外一个好处:可以尽量多的缓存元数据,进一步提高元数据检索的效率。

几个问题

因为将对象数据和元数据分开存储,可能会导致以下几个问题:

  • 垃圾回收;
  • 数据一致性;
  • 读性能优化。

接下来我们一一阐述,问题是什么以及WiscKey是如何解决这些问题。

垃圾回收

LevelDB等之类LSM-tree的存储系统对于对象的删除只是追加删除标记,延期至sstable compaction的时候回收那些无效数据。

对于WiscKey的设计,由于元数据和数据分离,回收涉及两个方面:元数据的回收和数据回收。

元数据回收与LevelDB类似,无需赘言。对象的数据位于Value Log区,如何回收Value Log区的无效对象呢?

如上图所示,WiscKey设计了一种比较巧妙的思路:log区有两个指针 head和tail。head指向当前新数据待写入的位置,而tail则指向当前待回收的位置。

当触发垃圾回收时,从tail位置读取一条记录,并从元数据判断其是否已经被删除:

  • 如果是,则tail向前移动至下一条记录;
  • 如果不是,则将该记录写入至head位置,接下来再将tail移动至下一条记录。

当然,为了避免垃圾回收过程不会因为异常导致数据丢失,会将head和tail信息持久化,当然,最好的地方便是LSM-tree内了。

当然,由于垃圾回收涉及IO(先读后写),因此不建议太频繁,只有在删除操作频率比较高时才触发该操作。

数据一致性

因为数据和元数据分离存储,因此,势必存在数据不一致性,如数据写入成功但元数据写入失败,或者相反。

如果数据存在,但是元数据不存在,那在后续的垃圾回收时数据会被清理。

如果元数据存在,那么接下来还需要验证数据有效性。验证的方法也比较多,如checksum、magic等。如果验证失败,那么会将元数据清除。

数据读优化

由于数据和元数据分离,数据的读取先是从LSM-tree读取元数据,然后再根据位置从Value Log中读取数据。这样做的劣势是会产生随机IO。

虽然SSD对随机IO性能有较大的提升,但是较小的随机IO依然会对性能产生较大影响。但是SSD也有个特性就是可并行的随机IO也会充分发挥其性能,如下图:

可以看到,在32个线程并发时,16KB的随机IO就基本快达到了SSD的带宽上线。

考虑到SSD的这种特性,WiscKey对range query进行了并发读的优化,具体的做法是:

  1. 对于range query,同时取出多个key的元数据信息<key, address>
  2. 将这些<key,address>信息放入队列,并唤醒预读线程(默认32个)
  3. 预读线程开始读数据

参考

编辑于 2017-11-11

文章被以下专栏收录