Rocksdb 基础操作
并发
一个数据库可能同时只能被一个进程打开。RocksDB的实现方式是,从操作系统那里申请一个锁,以此来阻止错误的写操作。
在单进程里面,同一个rocksdb::DB对象可以被多个同步线程共享。举个例子,不同的线程可以同时对同一个数据库调用写操作,迭代遍历操作或者Get操作,而且不需要使用额外的同步锁(rocksdb的实现会自动进行同步)。
然而其他对象(比如迭代器,WriteBatch)需要额外的同步机制保证线程同步。如果两个线程共同使用这些对象,他们必须使用自己的锁协议保证访问的同步。更多的细节会在公共头文件给出。
迭代器
下面的例子展示如何打印一个数据库里的所有键值对(key,value)。
rocksdb::Iterator* it = db->NewIterator(rocksdb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
assert(it->status().ok()); // Check for any errors found during the scan
delete it;
下面的例子展示如何处理从start到limit左闭右开区间[start, limt)范围内的键值:
The following variation shows how to process just the keys in the range [start, limit):
for (it->Seek(start);
it->Valid() && it->key().ToString() < limit;
it->Next()) {
...
}
assert(it->status().ok()); // Check for any errors found during the scan
快照
快照在整个kv存储之上提供一个一致的只读视图。ReadOptions::snapshot不是NULL的时候意味着这个读操作应该在某个特定的数据库状态版本进行。
如果ReadOptions::snapshot是NULL,读操作隐式地认为使用当前数据库状态进行读操作。
快照通过DB::GetSnapshot方法获得:
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 ...
// the former updates will not be seen
delete iter;
db->ReleaseSnapshot(options.snapshot);
注意,当一个快照不再需要了,他应该通过DB::ReleaseSnapshot接口释放。这样才能让数据库的实现摆脱那些只为了快照保留下来的数据。
Slice
上面调用的it->key()和it->value()的返回值类型是rocksdb::Slice类型。Slice是一个简单的结构体,他有一个长度字段和一个指针指向一个外部的字节数组。相比返回一个std::string类型,返回一个Slice是一个开销更低的选项,因为我们不必另外去拷贝那些可能会很大的键值对。(Slice默认是浅拷贝,只拷贝指针,而string是深拷贝,会拷贝对应的数据)另外,rocksdb的方法不会返回以null结束的c风格字符串,因为rocksdb的键值都是允许使用’\0’字符的。
使用Slice的时候要小心,因为需要由调用者来保证外部的字节数组在Slice使用期间存活。比如,下面这个代码就是有bug的:
rocksdb::Slice slice;
if (...) {
std::string str = ...;
slice = str;
}
Use(slice);// str is not exist
当if声明结束的时候,str会被析构,然后slice存储的数据就消失了。
比较器
前面的例子使用默认的排序函数对键值进行排序,也就是使用字典顺序排列。你也可以在打开数据库的时候使用自定义的比较器。例如,假如每个数据库的键值都是两个数字,然后我们应该按第一个数字排序,如果第一个数字相同,按照第二个数字排序。首先,定义一个合适的rocksdb::Comparator子类,实现下面的规则:
class TwoPartComparator : public rocksdb::Comparator {
public:
int Compare(const rocksdb::Slice& a, const rocksdb::Slice& b) const {
int a1, a2, b1, b2;
ParseKey(a, &a1, &a2);
ParseKey(b, &b1, &b2);
if (a1 < b1) return -1;
if (a1 > b1) return +1;
if (a2 < b2) return -1;
if (a2 > b2) return +1;
return 0;
}
// impliment the following methods as well:
const char* Name() const { return "TwoPartComparator"; }
void FindShortestSeparator(std::string*, const rocksdb::Slice&) const { }
void FindShortSuccessor(std::string*) const { }
};
//现在,使用自定义的比较器打开数据库
TwoPartComparator cmp;
rocksdb::DB* db;
rocksdb::Options options;
options.create_if_missing = true;
options.comparator = &cmp;
rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db);
...
backup以及checkpoint
备份允许用户创建周期性的增量备份到远程文件系统(想想HDFS和S3),然后从他们中的任意一个恢复。
检查点提供一种能力,为线上的RocksDB生成一个快照到一个独立的目录。文件通过硬链接(如果可以的话),而不是拷贝生成,所以这个是相对轻量的一个操作
I/O
默认RocksDB的IO会使用操作系统的页缓存。通过设置 速度限制器可以限制rocksdb的写文件操作速度,给读IO留下空间。
用户也可以选择直接跳过页缓存,使用DIO
块大小
rocksdb将一组连续的键打包到一个块,这个块就是跟持久存储的交换单位。默认的块大小是接近4096byte(压缩前)。一些经常需要做区间扫描的程序,可能希望增加这个大小。对于一些大量点查询的应用,如果确实能看到性能提升,会希望使用一个更小的值。使用一个小于1KB的块大小一般不会有特别多的好处,使用大于几个MB同理。主意,压缩对于越大的块效率越高。使用Options::block_size修改块大小
压缩
每个块都会在写入持久化存储前进行压缩。压缩是默认开启的,因为默认的压缩算法非常快,对于不可压缩的数据,则被自动关闭。在非常罕见的情况下,应用会希望彻底关闭压缩,除非你的压测显示这确实带来了好处,否则不要这么做:
rocksdb::Options options;
options.compression = rocksdb::kNoCompression;
缓存
数据库的数据会被存储到文件系统的一系列文件里,每个文件存储一部分压缩好的块。如果 options.block_cache != NULL,他会被用于缓存最常用的解压后的块的内容。我们使用操作系统来缓存原始的,压缩的数据。文件系统缓存扮演着压缩数据缓存的角色。
#include "rocksdb/cache.h"
rocksdb::BlockBasedTableOptions table_options;
table_options.block_cache = rocksdb::NewLRUCache(100 * 1048576); // 100MB uncompressed cache
rocksdb::Options options;
options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options));
rocksdb::DB* db;
rocksdb::DB::Open(options, name, &db);
... use the db ...
delete db
执行批量读取的时候,应用会希望关闭缓存,这样批量读区就不会污染已经缓存的数据。一个针对迭代器的选项可以做到这个:
rocksdb::ReadOptions options;
options.fill_cache = false; // read data not fill in LRU cache
rocksdb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}
键分布
注意,缓存与磁盘交换数据的单位是块。连续的键(根据数据库的排序)通常被放在同一个块。所以应用也可以通过把常常一起使用的键放在一起,然后把另一些不常用的放在另一个命名空间,以此提高性能。
例如,加入我们基于rocksdb开发一个简单的文件系统。每个节点的类型可能这样存储:
cf1: filename -> stat{permission-bits, length, list of file_block_ids}
cf2: file_block_id -> data
我们也可以给filename这个键使用一个前缀(例如’/’),然后给file_block_id使用另一个前缀(例如’0’),这样,扫描元数据的时候就不用关心大量的文件内容信息了。
Filter
由于Rocksdb在硬盘的数据组织方式,一个Get请求可能会导致多个磁盘读请求。这个时候,FilterPolicy机制就可以用来非常可观地减少磁盘读。
rocksdb::Options options;
rocksdb::BlockBasedTableOptions bbto;
bbto.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10));
options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(bbto));
rocksdb::DB* db;
rocksdb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy
这段代码需要对数据库使用一个基于 BloomFilter 的过滤策略。基于Bloom Filter的过滤器会在内存保留一部分键的内容(这里是10bit,因为我们传递给NewBloomFilter的参数就是这个)。
这个过滤器可以在Get请求的时候减少大概100倍不必要的磁盘读。增大每个键的bit数会导致更多的削减,但是回来带更多的内存使用。我们推荐那些数据没法全部存储在内存,但是有大量随机读的应用使用Filter策略。
如果你使用自定义的比较器,你需要保证你的过滤策略跟你的比较器是兼容的。例如,如果有一个比较器,比较键的时候,不关心末尾的空格。那么,NewBloomFilter就不能给这种比较器使用了。作为替代,应用应该提供一个自定义的过滤策略,忽略掉这些末尾的空格。
class CustomFilterPolicy : public rocksdb::FilterPolicy {
private:
FilterPolicy* builtin_policy_;
public:
CustomFilterPolicy() : builtin_policy_(NewBloomFilter(10, false)) { }
~CustomFilterPolicy() { delete builtin_policy_; }
const char* Name() const { return "IgnoreTrailingSpacesFilter"; }
void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// Use builtin bloom filter code after removing trailing spaces
std::vector<Slice> trimmed(n);
for (int i = 0; i < n; i++) {
trimmed[i] = RemoveTrailingSpaces(keys[i]);
}
return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
}
bool KeyMayMatch(const Slice& key, const Slice& filter) const {
// Use builtin bloom filter code after removing trailing spaces
return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter);
}
};
上面的应用提供一个不是使用bloom filter的过滤策略,而是使用其他的策略来提取一个键值集合的值。参考rocksdb/filter_policy.h
估算大小
GetApproximateSizes方法可以用于获得一个或多个键占用的文件系统的空间的大概值。
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)占用的文件系统大概的大小,然后sizes[1]则会填写成键空间[x..z)的。
环境
所有rocksdb实现的,发起的文件操作(以及其他系统调用),都是通过一个rocksdb::Env对象来实现的。一些熟悉原理的客户可能希望使用自己的Env实现来获得更好的控制。例如,一个应用可能会人为制造一些文件IO的延迟,来限制rocksdb对该系统上的其它活动的影响。
class SlowEnv : public rocksdb::Env {
.. implementation of the Env interface ...
};
SlowEnv env;
rocksdb::Options options;
options.env = &env;
Status s = rocksdb::DB::Open(options, ...);
可管理性
为了更有效地调优你的应用, 如果能获得一些统计数据,总是很有用的。你可以通过设置Options::table_properties_collectors或者Options::statistics来收集统计信息。其他信息,可以参考 rocksdb/table_properties.h和 rocksdb/statistics.h。这个不会给你的系统带来太多的负担,我们推荐你把他们导出到其它监控系统。参考 统计, 你也可以使用上下文及IO状态剖析对单一请求进行剖析。