LevelDB源码解读


提供的功能

leveldb index

前面打开关闭数据库的方式略过

read and write

std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);
  1. 写流程:
    1. 找到合适的memtable(skiplist)并写入数据,如果memtable写完并且immutableTable还未完成持久化,或者L0文件太多,则等待。如果memtable满了,则换一个memtable,并触发compaction
    2. 写日志,下刷日志
    3. 写memtable
  2. 读流程:
    1. 首先查找memtable,再查找immutable memtable
    2. 如果没有,查找下面所有持久化的levelVersion::Get,逐层向下搜索。搜索方法的策略是并不是遍历每一个sstable,而是先看需要查找的key是否落在sstable的范围内(每个sstable记录begin/end key),如果落在,对sstable搜索。
  3. 后台线程也会按需做compaction。通过MaybeScheduleCompaction()函数判断。

注意,日志只是为了保证内存中的数据不丢失(memtable和immutable),内存数据下刷完成之后,日志就可以删掉了

Group commit

为了防止写日志在高并发下成为瓶颈,LevelDB将不同线程同时产生的日志数据聚合在一起写入文件并进行一次fsync(),而非对每个线程写的数据进行单独fsync()。这个做法称为group commit。

具体的commit见added group commit

具体的实现上,通过在构造writer的时候控制mutex,只有一个线程会顺利进行BuildBatchGroup,而其他没有拿到mutex的线程会在随后将日志操作写进队列。重复这个过程,最终只有排在队首的线程会真正flush日志,其他的线程都在等待。

sequence number

sequence number 是一个由VersionSet直接持有的全局的编号,每次写入(注意批量写入时sequence number是相同的),就会递增。key的排序以及snapshot都要依赖它。

当我们进行Get操作时,我们只需要找到目标key,同时其sequence number 小于等于sequence number

  • 普通的读取,sepcific sequence number =last sequence number
  • snapshot读取,sepcific sequenc number =snapshot sequence number

delete

delete仍然是通过write batch的形式向存储引擎下发的。只不过ValueType是特殊的kTypeDeletion。但是delete在compact的时候需要特殊对待

Atomic Updates

将一系列的操作作为一个batch,提供原子性。这里猜测做法是在WAL中增加一些特殊的LogEntry如Transaction begin/end等。

#include "leveldb/write_batch.h"
...
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
  leveldb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db->Write(leveldb::WriteOptions(), &batch);
}

同步写Synchronous Writes

leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);

异步写比同步写块上千倍,但是默认的异步写有可能导致丢数据(系统crash的时候异步写还没完成)

Note that a crash of just the writing process (i.e., not a reboot) will not cause any loss since even when sync is false, an update is pushed from the process memory into the operating system before it is considered done.

如果系统希望避免丢数据,可以用以下几种方式实现:

  1. 只要上层应用知道当前写的数据是什么,那么就算crash了也没关系,重试就行了
  2. 另外可以每隔N个async write写一个sync write作为checkpoint,如果crash了,从上一个checkpoint处开始写就行了。
  3. 另外可以用atomic write提供的batch写。

遍历 iteration

scan table

leveldb::Iterator* it = db->NewIterator(leveldb::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;

范围搜索

for (it->Seek(start);
   it->Valid() && it->key().ToString() < limit;
   it->Next()) {
  ...
}

倒序,注意,这里和正序的时间复杂度不一致。

for (it->SeekToLast(); it->Valid(); it->Prev()) {
  ...
}

快照 Snapshots

快照只读,并且读时可以不用加锁,我们使用DB:GetSnapshot()创建快照:

leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
leveldb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);

Slice

Slice用来存放levelDB存储的数据类型,它存放数据的length和指针。使用Slice是一种比std::string更为轻量化的方法。LevelDB不允许返回末尾是null的C-Style字符串,LevelDB允许字符串含有\0。另外slice持有的是指针,而不是对象,所以要注意指针指向的对象的生命周期

class LEVELDB_EXPORT Slice {
 public:
  // 各种构造函数,成员函数...

 private:
  const char* data_;
  size_t size_;
};
leveldb::Slice s1 = "hello";  // null-termicated C-style string
std::string str("world");  // C++ string
leveldb::Slice s2 = str;

// Slice输出为std::string
std::string str = s1.ToString();
assert(str == std::string("hello"));

比较器 Comparators

