rocksdb-share

最近项目中用到这个nb的玩意,所以就花时间研究了下,同时整理下助自己记忆。这个猛虎上山的logo就是rocksdb官方的logo。 同时借用官网的一句话来综述下: 一个持久性的key-value存储,为了更快速的存储环境而生。

rocks特性

rocksdb是一个可嵌入的C++库, 可用于存储任意大小字节流的key, value 结构。支持的方法有Put, Get, Delete, 传统的数据库的常用方法CRUD.其中update这个方法对rocksdb来说也是一种Put方法。同时因为rocksdb是按key的顺序存储数据,所以还支持Iterator迭代的方法,即就是定位到数据库中的一个key后,可以从这个key开始正向的扫描,也可以逆向的扫描。同时支持PrefixExtractor前缀迭代,假设我们存储的key有20位,前10位为时间信息,那么我们可以根据前缀迭代把某个时间段的数据拉出来,这个需要我们在创建数据库时配置PrefixExtractor的位数为10,默认是0。同时rocksdb支持快照Snapshot, 每次以只读方式Get数据时都会创建一个快照,在这个时间点之前的数据对Get可见,之后不可见。同时rocksdb还提供一些用户可定义的filter, 比如提供bloomfilter可以优化读性能;提供DB级别的ttlfilter, 使得超过过期时间的数据在compaction过程中被删除。

Rocksdb还支持写入的原子性,数据的持久化,数据校验,数据压缩,数据备份,数据缓存等一些特性。

写逻辑

我们从一条数据对写入来了解rocksdb的存储结构吧。


首先当一条数据写入rocksdb时, 会将这条记录封装成一个batch, 也可以是多条记录一个batch,由batch来保证原子操作。就是一个batch里的数据要么全部成功要么全部失败。

第一步先以日志的形式落地磁盘,记write ahead log, 落地成功后再写入memtable。这里记录wal的原因就是防止重启时内存中的数据丢失。所以在db重新打开时会先从wal恢复内存中的mentable. 可配置WAL保存在可靠的存储里。

这里的memtable是在内存中的一个跳表结构(skiplist)。每一个节点都是存储着一个key, value. 跳表可使查找的复杂度为logn, 同时插入数据非常简单。每个batch独占memtable的写锁。这个是为了避免多线程写造成的数据错乱。

当memtable的数据大小超过阈值(write_buffer_size)后,会新生成一个memtable继续写,将前一个memtable保存为只读memtable.

当只读memtable的数量超过阈值后,会将所有的只读memtable合并并flush到磁盘生成一个SST文件。这里的SST属于level0, level0中的每个SST有序,整个level0不一定有序。

当level0的sst文件数超过阈值或者总大小超过阈值,会触发compaction操作,将level0中的数据合并到level1中。同样level1的文件数超过阈值或者总大小超过阈值,也会触发compaction操作, 这时候随机选择一个sst合并到更高层的level中。这里有两点比较重要,1:level1 及其以上的level都整体有序。每个sst存储一个范围的数据互不交叉互不重合;2: level1 以上的 compaction操作可以多线程执行,前提是每个线程所操作的数据互不交叉。

那么这样数据就由内存流入到高层的level。我们理想状态下,所有的数据都存在非level0的一层level上。这样可以保证最高效的查询速度。所以对rocksdb来说, 每次写都保证着原子性,所有数据都会落地到磁盘,保证着数据的可靠性和持久性。

读逻辑

同样,我们看一条数据是如何被读取的。

首先RocksDB中的每一条记录(KeyValue)都有一个LogSequenceNumber,从最初的0开始,每次写入加1。lsn在memtable中单调递增。之前提到的snapshot即就是基于lsn实现,每次以只读模式打开时,记录一个lsn, 大于该lsn的key不可见。

首先读操作先访问memtable。跳表的时间复杂度可达到logn, 如果不存在会访问level0, 而level0整体不是有序的, 所以会按创建时间由新到老依次访问每一个sst文件。所以时间复杂度为m*logn。如果仍不存在,则继续访问level1,由于level1及其以上的level都整体有序,所以只需要访问一个sst文件即可。 直到查找到最高层或者找到这个key。所以读操作可能会被放大好多倍。

