RocksDB学习笔记

一、RocksDB简介

RocksDB是Facebook基于LevelDB开发的Key-value的LSM-Tree引擎,用户写入的Key-value会先写入磁盘的Log(称WAL, Write Ahead Log)中,然后再写入内存中的跳表(MemTable)。

LSM-tree引擎将用户的随机写转换成对log文件的顺序写,因此比B树存储引擎有更好的写性能。

当内存中的数据达到阈值后,会刷到磁盘上生成SST文件,SST文件又分多层(默认最多6层),每层的数据达到一定阈值后,会挑选一部分在上升T合并到下一层,每一层的数据是上一层的10倍(因此90%的数据存储在最后一层)。

RocksDB允许用户创建多个ColumnFamily,这些ColumnFamily各自拥有独立的跳表及SST文件,但共享同一个WAL文件。这样的好处是可以根据应用的特点为不同的ColumnFamily选择不同的配置,但又没有增加对WAL的写次数。

二、TiKV与RocksDB

在这里插入图片描述
RocksDB作为TiKV的存储引擎,用于存储Raft日志和用户数据。每个TiKV实例中有两个RocksDB实例,其中一个用于存储Raft日志(通常称为raftdb),另一个存储用户数据及MVCC信息(通常称为kvdb)。

kvdb有四个ColumnFamily:raft, lock, default, write:

  • raft列:用于存储各个Region的元信息。占用极少空间,用户可不比关注。
  • lock列:用户存储悲观事务的悲观锁及分布式事务的一阶段PreWrite锁。当用户事务提交后,lock cf中对应的数据会很快删掉,因此大部分lock cf中的数据也很少(少于1GB),如果lock cf中的数据大量增加,说明有大量事务等待提交,系统出现了bug或者故障。
  • write列:用于存储用户真实的写入数据及MVCC信息(该数据所属事务的开始时间及提交时间)。当用户写入了一行数据时,如果该行数据长度小于255字节,那么会被存储write列中,否则该行数据会被存入到default列中。由于TiDB的非unique索引存储的value为空,unique索引存储的value为主键索引,因此二级索引只会占用writecf的空间。
  • default列:用于存储超过255字节长度的数据。

三、RocksDB的内存占用

为了提高读性能,减少磁盘IO次数,RocksDB将存储在磁盘上的文件按大小切分成了多个block(默认是64k),读取block时,先去内存中的BlockCache中查看该数据是否存在,存在的话则直接从内存读,不必读磁盘了。

BlockCache按照LRU算法,淘汰低频访问的数据,TiKV默认将系统总内存大小的45%用于BlockCache,用户也可以自行修改storage.block-cache.capacity配置设置为合适的值,但是不建议超过系统总内存的60%。

写入RocksDB中的数据会先写入MemTable, 当一个MemTable的大小超过128MB时,会切到一个新的MemTable来提供写入。TiKV中一共有2个RocksDB实例,合计4个ColumnFamily,每个ColumnFamily的单个MemTable大小限制是128MB,最多允许5个MemTable存在,否则会阻塞前台写入,因此这部分占用内存最多为 4* 5 * 128MB = 2.5GB。这部分占用内存较少,不建议用户自行更改。

四、RocksDB空间占用

4.1、多版本

RocksDB最欧文一个LSM-tree的key-value存储引擎,Memtable中的数据会首先被刷到SSTable文件的L0层。L0层的SSTable之间的范围可能存在重叠(因为文件是按生成的顺序排列的),因此同一个key在L0中可能存在多个版本。当文件从L0合并到L1时,会按照一定大小(默认是8MB)切割为多个文件,同一层的文件的范围互不重叠,所以L1及其以后的层每一层中的key都只有一个版本。

4.2、空间放大

RocksDB的每一层文件总大小都是上一层的x倍,在TiKV中这个配置默认是10,因此90%的数据存储在最后一层,这也意味着RocksDB的空间放到不超过1.11 (L0层数据较少,可以忽略不计)

4.3、TiKV的空间方法

TiKV在RocksDB之上还有一层自己的MVCC,当用户写入一个key的时候,实际上写入到RocksDB的是key + commit_ts, 也就是说,用户的更小和删除都会写入新的key到RocksDB。TiKV每个一段时间会删除旧版本的数据(通过RocksDB的Delete接口),因此可以认为用户存储在TiKV上的数据的实际空间放大为,1.11加最近10分钟内写入的数据。

4.4、RocksDB后台线程与Compact

RocksDB中,将内存中的Memtable转化为磁盘上的SST文件,以及合并各个层级的SST文件等操作都是在后台线程池中执行的。后台线程池的默认大小是8,当机器CPU数量小于8时,则后台线程池默认大小为CPU数减一。

五、RocksJava

5.1、RocksJava的组成

RocksJava由3部分组成:

  • org.rocksdb包里面的Java类,构成了RocksJava API, Java用户只会直接接触到这一层。
  • C++的JNI代码,提供Java API和RocksDB之间的链接
  • C++层的RocksDB本身,并且编译成了一个native库,被JNI层使用

5.2、怎样使用RocksJava

5.2.1、Maven

可以从这下载jar或pom:https://search.maven.org/search?q=g:org.rocksdb

添加mave依赖:

<dependency>
  <groupId>org.rocksdb</groupId>
  <artifactId>rocksdbjni</artifactId>
  <version>5.5.1</version>
</dependency>

5.3、内存管理

RocksJava中许多Java对象后面是C++对象,java对象对其拥有控制权。但C++没有自动垃圾回收,所以在使用完后,必须显式地释放C++对象使用的内存。

任何RocksJava中管理C++对象的Java对象都继承自org.rocksdb.AbstractNativeReference,当用完这个对象后,这个父类被用来帮助管理和清理他手上所有的C++对象。有两个函数:

1、AbstractNativeReference#close()
当用户使用完RocksJava对象后,需要显式的调用。

2、AbstractNativeReference@finalize()
当一个对象的所有存储引用都失效,并且对象就要进行垃圾回收的时候,这个方法被Java的Finalizer线程调用。他最后会委托给AbstractNativeReference#close()。不过,用户不应该依赖他,踏实最后的防线。

他保证了Java对象的C++对象最终会被回收。但他不能帮助RocksJava管理所有内存。因为native C++对象的内存在C++的堆上分配,然后返回给Java对象,这些对Java的GC机制是不可见的,所以JVM无法正确计算GC的内存压力。所以在使用完一个对象后,用户总是应该显式地调用AbstractNativeReference#close()

猜你喜欢

转载自blog.csdn.net/shijinghan1126/article/details/109716395