levelDB可以针对自定义的Slice数据类型使用自定义的比较器。只要在打开数据的时候指定比较器options.comparator = &cmp;就行了。该比较器的名字会在数据库初始化的时候持久化进数据库,如果后面使用不同命的比较器打开数据库,会失败。

concurrency保证

每个database(文件系统的一个对应目录)只能同一时刻被一个进程打开,level启动的时候会获取一个文件锁。在一个进程内,DB对象leveldb::DB可以被多个线程并发访问。由levelDB负责线程间同步。当然如果要做事务的话(writebatch)可能要外部进行并发控制(加锁)。

compaction

之前已经谈到了memtable、immutable、各个level。除了l0中每个run是可以重叠的(其实l0就是一个个的memtable),其他level都是只有一个run,被分配成很多2M的小文件。

做compaction的时候,只有l0层是将所有文件一起合并,其它层都是以2M为单位进行合并,写入合并的文件,丢弃之前的文件。

LevelDB Compaction分三种:

  • minor compaction
    • 将immutable memtable dump成sstable
  • major compaction
    • 如果某个level文件过多或过大、seek的次数过多都会引发Major compaction,每一层都有一个target size,通过比较算出一个score,然后对score较高的某个levelsstable进行major compaction
    • 通过PickLevelForMemTableOutput判断产生文件位于哪一层
  • Manual Compaction
    • 通过接口DBImpl::CompactRange(const Slice begin, const Slice end)接口调用
    • 可以看到,如果某层一直没有进行Major compaction,可能会造成compaction在高层的数据一直无法和低层的delete进行merge,导致数据无法被回收。这是可以主动调用Manual Compaction,手动对某个范围进行compaction,也就是说,通过一次全量的compaction,消除delete特别是range delete带来的负面影响
    • 数据无法被回收只是影响的一方面,在数据库应用中,delete会导致统计信息不准,影响sql优化器的决策

LevelDB的性能优化参数

Block size

levelDB默认最小存储单元Block默认是4K(压缩前),如果上层应用经常做全表扫描的话,可以增大Block size。如果上层应用经常做各种小的随机读,可以缩小Block size。实践表明Block大于几M或者小于1K是没有任何益处。有一点要注意的是,Blocksize越大Compression越高效。

Compression

每个block在持久化之前默认都需要被压缩。当然用户也可以显式指定不需要压缩

leveldb::Options options;
options.compression = leveldb::kNoCompression;
... leveldb::DB::Open(options, name, ...) ....

Cache

LevelDB可以指定Cache的大小,Cache中存放的是已经持久化的数据,注意这个cache不是文件系统的buffer cache。buffer cache存储的是已经被压缩的数据,levelDB中的cache存储的是解压后的数据。

如果上层应用进行一系列的读例如table scan,我们希望将cache disable掉,以防上面的cache全被刷掉,下面的代码,就是在当前的遍历中disable掉cache。

leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
  ...
}

键分布 Key Layout

LevelDB中的键是顺序分布的,所以可以将经常访问的key放一起,不长访问的key放一起。

Filters

因为LevelDB使用了LSM tree,因此一个简单的Get()操作可能会设计多个对磁盘的read,为了优化读性能,LevelDB使用了布隆过滤器(Bloom Filter)判断某个键是否存在。

leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy;

实际应用中,我们需要对过滤器的大小和过滤器的精度进行取舍。这部分的代码放在leveldb/filter_policy.h

校验和 Checksum

LevelDB使用Checksum校验持久化数据,ReadOptions::verify_checksums将对每一个read都做校验。Options::paranoid_checks将在数据库打开时校验整个数据库的Checksum

如果发现数据损坏,可以使用leveldb::RepairDB来修复数据。

Approximate Sizes

使用GetApproximateSizes用来获取大概占用的空间

leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
db->GetApproximateSizes(ranges, 2, sizes);

Environment

LevelDB可以使用用户自定义的环境参数、接口函数,例如,用户可以在磁盘IO路径上加入一些delay

class SlowEnv : public leveldb::Env {
  ... implementation of the Env interface ...
};

SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);

porting

LevelDB可以引入不同的系统实现,例如:mutex、condition variable等。从而在不同的系统上实现LevelDB。

参考链接

  1. [leveldb] 3.put/delete操作
  2. LevelDB设计与实现 - 基础篇
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值