rocksdb做了几点优化,一点是为每个SST提供一个可配置的bloomfilter. 每个level的配置不一样。这样可以快速的确认一个key在不在某个SST中,这点以牺牲磁盘空间来换取时间。另一点是提供可配置的cache, 用于保存访问过的key在内存中, 这里有一点就是它缓存的是某个key在SST文件中的整个block里的记录。

存储结构之memtable

rocksdb的memtable, 默认是skiplist跳表结构, 但它也同时支持hash-skiplist, hash-linklist结构。在创建数据库时,可配置选择存储合适结构。什么时候选择合适的结构类型,这块还值得研究。如果将所有的数据都保存到内存中,这时候用hash-linklist是不是更合适呢。

skiplist 结构:


hash-skiplist结构:


hash-linklist结构:


存储结构之SST

SST(Sorted Sequence Table)

SST作为rocksdb第二个存储方式, 它的数据主要以block块的方式存储,默认是4k大小。ps之所以默认设定4k是因为我们一般的操作系统存储的每个页刚好是4k, 这样每次加载一份数据一个block刚好就是一页。这样就不会造成,block过大需要分页存储或者block过小一页存储一个block造成内存浪费。


其中每个block以最后的crc码来效验数据的正确性。

根据block存储的不同数据可以分为多种类型。其中Datablock里存储着压缩的key, value 记录,是数据层。Metablock存储着filter的数据,其中bloomfilter, prefix_bloomfilter, 还有用户自定义的一些filter的数据存在这里。 Metablock Index记录着filter的大小偏移量等信息。Indexblock记录着每一个Datablock的最大key和最小key的偏移量等信息。 Footer记录着Metablockindex块的偏移量和大小以及Indexblock的偏移量和大小等信息。


Flush操作

* 首先当只读memtable的数量大于阈值,如果没有其它线程flush,则将该次操作加入队列。

* 遍历skiplist,通过迭代器逐一扫描key-value,将key-value写入到data-block,

* 如果data block大小已经超过阈值,或者key-value对是最后的一对,则触发一次block-flush, 同时根据压缩算法对block进行压缩,并生成对应的index block记录;

* 所有数据更新完后, 写入index block,meta block,metaindex block以及footer信息到文件尾;

* 并将变化sst文件的元信息写入manifest文件。 同时清理wal日志文件。

Compaction操作

* rocksdb会定期的检测每个level的状态, 并为每个level计算score. score通过level实际的size/base_size来计算;level0的score是通过实际sst数/ level0 的sst文件数阈值计算。

* 如果score>1 则会触发compaction, 找score最大的level,根据一定策略从中选择一个sst文件进行compaction. 

* 根据这个sst文件的minkey, maxkey找到leveln+1层中有重叠的sst文件。 多个sst文件进行归并排序,生成新的有序sst文件。

* 将变化的sst文件的信息写入manifest文件。

数据库文件结构


SST

rocksdb的数据文件, 严格说包含SST文件,还有内存中的memtable数据。

memtable写入到一定阈值,可能是这个参数max_total_wal_size(待验证),会flush memtable里的数据到SST level0文件。level0文件的key不一定是有序的,但leveln(n>0)的key必然是有序的.

可通过ldb工具来查看SST的内容

```

# ../ldb --db=./data/fansnum_tail scan

1162218773 : 34

1162222073 : 4433

1162231091 : 1025

1162231094 : 4

1162231403 : 72

1162233913 : 66

1162241104 : 35

./db_bench --db=/data1/xiaodong28/PacketServer/PacketComputation/offline_update/fansnum/ --benchmarks=fillrandom --num=10000 --compression_type=none

```

MANIFEST

*MANIFEST*: 记录rocksdb最近的状态变化日志。其中包含manifest日志 和最新的文件指针 *CURRENT*, 记录最近的MANIFEST

