RocksDB(一):What is RocksDB?

What’s RocksDB?

Preface

由于本人毕设所做项目为“面向RocksDB的纠删码的设计与实现”,因此首先对于RocksDB要有一个充分的了解,它是用来做什么的,原理与工作流又是怎样的。以下为学习记录。

Concept

RocksDB是FaceBook起初作为实验性质开发的NOSQL存储系统,旨在充分实现快存上存储数据的服务能力。其设计是基于Google开源的LevelDB,由Facebook的Dhruba Borthakur于2012年4月创建的LevelDB的分支,优化了LevelDB中存在的一些问题,其性能要比LevelDB强,设计与LevelDB极其类似。最初的目标是提高服务工作负载的性能,最大限度的发挥闪存和RAM的高度率读写性能。

RocksDB是一个C++库,可以用于存储KV(key和value是任意字节流),包括任意大小的字节。同时RocksDB还支持原子读写,提供java调用的api,因此可以通过java api对RocksDB数据库进行操作。

RocksDB 具有高度灵活的配置设置,深度支持各种配置,可以调整为在各种生产环境(包括纯内存,闪存,硬盘或 HDFS)上运行调优。RocksDB针对多核CPU、高效快速存储(SSD)、I/O bound workload做了优化,支持不同的数据压缩算法、和生产环境debug的完善工具。它支持各种压缩算法,并且有生产和调试环境的各种便利工具。RocksDB 借用了来自开源 leveldb 项目的核心代码,以及来自 Apache HBase 的重要思想。初始代码是从开源 leveldb 1.5 fork 的。它还融入了 facebook 团队在开发 RocksDB 之前的若干代码及想法。

Design

RocksDB的设计思想是数据冷热分离,怎样理解这个“冷热数据”分离呢?

新写入的“热数据”会保存在内存中,热数据就是指最新操作的数据,而如果一段时间没有更新,冷数据会“下沉”到磁盘底层的“表层文件”,如果继续没有更新过这个问题,冷数据继续“下沉”到更底层的文件中。如果磁盘底层的冷数据被修改了,它又会再次进入内存,一段时间后又会被持久化刷回到磁盘文件的浅层,然后再慢慢往下移动到底层。

B tree & B+ Tree

RocksDB相对于传统的关系数据库的一大改进是采用LSM树存储引擎。LSM树是一个非常具有创意的数据结构,它和传统的B+树不太一样,首先我们先说说B+树。

B+树是应数据库所需求而出现的一种B树的变形树。所以我们先来看一下什么是B树。

B Tree

我们知道,利用二叉查找树可以快速地找到数据。但是,如果在一些不利的情况下,二叉查找树可能会变为高度十分高的情况,在这种情况下,查找的次数增多,相应的代价也变大。因此,我们提出了AVL树,也就是平衡二叉树。这样可以尽量最小化二叉树的高度。

当我们把AVL树(平衡二叉树)拓展到多叉树时,就诞生了多路平衡查找树,也就是我们平时说的B树。

从图中,我们可以总结出一棵m阶的B树的特点:

  • 每个节点最多只有m个节点

  • 所有的叶节点均在最后一层,并且不带信息(查找到叶节点相当于查找失败,注意上面的图中没有画出叶节点)

  • 非叶子结点的根节点至少有两个子节点

  • 除根节点外,每个非叶子节点具有至少有 m/2(向下取整)个子节点。

  • 每个非叶子结点都包含索引和数据

B+ Tree

B+树是从B树的基础上提出的。下面是一棵4阶B+树:

从图中,我们可以总结出m阶B+树的特点:

  • 结点的子树个数与关键字(即索引)个数相等
  • 所有的叶节点包含全部关键字及指向相应记录的指针,叶节点将关键字按大小顺序排列,并且相邻叶节点按照大小顺序相互链接起来。
  • 所有的分支节点中仅包含他的各个子节点(即下一级的索引块)中关键字的最大值以及指向其子节点的指针

Difference

  • B+树内部有两种结点,一种是索引结点,一种是叶子结点。
  • B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中。而B树则是所有结点都会保存数据。
  • B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是 从小到大的。多了条链表方便范围查找数据。
  • B树的所有索引值是不会重复的,而B+树 非叶子结点的索引值最终一定会全部出现在叶子结点中。

Why we need B+Tree?

要说明这个问题,我们需要从B树的优势与劣势出发。

B树优势:

  • 每一个非叶子结点都包含key(索引值)和value(对应数据),因此距离根节点较近的元素可以更加迅速地找到。

B树劣势:

  • 不利于范围查找(区间查找),如果要找 0~100的索引值,那么B树需要多次从根结点开始逐个查找。

    而B+树由于叶子结点都有链表,且链表是以从小到大的顺序排好序的,因此可以直接通过遍历链表实现范围查找。

因此提出B+的好处有:

  • B+树的磁盘读写代价更低:B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了;

  • B+树的查询效率更稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;

  • B+树便于范围查询(最重要的原因,范围查找是数据库的常态):B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

注:B树的范围查找用的是中序遍历,而B+树用的是在链表上遍历

LSM Tree

concept

