written by Alex Stocks on 2018/03/28,版权全部,无受权不得转载html
近日工做中使用了 RocksDB。RocksDB 的优势此处无需多说,它的一个 feature 是其有不少优化选项用于对 RocksDB 进行调优。欲熟悉这些参数,必须对其背后的原理有所了解,本文主要整理一些 RocksDB 的 wiki 文档,以备本身参考之用。c++
先介绍一些 RocksDB 的基本操做和基本架构。git
参考文档5提到RocksDB 是一个快速存储系统,它会充分挖掘 Flash or RAM 硬件的读写特性,支持单个 KV 的读写以及批量读写。RocksDB 自身采用的一些数据结构如 LSM/SKIPLIST 等结构使得其有读放大、写放大和空间使用放大的问题。github
LSM 大体结构如上图所示。LSM 树并且经过批量存储技术规避磁盘随机写入问题。 LSM 树的设计思想很是朴素, 它的原理是把一颗大树拆分红N棵小树, 它首先写入到内存中(内存没有寻道速度的问题,随机写的性能获得大幅提高),在内存中构建一颗有序小树,随着小树愈来愈大,内存的小树会flush到磁盘上。磁盘中的树按期能够作 merge 操做,合并成一棵大树,以优化读性能【读数据的过程可能须要从内存 memtable 到磁盘 sstfile 读取屡次,称之为读放大】。RocksDB 的 LSM 体如今多 level 文件格式上,最热最新的数据尽在 L0 层,数据在内存中,最冷最老的数据尽在 LN 层,数据在磁盘或者固态盘上。RocksDB 还有一种日志文件叫作 manifest,用于记录对 sstfile 的更改,能够认为是 RocksDB 的 GIF,后面将会详述。算法
LSM-Tree(Log-Structured-Merge-Tree)
LSM从命名上看,容易望文生义成一个具体的数据结构,一个tree。但LSM并非一个具体的数据结构,也不是一个tree。LSM是一个数据结构的概念,是一个数据结构的设计思想。实际上,要是给LSM的命名断句,Log和Structured这两个词是合并在一块儿的,LSM-Tree应该断句成Log-Structured、Merge、Tree三个词汇,这三个词汇分别对应如下三点LSM的关键性质:数据库
很明显,LSM牺牲了一部分读的性能和增长了合并的开销,换取了高效的写性能。那LSM为何要这么作?实际上,这就关系到对于磁盘写已经没有什么优化手段了,而对于磁盘读,不论硬件仍是软件上都有优化的空间。经过多种优化后,读性能虽然还是降低,但能够控制在可接受范围内。实际上,用于磁盘上的数据结构不一样于用于内存上的数据结构,用于内存上的数据结构性能的瓶颈就在搜索复杂度,而用于磁盘上的数据结构性能的瓶颈在磁盘IO,甚至是磁盘IO的模式。express
以上三段摘抄自参考文档20。我的觉得,除了将随机写合并以后转化为顺写以外,LSM 的另一个关键特性就在于其是一种自带数据 Garbage Collect 的有序数据集合,对外只提供了 Add/Get 接口,其内部的 Compaction 就是其 GC 的关键,经过 Compaction 实现了对数据的删除、附带了 TTL 的过时数据地淘汰、同一个 Key 的多个版本 Value 地合并。RocksDB 基于 LSM 对外提供了 Add/Delete/Get 三个接口,用户则基于 RocksDB 提供的 transaction 还能够实现 Update 语义。编程
RocksDB的三种基本文件格式是 memtable/sstfile/logfile,memtable 是一种内存文件数据系统,新写数据会被写进 memtable,部分请求内容会被写进 logfile。logfile 是一种有利于顺序写的文件系统。memtable 的内存空间被填满以后,会有一部分老数据被转移到 sstfile 里面,这些数据对应的 logfile 里的 log 就会被安全删除。sstfile 中的内容是有序的。json
上图所示,全部 Column Family 共享一个 WAL 文件,可是每一个 Column Family 有本身单独的 memtable & ssttable(sstfile),即 log 共享而数据分离。后端
一个进程对一个 DB 同时只能建立一个 rocksdb::DB 对象,全部线程共享之。这个对象内部有锁机制保证访问安全,多个线程同时进行 Get/Put/Fetch Iteration 都没有问题,可是若是直接 Iteration 或者 WriteBatch 则须要额外的锁同步机制保护 Iterator 或者 WriteBatch 对象。
基于 RocksDB 设计存储系统,要考虑到应用场景进行各类 tradeoff 设置相关参数。譬如,若是 RocksDB 进行 compaction 比较频繁,虽然有利于空间和读,可是会形成读放大;compaction 太低则会形成读放大和空间放大;增大每一个 level 的 comparession 难度能够减少空间放大,可是会增长 cpu 负担,是运算时间增长换取使用空间减少;增大 SSTfile 的 data block size,则是增大内存使用量来加快读取数据的速度,减少读放大。
单独的 Get/Put/Delete 是原子操做,要么成功要么失败,不存在中间状态。
若是须要进行批量的 Get/Put/Delete 操做且须要操做保持原子属性,则可使用 WriteBatch。
WriteBatch 还有一个好处是保持加快吞吐率。
默认状况下,RocksDB 的写是异步的:仅仅把数据写进了操做系统的缓存区就返回了,而这些数据被写进磁盘是一个异步的过程。若是为了数据安全,能够用以下代码把写过程改成同步写:
rocksdb::WriteOptions write_options; write_options.sync = true; db->Put(write_options, …);
这个选项会启用 Posix 系统的 fsync(...) or fdatasync(...) or msync(..., MS_SYNC)
等函数。
异步写的吞吐率是同步写的一千多倍。异步写的缺点是机器或者操做系统崩溃时可能丢掉最近一批写请求发出的由操做系统缓存的数据,可是 RocksDB 自身崩溃并不会致使数据丢失。而机器或者操做系统崩溃的几率比较低,因此大部分状况下能够认为异步写是安全的。
RocksDB 因为有 WAL 机制保证,因此即便崩溃,其重启后会进行写重放保证数据一致性。若是不在意数据安全性,能够把 write_option.disableWAL
设置为 true,加快写吞吐率。
RocksDB 调用 Posix API fdatasync()
对数据进行异步写。若是想用 fsync()
进行同步写,能够设置 Options::use_fsync
为 true。
RocksDB 可以保存某个版本的全部数据(可称之为一个 Snapshot)以方便读取操做,建立并读取 Snapshot 方法以下:
rocksdb::ReadOptions options; options.snapshot = db->GetSnapshot(); … apply some updates to db …. rocksdb::Iterator* iter = db->NewIterator(options); … read using iter to view the state when the snapshot was created …. delete iter; db->ReleaseSnapshot(options.snapshot);
若是 ReadOptions::snapshot 为 null,则读取的 snapshot 为 RocksDB 当前版本数据的 snapshot。
无论是 it->key()
仍是 it->value()
,其值类型都是 rocksdb::Slice
。 Slice 自身由一个长度字段[ sizet size ]以及一个指向外部一个内存区域的指针[ const char* data_ ]构成,返回 Slice 比返回一个 string 廉价,并不存在内存拷贝的问题。RocksDB 自身会给 key 和 value 添加一个 C-style 的 ‘\0’,因此 slice 的指针指向的内存区域自身做为字符串输出没有问题。
Slice 与 string 之间的转换代码以下:
rocksdb::Slice s1 = “hello”; std::string str(“world”); rocksdb::Slice s2 = str; OR: std::string str = s1.ToString(); assert(str == std::string(“hello”));
可是请注意 Slice 的安全性,有代码以下:
rocksdb::Slice slice; if (…) { std::string str = …; slice = str; } Use(slice);
当退出 if 语句块后,slice 内部指针指向的内存区域已经不存在,此时再使用致使程序出问题。
Slice 自身虽然可以减小内存拷贝,可是在离开相应的 scope 以后,其值就会被释放,rocksdb v5.4.5 版本引入一个 PinnableSlice,其继承自 Slice,可替换以前 Get 接口的出参:
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, std::string* value) virtual Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, PinnableSlice* value)
这里的 PinnableSlice 如同 Slice 同样能够减小内存拷贝,提升读性能,可是 PinnableSlice 内部有一个引用计数功能,能够实现数据内存的延迟释放,延长相关数据的生命周期,相关详细分析详见 参考文档15。
参考文档16 提到 PinnableSlice, as its name suggests, has the data pinned in memory. The pinned data are released when PinnableSlice object is destructed or when ::Reset is invoked explicitly on it.
。所谓的 pinned in memory 即为引用计数之意,文中提到内存数据释放是在 PinnableSlice 析构或者调用 ::Reset 以后。
用户也能够把一个 std::string 对象做为 PinnableSlice 构造函数的参数, 把这个 std::string 指定为 PinnableSlice 的初始内部 buffer [ rocksdb/slice.h:PinnableSlice::buf_ ],使用方法能够参考 用新 Get 实现的旧版本的 Get:
virtual inline Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, std::string* value) { assert(value != nullptr); PinnableSlice pinnable_val(value); assert(!pinnable_val.IsPinned()); auto s = Get(options, column_family, key, &pinnable_val); if (s.ok() && pinnable_val.IsPinned()) { value->assign(pinnable_val.data(), pinnable_val.size()); } // else value is already assigned return s; }
经过 rocksdb/slice.h:PinnableSlice::PinSlice 实现代码能够看出,只有在这个函数里 PinnableSlice::pinned_ 被赋值为 true, 同时内存区域存放在 PinnableSlice::data_ 指向的内存区域,故而 PinnableSlice::IsPinned 为 true,则 内部 buffer [ rocksdb/slice.h:PinnableSlice::buf_ ] 一定为空。
具体的编程用例可参考 pinnalble_slice.cc。
当使用 TransactionDB 或者 OptimisticTransactionDB 的时候,可使用 RocksDB 的 BEGIN/COMMIT/ROLLBACK 等事务 API。RocksDB 支持活锁或者死等两种事务。
WriteBatch 默认使用了事务,确保批量写成功。
当打开一个 TransactionDB 的时候,若是 RocksDB 检测到某个 key 已经被别的事务锁住,则 RocksDB 会返回一个 error。若是打开成功,则全部相关 key 都会被 lock 住,直到事务结束。TransactionDB 的并发特性表现要比 OptimisticTransactionDB 好,可是 TransactionDB 的一个小问题就是无论写发生在事务里或者事务外,他都会进行写冲突检测。TransactionDB 使用示例代码以下:
TransactionDB* txn_db; Status s = TransactionDB::Open(options, path, &txn_db); Transaction* txn = txn_db->BeginTransaction(write_options, txn_options); s = txn->Put(“key”, “value”); s = txn->Delete(“key2”); s = txn->Merge(“key3”, “value”); s = txn->Commit(); delete txn;
OptimisticTransactionDB 提供了一个更轻量的事务实现,它在进行写以前不会进行写冲突检测,当对写操做进行 commit 的时候若是发生了 lock 冲突致使写操做失败,则 RocksDB 会返回一个 error。这种事务使用了活锁策略,适用于读多写少这种写冲突几率比较低的场景下,使用示例代码以下:
DB* db; OptimisticTransactionDB* txn_db; Status s = OptimisticTransactionDB::Open(options, path, &txn_db); db = txn_db->GetBaseDB(); OptimisticTransaction* txn = txn_db->BeginTransaction(write_options, txn_options); txn->Put(“key”, “value”); txn->Delete(“key2”); txn->Merge(“key3”, “value”); s = txn->Commit(); delete txn;
参考文档 6 详细描述了 RocksDB
的 Transactions。
CF 提供了对 DB 进行逻辑划分开来的方法,用户能够经过 CF 同时对多个 CF 的 KV 进行并行读写的方法,提升了并行度。
RocksDB 内存中的数据格式是 skiplist,磁盘则是以 table 形式存储的 SST 文件格式。
table 格式有两种:继承自 leveldb 的文件格式【详见参考文档2】和 PlainTable 格式【详见参考文档3】。PlainTable 格式是针对 低查询延迟 或者低延迟存储媒介如 SSD 特别别优化的一种文件格式。
RocksDB 把相邻的 key 放到同一个 block 中,block 是数据存储和传递的基本单元。默认 Block 的大小是 4096B,数据未经压缩。
常常进行 bulk scan 操做的用户可能但愿增大 block size,而常常进行单 key 读写的用户则可能但愿减少其值,官方建议这个值减少不要低于 1KB 的下限,变大也不要超过 a few megabytes
。启用压缩也能够起到增大 block size 的好处。
修改 Block size 的方法是修改 Options::block_size
。
Options::write_buffer_size
指定了一个写内存 buffer 的大小,当这个 buffer 写满以后数据会被固化到磁盘上。这个值越大批量写入的性能越好。
RocksDB 控制写内存 buffer 数目的参数是 Options::max_write_buffer_number
。这个值默认是 2,当一个 buffer 的数据被 flush 到磁盘上的时候,RocksDB 就用另外一个 buffer 做为数据读写缓冲区。
‘Options::minwritebuffernumberto_merge’ 设定了把写 buffer 的数据固化到磁盘上时对多少个 buffer 的数据进行合并而后再固化到磁盘上。这个值若是为 1,则 L0 层文件只有一个,这会致使读放大,这个值过小会致使数据固化到磁盘上以前数据去重效果太差劲。
这两个值并非越大越好,太大会延迟一个 DB 被从新打开时的数据加载时间。
在 1.8 章节里提到 “block 是数据存储和传递的基本单元”,RocksDB 的数据是一个 range 的 key-value 构成一个 Region,根据局部性原理每次访问一个 Region 的 key 的时候,有不少几率会访问其相邻的 key,每一个 Region 的 keys 放在一个 block 里,多个 Region 的 keys 放在多个 block 里。
下面以文件系统做为类比,详细解释下 RocksDB 的文件系统:
filename -> permission-bits, length, list of fileblockids
fileblockid -> data
以多个维度组织 key 的时候,咱们可能但愿 filename 的前缀都是 ‘/‘, 而 fileblockid 的前缀都是 ‘0’,这样能够把他们分别放在不一样的 block 里,以方便快速查询。
Rocksdb 对每一个 kv 以及总体数据文件都分别计算了 checksum,以进行数据正确性校验。下面有两个选项对 checksum 的行为进行控制。
ReadOptions::verify_checksums
强制对每次从磁盘读取的数据进行校验,这个选项默认为 true。Options::paranoid_checks
这个选项为 true 的时候,若是 RocksDB 打开一个数据检测到内部数据部分错乱,立刻抛出一个错误。这个选择默认为 false。若是 RocksDB 的数据错乱,RocksDB 会尽可能把它隔离出来,保证大部分数据的可用性和正确性。
GetApproximateSizes
方法能够返回一个 key range 的磁盘占用空间大体使用量,示例代码以下:
rocksdb::Range ranges[2]; ranges[0] = rocksdb::Range(“a”, “c”); ranges[1] = rocksdb::Range(“x”, “z”); uint64_t sizes[2]; rocksdb::Status s = db->GetApproximateSizes(ranges, 2, sizes);
上面的 sizes[0]
返回 [a..c)
key range 的磁盘使用量,而 sizes[1]
返回 [x..z)
key range 的磁盘使用量。
通常状况下,RocksDB 会删除一些过期的 WAL 文件,所谓过期就是 WAL 文件里面对应的一些 key 的数据已经被固化到磁盘了。可是 RocksDB 提供了两个选项以实让用户控制 WAL 什么时候删除:Options::WAL_ttl_seconds
和 Options::WAL_size_limit_MB
,这两个参数分别控制 WAL 文件的超时时间 和 最大文件 size。
若是这两个值都被设置为 0,则 log 不会被固化到文件系统上。
若是 Options::WAL_ttl_seconds
为 0 而 Options::WAL_size_limit_MB
不为 0, RocksDB 会每 10 分钟检测全部的 WAL 文件,若是其整体 size 超过 Options::WAL_size_limit_MB
,则 RocksDB 会删除最先的日志直到知足这个值位置。一切空文件都会被删除。
若是 Options::WAL_ttl_seconds
不为 0 而 Options::WAL_size_limit_MB
为 0,RocksDB 会每 Options::WAL_ttl_seconds
/ 2 检测一次 WAL 文件, 全部 TTL 超过 Options::WAL_ttl_seconds
的 WAL 文件都会被删除。
若是两个值都不为 0,RocksDB 会每 10 分钟检测全部的 WAL 文件,全部不知足条件的 WAL 文件都会被删除,其中 ttl 参数优先。
许多 LSM 引擎不支持高效的 RangeScan 操做,由于 Range 操做须要扫描全部的数据文件。通常状况下常规的技术手段是给 key 创建索引,只用遍历 key 就能够了。应用能够经过确认 prefix_extractor
指定一个能够的前缀,RocksDB 能够为这些 key prefix 创建 Bloom 索引,以加快查询速度。
参考文档 5 的 Compaction Styles
一节提到,若是启用 Level Style Compaction
, L0 存储着 RocksDB 最新的数据,Lmax 存储着比较老的数据,L0 里可能存着重复 keys,可是其余层文件则不可能存在重复 key。每一个 compaction 任务都会选择 Ln 层的一个文件以及与其相邻的 Ln+1 层的多个文件进行合并,删除过时 或者 标记为删除 或者 重复 的 key,而后把合并后的文件放入 Ln+1 层。Compaction 过程会致使写放大【如写qps是10MB/s,可是实际磁盘io是50MB/s】效应,可是能够节省空间并减小读放大。
若是启用 Universal Style Compaction
,则只压缩 L0 的全部文件,合并后再放入 L0 层里。
RocksDB 的 compaction 任务线程不宜过多,过多容易致使写请求被 hang 住。
RocksDB 的 API GetUpdatesSince
可让调用者从 transaction log 获知最近被更新的 key(原文意为用 tail 方式读取 transaction log),经过这个 API 能够进行数据的增量备份。
RocksDB 在进行数据备份时候,能够调用 API DisableFileDeletions
中止删除文件操做,调用 API GetLiveFiles/GetSortedWalFiles
以检索活跃文件列表,而后进行数据备份。备份工做完成之后在调用 API EnableFileDeletions
让 RocksDB 再启动过时文件淘汰工做。
RocksDB 会建立一个 thread pool 与 Env 对象进行关联,线程池中线程的数目能够经过 Env::SetBackgroundThreads()
设定。经过这个线程池能够执行 compaction 与 memtable flush 任务。
当 memtable flush 和 compaction 两个任务同时执行的时候,会致使写请求被 hang 住。RocksDB 建议建立两个线程池,分别指定 HIGH 和 LOW 两个优先级。默认状况下 HIGH 线程池执行 memtable flush 任务,LOW 线程池执行 compaction 任务。
相关代码示例以下:
#include “rocksdb/env.h” #include “rocksdb/db.h” auto env = rocksdb::Env::Default(); env->SetBackgroundThreads(2, rocksdb::Env::LOW); env->SetBackgroundThreads(1, rocksdb::Env::HIGH); rocksdb::DB* db; rocksdb::Options options; options.env = env; options.max_background_compactions = 2; options.max_background_flushes = 1; rocksdb::Status status = rocksdb::DB::Open(options, “/tmp/testdb”, &db); assert(status.ok());
还有其余一些参数,可详细阅读参考文档4。
RocksDB 的每一个 SST 文件都包含一个 Bloom filter。Bloom Filter 只对特定的一组 keys 有效,因此只有新的 SST 文件建立的时候才会生成这个 filter。当两个 SST 文件合并的时候,会生成新的 filter 数据。
当 SST 文件加载进内存的时候,filter 也会被加载进内存,当关闭 SST 文件的时候,filter 也会被关闭。若是想让 filter 常驻内存,能够用以下代码设置:
BlockBasedTableOptions::cache_index_and_filter_blocks=true
通常状况下不要修改 filter 相关参数。若是须要修改,相关设置上面已经说过,此处再也不多谈,详细内容见参考文档 7。
RocksDB 在进行 compact 的时候,会删除被标记为删除的数据,会删除重复 key 的老版本的数据,也会删除过时的数据。数据过时时间由 API DBWithTTL::Open(const Options& options, const std::string& name, StackableDB** dbptr, int32_t ttl = 0, bool read_only = false)
的 ttl 参数设定。
TTL 的使用有如下注意事项:
infinity
,即永不过时;(int32_t)Timestamp
时间值;Timestamp+ttl<time_now
,则会被淘汰掉;DBWithTTL::Open
可能会带上不一样的 TTL 值,此时 kv 以最大的 TTL 值为准;DBWithTTL::Open
的参数 read_only
为 true,则不会触发 compact 任务,不会有过时数据被删除。RocksDB的内存大体有以下四个区:
第三节详述了 Block Cache,这里只给出总结性描述:它存储一些读缓存数据,它的下一层是操做系统的 Page Cache。
Index 由 key、offset 和 size 三部分构成,当 Block Cache 增大 Block Size 时,block 个数必会减少,index 个数也会随之下降,若是减少 key size,index 占用内存空间的量也会随之下降。
filter是 bloom filter 的实现,若是假阳率是 1%,每一个key占用 10 bits,则总占用空间就是 num_of_keys * 10 bits
,若是缩小 bloom 占用的空间,能够设置 options.optimize_filters_for_hits = true
,则最后一个 level 的 filter 会被关闭,bloom 占用率只会用到原来的 10% 。
结合 block cache 所述,index & filter 有以下优化选项:
cache_index_and_filter_blocks
这个 option 若是为 true,则 index & filter 会被存入 block cache,而 block cache 中的内容会随着 page cache 被交换到磁盘上,这就会大大下降 RocksDB的性能,把这个 option 设为 true 的同时也把 pin_l0_filter_and_index_blocks_in_cache
设为 true,以减少对性能的影响。若是 cache_index_and_filter_blocks
被设置为 false (其值默认就是 false),index/filter 个数就会受 max_open_files
影响,官方建议把这个选项设置为 -1,以方便 RocksDB 加载全部的 index 和 filter 文件,最大化程序性能。
能够经过以下代码获取 index & filter 内存量大小:
c++
std::string out;
db->GetProperty(“rocksdb.estimate-table-readers-mem”, &out);
block cache、index & filter 都是读 buffer,而 memtable 则是写 buffer,全部 kv 首先都会被写进 memtable,其 size 是 write_buffer_size
。 memtable 占用的空间越大,则写放大效应越小,由于数据在内存被整理好,磁盘上就越少的内容会被 compaction。若是 memtable 磁盘空间增大,则 L1 size 也就随之增大,L1 空间大小受 max_bytes_for_level_base
option 控制。
能够经过以下代码获取 memtable 内存量大小:
std::string out; db->GetProperty(“rocksdb.cur-size-all-mem-tables”, &out);
这部份内存空间通常占用总量很少,可是若是有 100k 之多的transactions 发生,每一个 iterator 与一个 data block 外加一个 L1 的 data block,因此内存使用量大约为 num_iterators * block_size * ((num_levels-1) + num_l0_files)
。
能够经过以下代码获取 Pin Blocks 内存量大小:
c++
table_options.block_cache->GetPinnedUsage();
RocksDB 的读流程分为逻辑读(logical read)和物理读(physical read)。逻辑读一般是对 cache【Block Cache & Table Cache】进行读取,物理读就是直接读磁盘。
参考文档 12 详细描述了 LeveDB(RocksDB)的读流程,转述以下:
在第0层SSTable中查找,没法命中转到下一流程;
对于L0 的文件,RocksDB 采用遍历的方法查找,因此为了查找效率 RocksDB 会控制 L0 的文件个数。
在剩余SSTable中查找。
对于 L1 层以及 L1 层以上层级的文件,每一个 SSTable 没有交叠,可使用二分查找快速找到 key 所在的 Level 以及 SSTfile。
至于写流程,请参阅 ### 5 Flush & Compaction 章节内容。
无论 RocksDB 有多少 column family,一个 DB 只有一个 WriteController,一旦 DB 中一个 column family 发生堵塞,那么就会阻塞其余 column family 的写。RocksDB 写入时间长了之后,可能会不定时出现较大的写毛刺,可能有两个地方致使 RocksDB 会出现较大的写延时:获取 mutex 时可能出现几十毫秒延迟 和 将数据写入 memtable 时候可能出现几百毫秒延时。
获取 mutex 出现的延迟是由于 flush/compact 线程与读写线程竞争致使的,能够经过调整线程数量下降毛刺时间。
至于写入 memtable 时候出现的写毛刺时间,解决方法一就是使用大的 page cache,禁用系统 swap 以及配置 min_free_kbytes、dirty_ratio、dirty_background_ratio 等参数来调整系统的内存回收策略,更基础的方法是使用内存池。
采用内存池时,memtable 的内存分配和回收流程图以下:
使用内存池时,RocksDB 的内容分配代码模块以下:
Block Cache 是 RocksDB 的数据的缓存,这个缓存能够在多个 RocksDB 的实例下缓存。通常默认的Block Cache 中存储的值是未压缩的,而用户能够再指定一个 Block Cache,里面的数据能够是压缩的。用户访问数据先访问默认的 Block Cache,待没法获取后再访问用户 Cache,用户 Cache 的数据能够直接存入 page cache 中。
Cache 有两种:LRUCache 和 BlockCache。Block 分为不少 Shard,以减少竞争,因此 shard 大小均匀一致相等,默认 Cache 最多有 64 个 shards,每一个 shard 的 最小 size 为 512k,总大小是 8M,类别是 LRU。
std::shared_ptr<Cache> cache = NewLRUCache(capacity); BlockedBasedTableOptions table_options; table_options.block_cache = cache; Options options; options.table_factory.reset(new BlockedBasedTableFactory(table_options));
这个 Cache 是不压缩数据的,用户能够设置压缩数据 BlockCache,方法以下:
table_options.block_cache_compressed = cache;
若是 Cache 为 nullptr,则RocksDB会建立一个,若是想禁用 Cache,能够设置以下 Option:
table_options.no_block_cache = true;
默认状况下RocksDB用的是 LRUCache,大小是 8MB, 每一个 shard 单独维护本身的 LRU list 和独立的 hash table,以及本身的 Mutex。
RocksDB还提供了一个 ClockCache,每一个 shard 有本身的一个 circular list,有一个 clock handle 会轮询这个 circular list,寻找过期的 kv,若是 entry 中的 kv 已经被访问过则能够继续存留,相对于 LRU 好处是无 mutex lock,circular list 本质是 tbb::concurrenthashmap,从 benchmark 来看,两者命中率类似,但吞吐率 Clock 比 LRU 稍高。
Block Cache初始化之时相关参数:
默认状况下 index 和filter block 与 block cache 是独立的,用户不能设定两者的内存空间使用量,但为了控制 RocksDB 的内存空间使用量,能够用以下代码把 index 和 filter 也放在 block cache 中:
BlockBasedTableOptions table_options; table_options.cache_index_and_filter_blocks = true;
index 与 filter 通常访问频次比 data 高,因此把他们放到一块儿会致使内存空间与 cpu 资源竞争,进而致使 cache 性能抖动厉害。有以下两个参数须要注意:cacheindexfilterblockswithhighpriority 和 highpripoolratio 同样,这个参数只对 LRU Cache 有效,二者须同时生效。这个选项会把 LRU Cache 划分为高 prio 和低 prio 区,data 放在 low 区,index 和 filter 放在 high 区,若是高区占用的内存空间超过了 capacity * highpripoolratio,则会侵占 low 区的尾部数据空间。
SimCache 用于评测 Cache 的命中率,它封装了一个真正的 Cache,而后用给定的 capacity 进行 LRU 测算,代码以下:
c++
// This cache is the actual cache use by the DB.
std::shared_ptr<Cache> cache = NewLRUCache(capacity);
// This is the simulated cache.
std::shared_ptr<Cache> sim_cache = NewSimCache(cache, sim_capacity, sim_num_shard_bits);
BlockBasedTableOptions table_options;
table_options.block_cache = sim_cache;
大概只有容量的 2% 会被用于测算。
RocksDB 3.0 之后添加了一个 Column Family【后面简称 CF】 的feature,每一个 kv 存储之时都必须指定其所在的 CF。RocksDB为了兼容以往版本,默认建立一个 “default” 的CF。存储 kv 时若是不指定 CF,RocksDB 会把其存入 “default” CF 中。
RocksDB 的 Option 有 Options, ColumnFamilyOptions, DBOptions 三种。
ColumnFamilyOptions 是 table 级的,而 Options 是 DB 级的,Options 继承自 ColumnFamilyOptions 和 DBOptions,它通常影响只有一个 CF 的 DB,如 “default”。
每一个 CF 都有一个 Handle:ColumnFamilyHandle,在 DB 指针被 delete 前,应该先 delete ColumnFamilyHandle。若是 ColumnFamilyHandle 指向的 CF 被别的使用者经过 DropColumnFamily 删除掉,这个 CF 仍然能够被访问,由于其引用计数不为 0.
在以 Read/Write 方式打开一个 DB 的时候,须要指定一个由全部将要用到的 CF string name 构成的 ColumnFamilyDescriptor array。无论 “default” CF 使用与否,都必须被带上。
CF 存在的意义是全部 table 共享 WAL,但不共享 memtable 和 table 文件,经过 WAL 保证原子写,经过分离 table 可快读快写快删除。每次 flush 一个 CF 后,都会新建一个 WAL,都这并不意味着旧的 WAL 会被删除,由于别的 CF 数据可能尚未落盘,只有全部的 CF 数据都被 flush 且全部的 WAL 有关的 data 都落盘,相关的 WAL 才会被删除。RocksDB 会定时执行 CF flush 任务,能够经过 Options::max_total_wal_size
查看已有多少旧的 CF 文件已经被 flush 了。
RocksDB 会在磁盘上依据 LSM 算法对多级磁盘文件进行 compaction,这会影响写性能,拖慢程序性能,能够经过 WriteOptions.low_pri = true
下降 compaction 的优先级。
RocksDB 有不少选项以专门的目的进行设置,可是大部分状况下不须要进行特殊的优化。这里只列出一个经常使用的优化选项。
cf_options.write_buffer_size
CF 的 write buffer 的最大 size。最差状况下 RocksDB 使用的内存量会翻倍,因此通常状况下不要轻易修改其值。
这个值通常设置为 RocksDB 想要使用的内存总量的 1/3,其他的留给 OS 的 page cache。
BlockBasedTableOptions table_options; … \\ set options in table_options options.table_factory.reset(new std::shared_ptr<Cache> cache = NewLRUCache(<your_cache_size>); table_options.block_cache = cache; BlockBasedTableFactory(table_options));
本进程的全部的 DB 全部的 CF 全部的 table_options 都必须使用同一个 cahce 对象,或者让全部的 DB 全部的 CF 使用同一个 table_options。
cf_options.compression, cf_options.bottonmost_compression
选择压缩方法跟你的机器、CPU 能力以及内存磁盘空间大小有关,官方推荐 cf_options.compression
使用 kLZ4Compression,cf_options.bottonmost_compression
使用 kZSTD,选用的时候要确认你的机器有这两个库,这两个选项也能够分别使用 Snappy 和 Zlib。
官方真正建议修改的参数只有这个 filter 参数。若是大量使用迭代方法,不要修改这个参数,若是大量调用 Get() 接口,建议修改这个参数。修改方法以下:
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
一个可能的优化设定以下:
cf_options.level_compaction_dynamic_level_bytes = true; options.max_background_compactions = 4; options.max_background_flushes = 2; options.bytes_per_sync = 1048576; table_options.block_size = 16 * 1024; table_options.cache_index_and_filter_blocks = true; table_options.pin_l0_filter_and_index_blocks_in_cache = true;
上面只是罗列了一些优化选项,这些选项也只能在进程启动的时候设定。更多的选项请详细阅读参考文档1。
参考文档 5 的 Persistence 一节提到,RocksDB 每次接收写请求的时候,请求内容会先被写入 WAL transaction log,而后再把数据写入 memfile 里面。
Put 函数的参数 WriteOptions 里有一个选项能够指明是否须要把写请求的内容写入 WAL log 里面。
RocksDB 内部有一个 batch-commit 机制,经过一次 commit 批量地在一次 sync 操做里把全部 transactions log 写入磁盘。
RocksDB 的内存数据在 memtable 中存着,有 active-memtable 和 immutable-memtable 两种。active-memtable 是当前被写操做使用的 memtable,当 active-memtable 空间写满以后( Options.writebuffersize 控制其内存空间大小 )这个空间会被标记为 readonly 成为 immutable-memtable。memtable 实质上是一种有序 SkipList,因此写过程实际上是写 WAL 日志和数据插入 SkipList 的过程。
RocksDB 的数据删除过程跟写过程相同,只不过 插入的数据是 “key:删除标记”。
immutable-memtable 被写入 L0 的过程被称为 flush 或者 minor compaction。flush 的触发条件是 immutable memtable数量超过 minwritebuffernumberto_merge。flush 过程以 column family 为单位,一个 column family 会使用一个或者多个 immutable-memtable,flush 会一次把全部这些文件合并后写入磁盘的 L0 sstfile 中。
在 compaction 过程当中若是某个被标记为删除的 key 在某个 snapshot 中存在,则会被一直保留,直到 snapshot 不存在才会被删除。
RocksDB 的 compaction 策略分为 Universal Compaction
和 Leveled Compaction
两种。两种策略分别有不一样的使用场景,下面分两个章节详述。综述就是 Leveled Compaction
有利于减少空间放大却会增长读放大,Universal Compaction
有利于减小读放大却会增大空间放大。
compaction 的触发条件是文件个数和文件大小。L0 的触发条件是 sst 文件个数(level0filenumcompactiontrigger 控制),触发 compaction score 是 L0 sst 文件个数与 level0filenumcompactiontrigger 的比值或者全部文件的 size 超过 maxbytesforlevelbase。L1 ~ LN 的触发条件则是 sst 文件的大小。
若是 level_compaction_dynamic_level_bytes
为 false,L1 ~ LN 每一个 level 的最大容量由 max_bytes_for_level_base
和 max_bytes_for_level_multiplier
决定,其 compaction score 就是当前总容量与设定的最大容量之比,若是某 level 知足 compaction 的条件则会被加入 compaction 队列。
若是 level_compaction_dynamic_level_bytes
为 true,则 Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier
,此时若是某 level 计算出来的 target 值小于 max_bytes_for_level_base / max_bytes_for_level_multiplier
,则 RocksDB 不会再这个 level 存储任何 sst 数据。
5.1.1 Compaction Score
compact 流程的 Compaction Score,不一样 level 的计算方法不同,下面先列出 L0 的计算方法。其中 num 表明未 compact 文件的数目。
Param | Value | Description | Score |
---|---|---|---|
level0filenumcompactiontrigger | 4 | num 为 4 时,达到 compact 条件 | num < 20 时 Score = num / 4 |
level0slowdownwrites_trigger | 20 | num 为 20 时,RocksDB 会减慢写入速度 | 20 <= num && num < 24 时 Score = 10000 |
level0stopwrites_trigger | 24 | num 为 24 时,RocksDB 中止写入文件,尽快对 L0 进行 compact | 24 <= num 时 Score = 1000000 |
对于 L1+ 层,score = LevelBytes / TargetSize。
5.1.2 Level Max Bytes
每一个 level 容量总大小的计算前文已经提过,
Param | Value | Description |
---|---|---|
maxbytesforlevelbase | 10485760 | L1 总大小 |
maxbytesforlevelmultiplier | 10 | 最大乘法因子 |
maxbytesforlevelmultiplier_addtl[2…6] | 1 | L2 ~ L6 总大小调整参数 |
每一个 level 的总大小计算公式为 Level_max_bytes[N] = Level_max_bytes[N-1] * max_bytes_for_level_multiplier^(N-1)*max_bytes_for_level_multiplier_additional[N-1]
。
5.1.3 compact file
上面详述了 compact level 的选择,可是每一个 level 具体的 compact 文件对象,
L0 层全部文件会被选作 compact 对象,由于它们有很高的几率全部文件的 key range 发生重叠。
对于 L1+ 层的文件,先对全部文件的大小进行排序以选出最大文件。
LevelDB 的文件选取过程以下:
LN 中每一个文件都一个 seek 数值,其默认值非零,每次访问后这个数值减 1,其值越小说明访问越频繁。sst 文件的策略以下:
5.1.4 compaction
大体的 compaction 流程大体为:
5.1.5 并行 Compact 与 sub-compact
参数 maxbackgroundcompactions 大于 1 时,RocksDB 会进行并行 Compact,但 L0 和 L1 层的 Compaction 任务不能进行并行。
一次 compaction 只能 compact 一个或者多个文件,这会约束总体 compaction 速度。用户能够设置 max_subcompactions 参数大于 1,RocksDB 如上图同样尝试把一个文件拆为多个 sub,而后启动多个线程执行 sub-compact。
Univesal Compaction 主要针对 L0。当 L0 中的文件个数多于 level0_file_num_compaction_trigger
,则启动 compact。
L0 中全部的 sst 文件均可能存在重叠的 key range,假设全部的 sst 文件组成了文件队列 R1,R2,R3,...,Rn,R1 文件的数据是最新的,R2 其次,Rn 则包含了最老的数据,其 compact 流程以下:
max_size_amplification_percent
,则对全部的 sst 进行 compaction(就是所谓的 full compaction);Universal Compaction
主要针对低写放大场景,跟 Leveled Compaction
相比一次合并文件较多但由于一次只处理 L0 因此写放大总体较低,可是空间放大效应比较大。
RocksDB 还支持一种 FIFO 的 compaction。FIFO 顾名思义就是先进先出,这种模式周期性地删除旧数据。在 FIFO 模式下,全部文件都在 L0,当 sst 文件总大小超过阀值 maxtablefiles_size,则删除最老的 sst 文件。参考文档21中提到能够基于 FIFO compaction 机制把 RocksDB 当作一个时序数据库:对于 FIFO 来讲,它的策略很是的简单,全部的 SST 都在 Level 0,若是超过了阈值,就从最老的 SST 开始删除,其实能够看到,这套机制很是适合于存储时序数据
。
整个 compaction 是 LSM-tree 数据结构的核心,也是rocksDB的核心,详细内容请阅读 参考文档8 和 参考文档9。
RocksDB 自身之提供了 Put/Delete/Get 等接口,若须要在现有值上进行修改操做【或者成为增量更新】,能够借助这三个操做进行如下操做实现之:
若是但愿整个过程是原子操做,就须要借助 RocksDB 的 Merge 接口了。参考文档14 给出了 RocksDB Merge 接口定义以下:
RocksDB 提供了一个 MergeOperator 做为 Merge 接口,其中一个子类 AssociativeMergeOperator 可在大部分场景下使用,其定义以下:
// The simpler, associative merge operator. class AssociativeMergeOperator : public MergeOperator { public: virtual ~AssociativeMergeOperator() {} // Gives the client a way to express the read -> modify -> write semantics // key: (IN) 操做对象 KV 的 key // existing_value:(IN) 操做对象 KV 的 value,若是为 null 则意味着 KV 不存在 // value: (IN) 新值,用于替换/更新 @existing_value // new_value: (OUT) 客户端负责把 merge 后的新值填入这个变量 // logger: (IN) Client could use this to log errors during merge. // // Return true on success. // All values passed in will be client-specific values. So if this method // returns false, it is because client specified bad data or there was // internal corruption. The client should assume that this will be treated // as an error by the library. virtual bool Merge(const Slice& key, const Slice* existing_value, const Slice& value, std::string* new_value, Logger* logger) const = 0; private: // Default implementations of the MergeOperator functions virtual bool FullMergeV2(const MergeOperationInput& merge_in, MergeOperationOutput* merge_out) const override; virtual bool PartialMerge(const Slice& key, const Slice& left_operand, const Slice& right_operand, std::string* new_value, Logger* logger) const override; };
RocksDB AssociativeMergeOperator 被称为关联性 Merge Operator,参考文档14 给出了关联性的定义:
**MergeOperator还能够用于非关联型数据类型的更新。** 例如,在RocksDB中保存json字符串,即Put接口写入data的格式为合法的json字符串。而Merge接口只但愿更新json中的某个字段。因此代码多是这样
:
// Put/store the json string into to the database db_->Put(put_option_, "json_obj_key", "{ employees: [ {first_name: john, last_name: doe}, {first_name: adam, last_name: smith}] }"); // Use a pre-defined "merge operator" to incrementally update the value of the json string db_->Merge(merge_option_, "json_obj_key", "employees[1].first_name = lucy"); db_->Merge(merge_option_, "json_obj_key", "employees[0].last_name = dow"); `AssociativeMergeOperator没法处理这种场景,由于它假设Put和Merge的数据格式是关联的。咱们须要区分Put和Merge的数据格式,也没法把多个merge操做数合并成一个。这时候就须要Generic MergeOperator。` // The Merge Operator // // Essentially, a MergeOperator specifies the SEMANTICS of a merge, which only // client knows. It could be numeric addition, list append, string // concatenation, edit data structure, ... , anything. // The library, on the other hand, is concerned with the exercise of this // interface, at the right time (during get, iteration, compaction...) class MergeOperator { public: virtual ~MergeOperator() {} // Gives the client a way to express the read -> modify -> write semantics // key: (IN) The key that's associated with this merge operation. // existing: (IN) null indicates that the key does not exist before this op // operand_list:(IN) the sequence of merge operations to apply, front() first. // new_value: (OUT) Client is responsible for filling the merge result here // logger: (IN) Client could use this to log errors during merge. // // Return true on success. Return false failure / error / corruption. // 用于对已有的值作Put或Delete操做 virtual bool FullMerge(const Slice& key, const Slice* existing_value, const std::deque<std::string>& operand_list, std::string* new_value, Logger* logger) const = 0; // This function performs merge(left_op, right_op) // when both the operands are themselves merge operation types. // Save the result in *new_value and return true. If it is impossible // or infeasible to combine the two operations, return false instead. // 若是连续屡次对一个 key 进行操做,则能够能够借助 PartialMerge 将两个操做数合并. virtual bool PartialMerge(const Slice& key, const Slice& left_operand, const Slice& right_operand, std::string* new_value, Logger* logger) const = 0; // The name of the MergeOperator. Used to check for MergeOperator // mismatches (i.e., a DB created with one MergeOperator is // accessed using a different MergeOperator) virtual const char* Name() const = 0; };
当调用DB::Put()和DB:Merge()接口时, 并不须要马上计算最后的结果. RocksDB将计算的动做延后触发, 例如在下一次用户调用Get, 或者RocksDB决定作Compaction时. 因此, 当merge的动做真正开始作的时候, 可能积压(stack)了多个操做数须要处理. 这种状况就须要MergeOperator::FullMerge来对existing_value和一个操做数序列进行计算, 获得最终的值.
有时候, 在调用FullMerge以前, 能够先对某些merge操做数进行合并处理, 而不是将它们保存起来, 这就是PartialMerge的做用: 将两个操做数合并为一个, 减小FullMerge的工做量.
当遇到两个merge操做数时, RocksDB老是先会尝试调用用户的PartialMerge方法来作合并, 若是PartialMerge返回false才会保存操做数. 当遇到Put/Delete操做, 就会调用FullMerge将已存在的值和操做数序列传入, 计算出最终的值.
merge 操做数的格式和Put相同
多个顺序的merge操做数能够合并成一个
merge 操做数的格式和Put不一样
当多个merge操做数能够合并时,PartialMerge()方法返回true
*!!!: 本节文字摘抄自 参考文档14 。
参考文档 12 列举了 RocksDB 磁盘上数据文件的种类:
* db的操做日志 * 存储实际数据的 SSTable 文件 * DB的元信息 Manifest 文件 * 记录当前正在使用的 Manifest 文件,它的内容就是当前的 manifest 文件名 * 系统的运行日志,记录系统的运行信息或者错误日志。 * 临时数据库文件,repair 时临时生成的。
manifest 文件记载了全部 SSTable 文件的 key 的范围、level 级别等数据。
上面是 leveldb 的架构图,能够做为参考,明白各类文件的做用。
log 文件就是 WAL。
如上图,log 文件的逻辑单位是 Record,物理单位是 block,每一个 Record 能够存在于一个 block 中,也能够占用多个 block。Record 的详细结构见上图文字部分,其 type 字段的意义见下图。
从上图可见 Record type的意义:若是某 KV 过长则能够用多 Record 存储。
RocksDB 整个 LSM 树的信息须要常驻内存,以让 RocksDB 快速进行 kv 查找或者进行 compaction 任务,RocksDB 会用文件把这些信息固化下来,这个文件就是 Manifest 文件。RocksDB 称 Manifest 文件记录了 DB 状态变化的事务性日志,也就是说它记录了全部改变 DB 状态的操做。主要内容有事务性日志和数据库状态的变化。
RocksDB 的函数 VersionSet::LogAndApply 是对 Manifest 文件的更新操做,因此能够经过定位这个函数出现的位置来跟踪 Manifest 的记录内容。
Manifest 文件做为事务性日志文件,只要数据库有变化,Manifest都会记录。其内容 size 超过设定值后会被 VersionSet::WriteSnapShot 重写。
RocksDB 进程 Crash 后 Reboot 的过程当中,会首先读取 Manifest 文件在内存中重建 LSM 树,而后根据 WAL 日志文件恢复 memtable 内容。
上图是 leveldb 的 Manifest 文件结构,这个 Manifest 文件有如下文件内容:
RocksDB MANIFEST文件所保存的数据基本是来自于VersionEdit这个结构,MANIFEST包含了两个文件,一个log文件一个包含最新MANIFEST文件名的文件,Manifest的log文件名是这样 MANIFEST-(seqnumber),这个seq会一直增加,只有当 超过了指定的大小以后,MANIFEST会刷新一个新的文件,当新的文件刷新到磁盘(而且文件名更新)以后,老的文件会被删除掉,这里能够认为每一次MANIFEST的更新都表明一次snapshot,其结构描述以下:
MANIFEST = { CURRENT, MANIFEST-<seq-no>* } CURRENT = File pointer to the latest manifest log MANIFEST-<seq no> = Contains snapshot of RocksDB state and subsequent modifications
在RocksDB中任意时间存储引擎的状态都会保存为一个Version(也就是SST的集合),而每次对Version的修改都是一个VersionEdit,而最终这些VersionEdit就是 组成manifest-log文件的内容。
下面就是MANIFEST的log文件的基本构成:
version-edit = Any RocksDB state change version = { version-edit* } manifest-log-file = { version, version-edit* } = { version-edit* }
关于 VersionSet 相关代码分析见参考文档13。
SSTfile 结构以下:
<beginning_of_file> [data block 1] [data block 2] … [data block N] [meta block 1: filter block] [meta block 2: stats block] [meta block 3: compression dictionary block] … [meta block K: future extended block] [metaindex block] [index block] [Footer] <end_of_file>
LevelDB 的 SSTfile 结构以下:
见参考文档12,SSTtable 大体分为几个部分:
block 结构以下图:
record 结构以下图:
Footer 结构以下图:
memtable 中存储了一些 metadata 和 data,data 在 skiplist 中存储。metadata 数据以下(源自参考文档 12):
RocksDB 的 Version 表示一个版本的 metadata,其主要内容是 FileMetaData 指针的二维数组,分层记录了全部的SST文件信息。
FileMetaData 数据结构用来维护一个文件的元信息,包括文件大小,文件编号,最大最小值,引用计数等信息,其中引用计数记录了被不一样的Version引用的个数,保证被引用中的文件不会被删除。
Version中还记录了触发 Compaction 相关的状态信息,这些信息会在读写请求或 Compaction 过程当中被更新。在 CompactMemTable 和 BackgroundCompaction 过程当中会致使新文件的产生和旧文件的删除,每当这个时候都会有一个新的对应的Version生成,并插入 VersionSet 链表头部,LevelDB 用 VersionEdit 来表示这种相邻 Version 的差值。
VersionSet 结构如上图所示,它是一个 Version 构成的双向链表,这些Version按时间顺序前后产生,记录了当时的元信息,链表头指向当前最新的Version,同时维护了每一个Version的引用计数,被引用中的Version不会被删除,其对应的SST文件也所以得以保留,经过这种方式,使得LevelDB能够在一个稳定的快照视图上访问文件。
VersionSet中除了Version的双向链表外还会记录一些如LogNumber,Sequence,下一个SST文件编号的状态信息。
本节内容节选自参考文档 12。
为了不进程崩溃或机器宕机致使的数据丢失,LevelDB 须要将元信息数据持久化到磁盘,承担这个任务的就是 Manifest 文件,每当有新的Version产生都须要更新 Manifest。
新增数据正好对应于VersionEdit内容,也就是说Manifest文件记录的是一组VersionEdit值,在Manifest中的一次增量内容称做一个Block。
Manifest Block 的详细结构如上图所示。
上图最上面的流程显示了恢复元信息的过程,也就是一次应用 VersionEdit 的过程,这个过程会有大量的临时 Version 产生,但这种方法显然太过于耗费资源,LevelDB 引入 VersionSet::Builder 来避免这种中间变量,方法是先将全部的VersoinEdit内容整理到VersionBuilder中,而后一次应用产生最终的Version,详细流程如上图下边流程所示。
数据恢复的详细流程以下:
RocksDB 每次进行更新操做就会把更新内容写入 Manifest 文件,同时它会更新版本号。
版本号是一个 8 字节的证书,每一个 key 更新时,除了新数据被写入数据文件,同时记录下 RocksDB 的版本号。RocksDB 的 Snapshot 数据仅仅是逻辑数据,并无对应的真实存在的物理数据,仅仅对应一下当前 RocksDB 的全局版本号而已,只要 Snapshot 存在,每一个 key 对应版本号的数据在后面的更新、删除、合并时会一并存在,不会被删除,以保证数据一致性。
6.7.1 Checkpoints
Checkpoints 是 RocksDB 提供的一种 snapshot,独立的存在一个单独的不一样于 RocksDB 自身数据目录的目录中,既能够 ReadOnly 模式打开,也能够 Read-Write 模式打开。Checkpoints 被用于全量或者增量 Backup 机制中。
若是 Checkpoints 目录和 RocksDB 数据目录在同一个文件系统上,则 Checkpoints 目录下的 SST 是一个 hard link【SST 文件是 Read-Only的】,而 manifest 和 CURRENT 两个文件则会被拷贝出来。若是 DB 有多个 Column Family,wal 文件也会被复制,其时间范围足以覆盖 Checkpoints 的起始和结束,以保证数据一致性。
若是以 Read-Write 模式打开 Checkpoints 文件,则其中过期的 SST 文件会被删除掉。
RocksDB 提供了 point-of-time 数据备份功能,能够调用 BackupEngine::CreateNewBackup(db, flush_before_backup = false)
接口进行数据备份, 其大体流程以下:
GetLiveFiles()
获取当前的有效文件,如 table files, current, options and manifest file;将 RocksDB 中的全部的 sst/Manifest/配置/CURRENT 等有效文件备份到指定目录;
GetLiveFiles() 接口返回的 SST 文件若是已经被备份过,则这个文件不会被从新复制到目标备份目录,可是 BackupEngine
会对这个文件进行 checksum 校验,若是校验失败则会停止备份过程。
若是 flush_before_backup
为 false,则BackupEngine
会调用 GetSortedWalFiles()
接口把当前有效的 wal 文件也拷贝到备份目录;
从新容许删除文件。
sst 文件只有在 compact 时才会被删除,因此禁止删除就至关于禁止了 compaction。别的 RocksDB 在获取这些备份数据文件后会依据 Manifest 文件重构 LSM 结构的同时,也能恢复出 WAL 文件,进而重构出当时的 memtable 文件。
在进行 Backup 的过程当中,写操做是不会被阻塞的,因此 WAL 文件内容会在 backup 过程当中发生改变。RocksDB 的 flushbeforebackup 选项用来控制在 backup 时是否也拷贝 WAL,其值为 true 则不拷贝。
6.8.1 Backup 编程接口
RocksDB 提供的 Backup 接口使用方法详见 参考文档17。include/rocksdb/utilities/backupable_db.h 主要提供了 BackupEngine
和 BackupEngineReadOnly
,分别用于备份数据和恢复数据。
BackupEngine
备份数据是增量式备份【设置选项 BackupableDBOptions::share_table_files
】,调用 BackupEngine::CreateNewBackup()
接口进行备份后,能够调用接口 BackupEngine::GetBackupInfo()
获取备份文件的信息:ID、timestamp、size、file number 和 metadata【用户自定义数据】。
备份 DB 目录见上图,各个备份文件的 size 是其 private 目录下数据与 shared 目录下数据之和,shared 下面存储的数据是各个备份公共的数据,因此全部备份文件的 size 之和可能大于实际占用的磁盘空间大小。meta 目录下各个文件的格式详见 utilities/backupable/backupable_db.cc,上图中 meta/1
内容以下:
1536821592 # checksum 1 # backup ID 4 # private file number private/1/MANIFEST-000008 crc32 272357318 private/1/OPTIONS-000011 crc32 3039312718 private/1/CURRENT crc32 1581506767 private/1/000009.log crc32 3494278128
Private 目录则包含一些非 SST 文件:options, current, manifest, WALs。若是 Options::share_table_files
为false,则 private 目录会存储 SST 文件。若是 Options::share_table_files
为 true 且 Options::share_files_with_checksum
为 false,shared 目录包含一些 SST 文件,SST 文件命名与原 RocksDB 目录下命名一致,因此在一个备份目录下只能备份一个 RocksDB 实例的数据。
接口 BackupEngine::VerifyBackups()
用于对备份数据进行校验,可是仅仅根据 meta 目录下各个 ID 文件记录的文件 size 与 相应的 private 目录下的文件的 size 是否相等,并不会进行 checksum 校验, 校验 checksum 须要读取数据文件,比较费时。另外须要注意的是,这个接口相应的 BackupEngine
句柄只能由BackupEngine::CreateNewBackup()
建立,也即只能在进行文件备份且句柄未失效前进行数据校验,由于校验时所依据的数据是在备份过程当中产生的。
接口 BackupEngineReadOnly::RestoreDBFromBackup(backup_id, db_dir, wal_dir,restore_options)
用于备份数据恢复,参数 db_dir
和wal_dir
大部分场景下都是同一个目录,但在 参考文档18 所提供的把 RocksDB 当作纯内存数据库的使用场景下, db_dir
在内存虚拟文件系统上,而 wal_dir
则是一个磁盘文件目录。进行数据恢复时,这个接口还会根据 meta 下相应 ID 记录的 备份数据 checksum 对 private 目录下的数据进行校验,发错错误时返回 Status::Corruption
错误。
6.8.2 Backup 性能优化
BackupEngine::Open()
启用时须要进行一些初始化工做,因此它会消耗一些时间。例如须要把本地 RocksDB 数据备份到远端的 HDFS 上,这个过程就可能消耗多个 network round-trip,因此在实际使用中不要频繁建立 BackupEngine
对象。
加快 BackupEngine 对象的方式之一是经过调用 PurgeOldBackups(N)
来删除非必要的备份文件,接口 PurgeOldBackups(N)
自己之意就是只保留最近的 N 个备份,多余的会被删除掉。也能够经过调用 DeleteBackup(id)
接口根据备份 ID 删除某个肯定的备份。
初始化 BackupEngine 对象事后,备份的速度就取决于本地与远端的媒介运行速度了。例如,若是本地媒介是 HDD,在其自身饱和运转以后就算是打开再多的线程也无济于事。若是媒介是一个小的 HDFS 集群,其表现也不会很好。若是本地是 SSD 而远端是一个大的 HDFS 集群,则相较于单线程, 16 个备份线程会被备份时间缩短 2/3。
6.8.3 高级编程接口
BackupEngine::CreateNewBackupWithMetadata()
用于设置 metadata,例如设置你能辨识的备份 ID,metadata 能够经过 BackupEngine::GetBackupInfo()
获取;rocksdb::LoadLatestOptions()
or rocksdb:: LoadOptionsFromFile()
RocksDB 如今也能对 RocksDB 的 options 进行备份,能够经过这两个接口获取相应备份的 Options;BackupableDBOptions::backup_env
用于设置备份目录的 ENV;BackupableDBOptions::backup_dir
用于设置备份文件的根目录;BackupableDBOptions::share_table_files
若是这个选项为 true,则 BackupEngine
会进行增量备份,把全部的 SST 文件存储到 shared/ 子目录,其危险是 SST 文件名字可能相同【在多个 RocksDB 对象共用同一备份目录的场景下】;BackupableDBOptions::share_files_with_checksum
在多个 RocksDB 对象共用同一备份目录的场景下,SST 文件名字可能相同,把这个选项设置为 true 能够处理这个冲突,SST 文件会被 BackupEngine
经过 checksum/size/seqnum 三个参数进行校验;BackupableDBOptions::max_background_operations
这个参数用于设置备份和恢复数据的线程数,在使用分布式文件系统如 HDFS 场景下,这个参数会大大提升备份和恢复的效率;BackupableDBOptions::info_log
用于设置 LOG 对象,能够在备份和恢复数据时进行日志输出;BackupableDBOptions::sync
若是设置为 true,BackupEngine
会调用 fsync
系统接口进行文件数据和 metadata 的数据写入,以防备系统重启或者崩溃时的数据不一致现象,大部分状况下若是为追求性能,这个参数能够设置为 false;BackupableDBOptions::destroy_old_data
若是这个选项为 true,新的 BackupEngine
被建立出来以后备份目录下旧的备份数据会被清空;BackupEngine::CreateNewBackup(db, flush_before_backup = false)
flushbeforebackup 被设置为 true 时,BackupEngine
首先 flush memtable,而后再进行数据复制,而 WAL log 文件不会被复制,由于 flush 时候它会被删掉,若是这个为 false 则相应的 WAL 日志文件也会被复制以保证备份数据与当前 RocksDB 状态一致;官方 wiki 【参考文档 11】提供了一份 FAQ,下面节选一些比较有意义的建议,其余内容请移步官方文档。
DB::SyncWAL()
以前的数据 或者已经被写入 L0 的 memtable 的数据都是安全的;GetIntProperty(cf_handle, “rocksdb.estimate-num-keys")
获取一个 column family 中大概的 key 的个数;GetAggregatedIntProperty(“rocksdb.estimate-num-keys", &num_keys)
获取整个 RocksDB 中大概的 key 的总数,之因此只能获取一个大概数值是由于 RocksDB 的磁盘文件有重复 key,并且 compact 的时候会进行 key 的淘汰,因此没法精确获取;DB::OpenForReadOnly()
对 RocksDB 进行只读访问;options.max_background_flushes
最少为 4;插入数据以前设置关闭自动 compact,把 options.level0_file_num_compaction_trigger/options.level0_slowdown_writes_trigger/options.level0_stop_writes_trigger
三个值放大,数据插入后再启动调用 compact 函数进行 compaction 操做。 若是调用了Options::PrepareForBulkLoad()
,后面三个方法会被自动启用;DBOptions::db_paths/DBOptions::db_log_dir/DBOptions::wal_dir
三个参数分别存储 RocksDB 的数据,这种状况下若是要释放 RocksDB 的数据能够经过 DestroyDB() 这个 API 去执行删除任务;BackupOptions::backup_log_files
或者 flush_before_backup
的值为 true 的时候,若是程序调用 CreateNewBackup()
则 RocksDB 会建立 point-in-time snapshot
,RocksDB进行数据备份的时候不会影响正常的读写逻辑;prefix extractor
;ColumnFamilyOptions::bottommost_compression
使用不一样的压缩的方法;prefix iterating
;rocksdb.estimate-live-data-size
能够估算 RocksDB 使用的磁盘空间;