manifest日志: *MANIFEST-(seq number)*, 记录一系列版本更新记录。

在RocksDB中任意时间存储引擎的状态都会保存为一个Version(也就是SST的集合),而每次对Version的修改都是一个VersionEdit,而最终这些VersionEdit就是 组成manifest-log文件的内容

```

/data1/rocksdb/rocksdb/bin/ldb --path=./fansnum/MANIFEST-000014  manifest_dump

```

log

WAL文件。 rocksdb在写数据时, 先会写WAL,再写memtable。为了避免crash时, memtable的数据丢失。服务重启时会从该文件恢复memtable。

OPTIONS

rocksdb的配置文件, 配置参数说明可参考下一小节。

IDENTITY

存放当前rocksdb的唯一标识

LOCK

LOCK 进程的全局锁,DB一旦被open, 其他进程将无法修改,报类似以下错误。

```

Open rocksdb ../data/fansnum_rocksdb/ failed,  reason: IO error: while open a file for lock: ../data/fansnum_rocksdb//LOCK: Permission deniedCommand init,  costtime: 0.946000 ms

```

LOG

rocksdb的操作日志文件, 可配置定期的统计信息写入LOG. 可通过info_log_level调整日志输出级别; 通过keep_log_file_num限制文件数量 等等。

写数据测试

对于rocksdb的写操作做了个测试数据分析:

* 测试机器是20CPU, 32G的内存, STAF磁盘

* 测试数据库的总key量为3.9亿,大小为5.2G。

* 最终生成的数据文件大小为2.63G, 数据压缩比为50.6%,

* 生成的level结构为Level0没有数据,Level1 5个sst文件,总大小290.22M; Level2 43个文件,总大小2.35G. 我们从level文件结构可以看出结果还是比较好的,比如查一个key,只需读level1和level2中各一个sst文件即可。

* 总compaction次数255次,写磁盘总数据量为22.9G, 写放大为8.7,这就相当于在这次测试中平均每条记录写了8.7次磁盘。而且这个值会随着数据量的增大而增大。

* 总耗时1140s, 平均每秒写34.2w条数据。

读性能测试

针对LRUcache, 做了一个读性能的测试的数据分析

测试机器是20CPU, 32G的内存, STAF磁盘, 测试数据库的总key量为3.9亿, 数据库大小为2.63G,测试数据的key量为1kw。根据创建不同大小的LRUcache, 主要关注了两个性能指标,平均耗时和cache命中率。其中db默认的cache是8M的LRUcache。上图可以看出随着cache大小的增加, 平均耗时再递减,命中率在递增。16GB的cache可以将所有数据全部存在cache中。那时查询一个key的平均耗时需要3.3微妙。





从上图可以看出不同比例的数据的最大耗时, 比如在配置cache的db中,95%数据的都可以在14微秒之内返回, 99%的数据都可以在25微秒之内返回。配置最大cache时单线程访问可以达到30w的qps。

可优化的点

写放大

由于存在数据的compaction操作,所以rocksdb实际总的写磁盘数据量并不是等同于输入的数据量, 我们看下我写一次全量粉丝数的数据量5.2G, 经过压缩后2.63G, 直到所有数据更新完成后写磁盘的量达到22.9G, 近8.7倍的写放大。

读放大

同时rocksdb也存在读放大,基于之前的读操作内容,我们可以知道一次get操作可能包含多次读磁盘操作。而且每次读一个记录会将该记录所在的block都读入内存。

空间放大

空间放大主要表现在数据的更新和删除实际都在compaction操作中执行,这样在compaction之前一个key在数据库中会存好多份记录。这样造成的空间的浪费。

应用场景

通过以上对rocksdb的了解,rocksdb比较适合小规模数据存储,因为数据量越大,其写放大,读放大,空间放大就会越严重, 但具体到什么规模这个还定义不了,当前我们本地存储10G左右的数据,性能杠杠的。适用于对写性能要求高,同时有大量内存来缓存SST数据以达到快速读取的场景。适用于存储变长key,value的场景。

推荐阅读更多精彩内容