RocksDB
简介
RocksDB项目是起源于Facebook,是一个高性能的嵌入式键值存储库,特别适合用于需要低延迟和高吞吐量的场景。。RocksDB 借鉴了开源leveldb项目的重要代码以及Apache HBase的想法。最初的代码是从开源 leveldb 1.5 fork出来的。
以库组件形式嵌入程序中,为大规模分布式应用在ssd上运行提供优化。RocksDB不提供高层级的操作,例如备份、负载均衡、快照等,而是选择提供工具支持将实现交给上层应用。正是这种高度可定制化能力,允许RocksDB对广泛的需求和工作负载场景进行定制。
很多项目都接纳了RocksDB作为其后端存储的一种解决方案,如Mysql, Ceph, Flink, MongoDB, TiDB等。
RocksDB 架构
RocksDB使用Log-Structured Merge(LSM)trees做为基本的数据存储结构。
Block Cache
纯内存存储结构,存储SST文件被经常访问的热点数据
一个Cache对象可以被同一个进程中的多个RocksDB实例共享,用户可以控制整体的缓存容量。
块缓存存储未压缩的块。用户可以选择设置存储压缩块的二级块缓存。读取将首先从未压缩的块缓存中获取数据块,然后是压缩的块缓存。如果使用 Direct-IO,压缩块缓存可以替代 OS 页面缓存。
Block Cache有两种缓存实现,分别是 LRUCache 和 ClockCache。两种类型的缓存都使用分片以减轻锁争用。容量平均分配给每个分片,分片不共享容量。默认情况下,每个缓存最多会被分成 64 个分片,每个分片的容量不小于 512k 字节。
- LRUCache: 默认的缓存实现。使用容量为8MB的基于LRU的缓存。缓存的每个分片都维护自己的LRU列表和自己的哈希表以供查找。通过每个分片的互斥锁实现同步,查找与插入都需要对分片加锁。
极少数情况下,在块上进行读或迭代的,并且固定的块总大小超过限制,缓存的大小可能会大于容量。如果主机没有足够的内存,这可能会导致意外的 OOM 错误,从而导致数据库崩溃 - ClockCache: ClockCache 实现了 CLOCK 算法。时钟缓存的每个分片都维护一个缓存条目的循环列表。时钟句柄在循环列表上运行,寻找要驱逐的未固定条目,但如果自上次扫描以来已使用过,也给每个条目第二次机会留在缓存中。
ClockCache 还不稳定,不建议使用
Write Buffer Manager
Write Buffer Manager 用于控制多个列族或者多个数据库实例的内存表总使用量。
使用方式:用户创建一个write buffer manager对象,并将对象传递到需要控制内存的列族或数据库实例中。
有两种限制方式:
- 限制 memtables 的总内存用量
触发其中一个条件将会在实例的列族上触发flush操作:
1. 如果活跃的 memtables 使用超过阈值的90%
2. 总内存超过限制,活跃的 mamtables 使用也超过阈值的 50% 时。
- memtable 的内存占用转移到 block cache
大多数情况下,block cache中实际使用的block远小于block cache中缓存的,所以当用户启用该功能时,block cache容量将覆盖block cache和memtable两者的内存使用量。
如果用户同时开启 cache_index_and_filter_blocks,那么RocksDB的三大内存区域(index and filter cache, memtables, block cache)内存占用都在block cache中。
SST文件格式
BlockBasedTable 是 SSTable 的默认表格式。
<beginning_of_file>
[data block 1]
[data block 2]
...
[data block N]
[meta block 1: filter block] (see section: "filter" Meta Block)
[meta block 2: index block]
[meta block 3: compression dictionary block] (see section: "compression dictionary" Meta Block)
[meta block 4: range deletion block] (see section: "range deletion" Meta Block)
[meta block 5: stats block] (see section: "properties" Meta Block)
...
[meta block K: future extended block] (we may add more meta blocks in the future)
[metaindex block]
[Footer] (fixed size; starts at file_size - sizeof(Footer))
<end_of_file>
数据块 DataBlock:键值对序列按照根据排序规则顺序排列,划分为一系列数据块(data block)。这些块在文件开头一个接一个排列,每个数据块可选择性压缩。
元数据块 MetaBlock:紧接着数据块的是一堆元数据块(meta block),元数据块包括:过滤块(filter block)、索引块(index block)、压缩字典块(compression dictionary block)、范围删除块(range deletion block)、属性块(properties block)。
MetaIndexBlock: 元索引块包含一个映射表指向每个meta block,key是meta block的名称,value是指向该meta block的指针,指针通过offset、size指向数据块。
页脚 Footer:文件末尾是固定长度的页脚。包括指向metaindex block的指针,指向index block的指针,以及一个magic number。
Meta Block的具体种类
索引块 Index Block
索引块用于查找包含指定key的数据块。是一种基于二分搜索的数据结构。一个文件可能包含一个索引块,也可能包含一组分区索引块,这取决于使用配置。即存在全局索引与分区索引两种索引方式。
过滤器块 Filter Block
全局过滤器、分区过滤器,都是通过用布隆过滤器实现
全局过滤器 Full Filter: 在此过滤器中,整个 SST 文件只有一个过滤器块。
分区过滤器 Partitioned Filter: Full Filter 被分成多个子过滤器块,在这些块的顶层有一个索引块用于将key映射到相应的子过滤器块。
压缩字典块 Compression Dictionary Block
包含用于在压缩/解压缩每个块之前准备压缩库的字典。
范围删除块 Range Deletion Block
范围删除块包含文件中key与序列号中的删除范围。在读请求下发到sst的时候能够从sst中的指定区域判断key是否在deleterange 的范围内部,存在则直接返回NotFound。memtable中也有一块区域实现同样的功能。
compaction或者flush的时候会清除掉过时的tombstone数据。
属性块 Properties Block
包含属性信息。
统计块格式:
[prop1] 每个property都是一个键值对
[prop2]
...
[propN]
属性信息保证顺序且没有重复,默认情况下包含了以下信息:
data size // data block总大小
index size // index block总大小
filter size // filter block总大小
raw key size // 所有key的原始大小
raw value size // 所有value的原始大小
number of entries
number of data blocks
RocksDB 子模块
RocksDB 5大子模块,分别为:
- Basic Operation,基本操作定义
- Terminology,内部术语定义
- Tool,内部工具
- Logging/Monitoring ,日志和监控
- System Behavior,内部系统行为
Basic Operation
除了 RocksDB 核心的KV的操作接口get,put两类操作外,RocksDB 还在此模块中封装了如下几类能适用于特殊使用场景的操作:
- Iteration,Rocks DB能够支持区间范围内的key迭代器的遍历查找。
- Compaction Filter,用户可使用 Compaction Filter 对 key
值进行删除或其它更新操作的逻辑定义,当系统进行 Compact 行为的时候。 - Creating and Ingesting SST files,当用户想要快速导入大批量数据到系统内时,可以通过线下创建有效格式的
SST 文件并导入的方式,而非使用 API 进行 KV 值的单独PUT操作。 - Delete Range,区间范围的删除操作,比一个个 Key 的单独删除调用使用更方便。
- Low Priority Write,当用户执行大批量数据 load
的操作时但担心可能会影响到系统正常的操作处理时,可以开启此属性进行优先级的调整。 - Read-Modify-Write,这个操作的实际含义是 Merge操作的含义,读取现有键值,进行更新(累加计数或依赖原有值的任何更新操作),将新的值写入到原 Key 下。 如果使用原始 Get/Set API 的前提下,我们要调用2次 Get 1次,然后再 Set 1次,在 Merge API 下,使用者调用1次就足够了。
- Transaction,RocksDB 内部提供乐观式的 OptimisticTransactionDB 和悲观式(事务锁方式)的
TransactionDB 来支持并发的键值更新操作。
Terminology
首先是RocksDB内部的相关术语定义说明,如上图所示,主要有以下一些术语:
- Write-Ahead-Log File,类似于HDFS
JournalNode中的editlog,用于记录那些未被成功提交的数据操作,然后在重启时进行数据的恢复。 - SSTFile,SST文件是一段排序好的表文件,它是实际持久化的数据文件。里面的数据按照key进行排序能方便对其进行二分查找。在SST文件内,还额外包含以下特殊信息:
- Bloom Fileter,用于快速判断目标查询key是否存在于当前SST文件内。
- Index / Partition Index,SST内部数据块索引文件快速找到数据块的位置。
- Memtable,内存数据结构,用以存储最近更新的db更新操作,memtable空间写满后,会触发一次写出更新操作到SST文件的操作。
- Block Cache,纯内存存储结构,存储SST文件被经常访问的热点数据。
System Behavior
在RocksDB内部,有着许多系统操作行为来保障系统的平稳运行。
- Compression,SST文件内的数据能够被压缩存储来减小占用空间。
- Rate Limit行为。用户能够对其写操作进行速度控制,以此避免写入速度过快造成系统读延迟的现象。
- Delete Schedule,系统文件删除行为的速度控制。
- Direct IO,RocksDB支持绕过系统Page Cache,通过应用内存从存储设置中直接进行IO读写操作。
- Compaction,数据的Compact行为,删除SST文件中重复的key以及过期的key数据。
Logging/Monitoring
RocksDB内部有以下的日志监控工具:
- Logger,可用的Logger使用类。
- Statistic / Perf Context and IO Stats
Context,RocksDB内部各类型操作的时间,操作数计数统计信息,此数据信息能被用户用来发现系统的性能瓶颈操作。 - EventListener,此监听接口提供了一些event事件发生后的接口回调,比如完成一次flush操作,开始Compact操作的时候等等。
使用
java使用rocksdb
添加依赖
首先,你需要在项目中添加 RocksDB 的依赖。如果你使用 Maven,可以在 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>6.29.3</version> <!-- 请使用最新版本 -->
</dependency>
基本操作
以下是一些基本的 RocksDB 操作示例:
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
public class RocksDBExample {
static {
RocksDB.loadLibrary();
}
public static void main(String[] args) {
try (final Options options = new Options().setCreateIfMissing(true)) {
//打开数据库
try (final RocksDB db = RocksDB.open(options, "path/to/db")) {
//插入数据
db.put("key1".getBytes(), "value1".getBytes());
//读取数据
byte[] value = db.get("key1".getBytes());
if (value != null) {
System.out.println(new String(value));
}
//删除
db.delete("key1".getBytes());
//迭代数据
try (final RocksIterator iterator = db.newIterator()) {
for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) {
System.out.println(new String(iterator.key()) + ": " + new String(iterator.value()));
}
}
}
} catch (RocksDBException e) {
e.printStackTrace();
}
}
}
关闭
如果没有使用try-with-resource,需要关闭数据库
db.close();
options.close();
配置
RocksDB 提供了许多配置选项来优化性能和行为。以下是一些常见的配置:
setCreateIfMissing(true):如果数据库不存在则创建。
setMaxOpenFiles(5000):设置最大打开文件数。
setWriteBufferSize(64 * 1024 * 1024):设置写缓冲区大小。
setMaxWriteBufferNumber(4):设置最大写缓冲区数量。
列族
在 RocksDB 中,列族(Column Families) 是一种将键值对分组管理的机制。每个列族都是一个独立的命名空间,允许在同一数据库中存储多个逻辑上独立的数据集。db可以认为是数据库,列族对应数据库中的表。列族的使用可以提高数据管理的灵活性,并支持更高效的查询和操作。
如果没有指定 Column Family,键值对将会结合到“default” 列族。
列族的特点
- 独立命名空间:每个列族有自己独立的键值对集合。
- 共享 WAL(Write-Ahead Log):所有列族共享同一个 WAL,确保原子性。
- 独立配置:每个列族可以有自己的配置(如压缩策略、内存大小等)。
- 高效查询:可以单独查询某个列族的数据,减少扫描范围。
列族的基本操作
import org.rocksdb.*;
import java.util.ArrayList;
import java.util.List;
public class RocksDBColumnFamilyExample {
static {
RocksDB.loadLibrary();
}
public static void main(String[] args) {
try (final Options options = new Options().setCreateIfMissing(true);
final ColumnFamilyOptions cfOptions = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) {
// 定义列族名称
final List<ColumnFamilyDescriptor> cfDescriptors = new ArrayList<>();
cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOptions));
cfDescriptors.add(new ColumnFamilyDescriptor("cf1".getBytes(), cfOptions));
cfDescriptors.add(new ColumnFamilyDescriptor("cf2".getBytes(), cfOptions));
// 打开数据库并获取列族句柄
final List<ColumnFamilyHandle> cfHandles = new ArrayList<>();
try (final RocksDB db = RocksDB.open(options, "path/to/db", cfDescriptors, cfHandles)) {
// 获取默认列族和自定义列族的句柄
ColumnFamilyHandle defaultCfHandle = cfHandles.get(0);
ColumnFamilyHandle cf1Handle = cfHandles.get(1);
ColumnFamilyHandle cf2Handle = cfHandles.get(2);
// 插入数据到不同列族
db.put(cf1Handle, "key1".getBytes(), "value1".getBytes());
db.put(cf2Handle, "key2".getBytes(), "value2".getBytes());
// 从列族中读取数据
byte[] value1 = db.get(cf1Handle, "key1".getBytes());
byte[] value2 = db.get(cf2Handle, "key2".getBytes());
System.out.println("cf1:key1 -> " + new String(value1));
System.out.println("cf2:key2 -> " + new String(value2));
// 关闭列族句柄
for (ColumnFamilyHandle handle : cfHandles) {
handle.close();
}
}
} catch (RocksDBException e) {
e.printStackTrace();
}
}
}
对列族的put/get/delete操作时,要指定列族
列族的管理
创建新列族
ColumnFamilyHandle newCfHandle = db.createColumnFamily(new ColumnFamilyDescriptor("new_cf".getBytes(), cfOptions));
删除
db.dropColumnFamily(cfHandle);
cfHandle.close(); // 关闭句柄
列出所有列族
List<byte[]> columnFamilies = RocksDB.listColumnFamilies(new Options(), "path/to/db");
for (byte[] cfName : columnFamilies) {
System.out.println(new String(cfName));
}
- 列族的配置
每个列族可以有自己的配置选项,例如:
- 压缩策略:setCompressionType(CompressionType.SNAPPY_COMPRESSION)
- 内存大小:setWriteBufferSize(64 * 1024 * 1024)
- 合并操作:optimizeUniversalStyleCompaction()
示例:
ColumnFamilyOptions cfOptions = new ColumnFamilyOptions()
.setCompressionType(CompressionType.SNAPPY_COMPRESSION)
.setWriteBufferSize(64 * 1024 * 1024);