LSM树,即日志结构合并树(Log-Structured Merge-Tree)。事实上,LSM树并不像B+树、红黑树一样是一颗严格的树状数据结构,它其实是一种存储结构,或者一种数据结构的设计实现。目前HBase、LevelDB、RocksDB这些NoSQL存储都是采用的LSM树。大多NOSQL数据库核心思想都是基于LSM来做的,只是具体的实现不同。

LSM树的核心特点是利用顺序写来提高写性能,但因为分层(此处分层是指的分为内存和文件两部分)的设计会稍微降低读性能,但是通过牺牲小部分读性能换来高性能写,使得LSM树成为非常流行的存储结构。

LSM树数据结构定义

查阅了一些资料,LSM树并没有一种固定死的实现方式,更多的是一种将:“磁盘顺序写” + “多个树(状数据结构)” + “冷热(新老)数据分级” + “定期归并” + “非原地更新”这几种特性统一在一起的思想。
为了方便后续的讲解分析,我们尝试先对LSM树做一个定义。

LSM树的定义:
  • LSM树是一个横跨内存和磁盘的,包含多颗"子树"的一个森林。

  • LSM树分为Level 0,Level 1,Level 2 … Level n 多颗子树,其中只有Level 0在内存中,其余Level 1-n在磁盘中。

  • 内存中的Level 0子树一般采用排序树(红黑树/AVL树)、跳表或者TreeMap等这类有序的数据结构,方便后续顺序写磁盘。

  • 磁盘中的Level 1-n子树,本质是数据排好序后顺序写到磁盘上的文件,只是叫做树而已。

  • 每一层的子树都有一个阈值大小,达到阈值后会进行合并,合并结果写入下一层。

  • 只有内存中数据允许原地更新,磁盘上数据的变更只允许追加写,不做原地更新。

Background

传统关系型数据库使用B Tree或一些变体作为存储结构,能高效进行查找。但保存在磁盘中时它也有一个明显的缺陷,那就是逻辑上相离很近但物理却可能相隔很远,这就可能造成大量的磁盘随机读写。随机读写比顺序读写慢很多,为了提升IO性能,我们需要一种能将随机操作变为顺序操作的机制,于是便有了LSM树。LSM树能让我们进行顺序写磁盘,从而大幅提升写操作,作为代价的是牺牲了一些读性能。

Disk I/O

磁盘读写时涉及到磁盘上数据查找,地址一般由柱面号、盘面号和块号三者构成。也就是说移动臂先根据柱面号移动到指定柱面,然后根据盘面号确定盘面的磁道,最后根据块号将指定的磁道段移动到磁头下,便可开始读写。

整个过程主要有三部分时间消耗,查找时间(seek time) +等待时间(latency time)+传输时间(transmission time) 。分别表示定位柱面的耗时、将块号指定磁道段移到磁头的耗时、将数据传到内存的耗时。整个磁盘IO最耗时的地方在查找时间,所以减少查找时间能大幅提升性能。

Core Idea

如上图所示,LSM树有以下三个重要组成部分:

1)MemTable

MemTable是在内存中的数据结构,用于保存最近更新的数据,会按照Key有序地组织这些数据,LSM树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如Hbase使跳跃表来保证内存中key的有序。

因为数据暂时保存在内存中,内存并不是可靠存储,如果断电会丢失数据,因此通常会通过WAL(Write-ahead logging,预写式日志)的方式来保证数据的可靠性。

内存中的数据和SST文件组成了RocksDB数据的全集。

rocksdb中的数据结构有三种,分别是skiplist、hash-skiplist、hash-linklist。

Skiplist Memtable

基于Skiplist的memtable在多数情况下都有较好读,写,随机访问以及序列化扫描性能。除此之外,他还提供其他memtable没有的有用的功能,比如并发插入以及带Hint插入。跟leveldb不同,跳表的好处在于插入的时候可以保证数据的有序,支持二分查找、范围查询。当然,删除的时候不是立即删除,因为会影响到数据的写放大,一般是在compact阶段进行真正的删除。

Hash xxxx Memtable

hash类型的Memtable有两种不同的实现,一种是HashSkipList,另一种是HashLinkList。正如他们的名字所描述的,HashSkipList用一张哈希表组织数据,每个哈希桶内都是一个的skiplist,而HashLinkList则是用一张哈希表组织数据,每个哈希桶内则是使用一个排序好的linkedlist。两种类型都是为了减少查询的时候的比较次数,而两者之间的差别其实就是skiplist与linkedlist的差别。一种好的使用例子是使用PlainTable SST格式结合他们,然后把数据存储在RAM FS里。hash-skiplist的索引既有hash索引,又有skiplit的二分索引,针对于有明确key或教完整key前缀的查询,如果要进行全表扫描,会非常耗费内存及低性能,因为要产生临时的有序表; hash-linklist也是一样的道理。

当做数据查询或者插入一个key的时候,目标key的前缀通过Options.prefix_extractor被提取出来,用于找到具体的哈希桶。在哈希桶里面,所有的比较都是完整(内部)key比较,跟SkipList的memtable一样。

使用基于哈希的memtable最大的限制就是做多个前缀扫描的时候需要拷贝和排序,这非常慢并且浪费内存。

2)Immutable MemTable

当 MemTable达到一定大小后,会转化成Immutable MemTable。Immutable MemTable是将转MemTable变为SSTable的一种中间状态。写操作由新的MemTable处理,在转存过程中不阻塞数据更新操作。

3)SSTable(Sorted String Table)

有序键值对集合,是LSM树组在磁盘中的数据结构。为了加快SSTable的读取,可以通过建立key的索引以及布隆过滤器来加快key的查找。

这里需要关注一个重点,LSM树(Log-Structured-Merge-Tree)正如它的名字一样,LSM树会将所有的数据插入、修改、删除等操作记录(注意是操作记录)保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。这与B+树不同,B+树数据的更新会直接在原数据所在处修改对应的值,但是LSM数的数据更新是日志式的,当一条数据更新是直接append一条更新记录完成的。这样设计的目的就是为了顺序写,不断地将Immutable MemTable flush到持久化存储即可,而不用去修改之前的SSTable中的key,保证了顺序写。

因此当MemTable达到一定大小flush到持久化存储变成SSTable后,在不同的SSTable中,可能存在相同Key的记录,当然最新的那条记录才是准确的。这样设计的虽然大大提高了写性能,但同时也会带来一些问题:

1)冗余存储,对于某个key,实际上除了最新的那条记录外,其他的记录都是冗余无用的,但是仍然占用了存储空间。因此需要进行Compact操作(合并多个SSTable)来清除冗余的记录。
2)读取时需要从最新的倒着查询,直到找到某个key的记录。最坏情况需要查询完所有的SSTable,这里可以通过前面提到的索引/布隆过滤器来优化查找速度。

Compact Strategy

从上面可以看出,Compact操作是十分关键的操作,否则SSTable数量会不断膨胀。在Compact策略上,主要介绍两种基本策略:size-tiered和leveled。

不过在介绍这两种策略之前,先介绍三个比较重要的概念,事实上不同的策略就是围绕这三个概念之间做出权衡和取舍。

1)读放大:读取数据时实际读取的数据量大于真正的数据量。例如在LSM树中需要先在MemTable查看当前key是否存在,不存在继续从SSTable中寻找。
2)写放大:写入数据时实际写入的数据量大于真正的数据量。例如在LSM树中写入时可能触发Compact操作,导致实际写入的数据量远大于该key的数据量。
3)空间放大:数据实际占用的磁盘空间比数据的真正大小更多。上面提到的冗余存储,对于一个key来说,只有最新的那条记录是有效的,而之前的记录都是可以被清理回收的。

1)size-tiered策略

size-tiered策略保证每层SSTable的大小相近,同时限制每一层SSTable的数量。如上图,每层限制SSTable为N,当每层SSTable达到N后,则触发Compact操作合并这些SSTable,并将合并后的结果写入到下一层成为一个更大的sstable。

由此可以看出,当层数达到一定数量时,最底层的单个SSTable的大小会变得非常大。并且size-tiered策略会导致空间放大比较严重。即使对于同一层的SSTable,每个key的记录是可能存在多份的,只有当该层的SSTable执行compact操作才会消除这些key的冗余记录。

2)leveled策略

leveled策略也是采用分层的思想,每一层限制总文件的大小。

但是跟size-tiered策略不同的是,leveled会将每一层切分成多个大小相近的SSTable。这些SSTable是这一层是全局有序的,意味着一个key在每一层至多只有1条记录,不存在冗余记录。之所以可以保证全局有序,是因为合并策略和size-tiered不同,接下来会详细提到。

每一层的SSTable是全局有序的

假设存在以下这样的场景:

1)L1的总大小超过L1本身大小限制

2)合并后存储

此时会从L1中选择至少一个文件,然后把它跟L2有交集的部分(非常关键)进行合并。生成的文件会放在L2,如下图所示,此时L1第二SSTable的key的范围覆盖了L2中前三个SSTable,那么就需要将L1中第二个SSTable与L2中前三个SSTable执行Compact操作。

3)重复之前操作

如果L2合并后的结果仍旧超出L5的阈值大小,需要重复之前的操作 —— 选至少一个文件然后把它合并到下一层:

需要注意的是,多个不相干的合并是可以并发进行的:

leveled策略相较于size-tiered策略来说,每层内key是不会重复的,即使是最坏的情况,除开最底层外,其余层都是重复key,按照相邻层大小比例为10来算,冗余占比也很小。因此空间放大问题得到缓解。但是写放大问题会更加突出。举一个最坏场景,如果LevelN层某个SSTable的key的范围跨度非常大,覆盖了LevelN+1层所有key的范围,那么进行Compact时将涉及LevelN+1层的全部数据。

注意:

  • 由于Level1-Leveln都是从Level0里面依次合并过来的,因此每一层层内都不会有重复文件。但是层与层之间会有重复文件。而内存中的数据是就地更新,所以Level0单个文件时没有重复。同时在所有层中,单个文件是有序不重复的。

  • Level1-Leveln文件间也是有序的

原文链接:RocksDB(一):What is RcoksDB?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值