[LevelDB] 4.Get操做

---恢复内容开始---程序员

LevelDB虽然支持多线程, 但本质上并无使用一些复杂到爆炸的数据结构来达成无锁多写多读, 而是坚持天然朴实的有锁单写多读. 那么是否是只有对时间线产生变更的操做(Put, Compaction etc.)才须要上锁? 不是的. 全部操做几乎都要在某一时间上锁来确保结果是线性的符合预期的. 怎么讲? 用户在t1创建了快照, 那就必定不能获得t2时才写入的数据. 在t1创建快照这件事对数据库来讲没有改变时间线(没有反作用, 不须要锁), 但为了让快照成功创建, 那就要上锁, 不能有两个线程同时创建快照. 因此多线程在不少状况下就是个伪命题, 真正须要的是协程. 反正最后也会用各类锁模拟出顺序时间线, 那还不如event loop呢.数据库

LevelDB算是NoSQL, 但不能说没有ACID. 事实上, 单机数据库作到ACID几乎是没什么成本的. 由于硬盘就一个, 自然是线性的. 既然提到了, 那就再说下, 分布式数据库的CAP,数组

C = Consistency 一致性安全

A = Availability 可用性数据结构

P = Partition tolerance 分区容错性多线程

三者最多取其二, 这个我历来没看过证实论文, 但几乎是不言自明的. 要P就要有副本在不一样的机器上, 那更新一个数据, 就要同步到副本. 这时候只能在C和A之中选一个. 若是选C, 那么必须等全部副本确认同步完成以后, 才能再次提供服务, 系统就锁死(不可用)了. 若是选A, 那么就存在着副本版本不一样步的问题.并发

------分布式

回到源码上来,  1110-1130,函数

 1 Status DBImpl::Get(const ReadOptions& options,
 2                    const Slice& key,
 3                    std::string* value) {
 4   Status s;
 5   MutexLock l(&mutex_);
 6   SequenceNumber snapshot;
 7   if (options.snapshot != NULL) {
 8     snapshot = reinterpret_cast<const SnapshotImpl*>(options.snapshot)->number_;
 9   } else {
10     snapshot = versions_->LastSequence();
11   }
12 
13   MemTable* mem = mem_;
14   MemTable* imm = imm_;
15   Version* current = versions_->current();
16   mem->Ref();
17   if (imm != NULL) imm->Ref();
18   current->Ref();
19 
20   bool have_stat_update = false;
21   Version::GetStats stats;

以上代码在锁的保护下完成了两件事,oop

  1. 生成一个SequenceNumber做为标记, 后续无论线程会不会被切出去, 结果都要至关于在这个时间点瞬间完成
  2. memtable, immemtable, Version, 因为采用了引用计数, 这里Ref()一下

快照创建完了, 接下来的操做只会有单纯的读, 能够把锁暂时释放, 1132-1146,

 1   // Unlock while reading from files and memtables
 2   {
 3     mutex_.Unlock();
 4     // First look in the memtable, then in the immutable memtable (if any).
 5     LookupKey lkey(key, snapshot); // 黑科技
 6     if (mem->Get(lkey, value, &s)) {
 7       // Done
 8     } else if (imm != NULL && imm->Get(lkey, value, &s)) {
 9       // Done
10     } else {
11       s = current->Get(options, lkey, value, &stats);
12       have_stat_update = true;
13     }
14     mutex_.Lock();
15   }

查询先找memtable, 再immemtable, 最后是SSTable, 这都很正常.

请注意我标注了黑科技那行的"LookupKey", 工程师用了些特别的技巧. 这个类主要的功能是把输入的key转换成用于查询的key. 好比key是"Sherry", 实际在数据库中的表达可能会是"6Sherry", 6是长度. 这样比对key是否相等时速度会更快.

 121-138,

 1 LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
 2   size_t usize = user_key.size();
 3   size_t needed = usize + 13;  // A conservative estimate
 4   char* dst;
 5   if (needed <= sizeof(space_)) {
 6     dst = space_;
 7   } else {
 8     dst = new char[needed];
 9   }
10   start_ = dst;
11   dst = EncodeVarint32(dst, usize + 8); // 黑科技
12   kstart_ = dst;
13   memcpy(dst, user_key.data(), usize);
14   dst += usize;
15   EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
16   dst += 8;
17   end_ = dst;
18 }

LookupKey格式 = 长度 + key + SequenceNumber + type

tricks:

  1. 在栈上分配一个200长度的数组, 若是运行时发现长度不够用再从堆上new一个, 能够极大避免内存分配
  2. 黑科技函数"EncodeVarint32", 通常key的长度不可能用满32bit. 大量很短的Key却要用32bit来描述长度无疑是很浪费的. 这个函数让小数值用更少的空间, 代价是最糟要多花一字节(8bit)

快来欣赏一下,  47-73,

 1 char* EncodeVarint32(char* dst, uint32_t v) {
 2   // Operate on characters as unsigneds
 3   unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
 4   static const int B = 128;
 5   if (v < (1<<7)) {
 6     *(ptr++) = v;
 7   } else if (v < (1<<14)) {
 8     *(ptr++) = v | B;
 9     *(ptr++) = v>>7;
10   } else if (v < (1<<21)) {
11     *(ptr++) = v | B;
12     *(ptr++) = (v>>7) | B;
13     *(ptr++) = v>>14;
14   } else if (v < (1<<28)) {
15     *(ptr++) = v | B;
16     *(ptr++) = (v>>7) | B;
17     *(ptr++) = (v>>14) | B;
18     *(ptr++) = v>>21;
19   } else { // 最多用5字节
20     *(ptr++) = v | B;
21     *(ptr++) = (v>>7) | B;
22     *(ptr++) = (v>>14) | B;
23     *(ptr++) = (v>>21) | B;
24     *(ptr++) = v>>28;
25   }
26   return reinterpret_cast<char*>(ptr);
27 }       

这篇分析如何在SSTable中找到KV, 工程师一样花了很多心思去优化.

在Get刚开始的时候, 线程就在锁的保护下取得了当前Version的指针. 每一个Version都是只读的. 大脉络上, 只要遍历那个Version全部跟key有关的SSTable文件就能获得value.

判断是否与key相关的代码在 349-390,

 1  std::vector<FileMetaData*> tmp;
 2   FileMetaData* tmp2;
 3   for (int level = 0; level < config::kNumLevels; level++) {
 4     size_t num_files = files_[level].size();
 5     if (num_files == 0) continue;
 6 
 7     // Get the list of files to search in this level
 8     FileMetaData* const* files = &files_[level][0]; // 不用iterator
 9     if (level == 0) {
10       // Level-0 files may overlap each other.  Find all files that
11       // overlap user_key and process them in order from newest to oldest.
12       tmp.reserve(num_files);
13       for (uint32_t i = 0; i < num_files; i++) {
14         FileMetaData* f = files[i];
15         if (ucmp->Compare(user_key, f->smallest.user_key()) >= 0 &&
16             ucmp->Compare(user_key, f->largest.user_key()) <= 0) {
17           tmp.push_back(f);
18         }
19       }
20       if (tmp.empty()) continue;
21 
22       std::sort(tmp.begin(), tmp.end(), NewestFirst);
23       files = &tmp[0]; // 注意
24       num_files = tmp.size();
25     } else {
26       // Binary search to find earliest index whose largest key >= ikey.
27       uint32_t index = FindFile(vset_->icmp_, files_[level], ikey);
28       if (index >= num_files) {
29         files = NULL;
30         num_files = 0;
31       } else {
32         tmp2 = files[index];
33         if (ucmp->Compare(user_key, tmp2->smallest.user_key()) < 0) {
34           // All of "tmp2" is past any data for user_key
35           files = NULL;
36           num_files = 0;
37         } else {
38           files = &tmp2; // 注意
39           num_files = 1;
40         }
41       }
42     }

除了level0, 别的level最多只有一张可能包含key的SSTable. 但工程师为了统一这两种状况, 用了一个很黑的写法, 不知道是否是对全部版本的STL都兼容.

std::vector<FileMetaData*> tmp;
FileMetaData* const* files = &tmp[0];
那么能不能永远安全地用这个files去访问vector容器内的数据呢? 好比, files[0], files[1].
假如vector内部的实现不是连续内存, 这就糟了, 但标准好像又有规定vector的复杂度啊?

------

真正查询SSTable的代码在392-409,

 1     for (uint32_t i = 0; i < num_files; ++i) {
 2       if (last_file_read != NULL && stats->seek_file == NULL) {
 3         // We have had more than one seek for this read.  Charge the 1st file.
 4         stats->seek_file = last_file_read;
 5         stats->seek_file_level = last_file_read_level;
 6       }
 7 
 8       FileMetaData* f = files[i];
 9       last_file_read = f;
10       last_file_read_level = level;
11 
12       Saver saver;
13       saver.state = kNotFound;
14       saver.ucmp = ucmp;
15       saver.user_key = user_key;
16       saver.value = value;
17       s = vset_->table_cache_->Get(options, f->number, f->file_size,
18                                    ikey, &saver, SaveValue);

LevelDB毕竟完成年代比较久远, 除了之前吐槽的异常和智能指针外, 没有lambda也让我这种Python, JS背景的人看调用看到瞎眼... 各类回调(SaveValue)根本找不到在哪里. 不过这些写法说是笨笨的, 可读性仍是很高的. 我估摸着对代码有追求的程序员写汇编应该也会写得很好看吧.

------

对SSTable的查询就是对table_cache_的查询, 这个cache是不可取消的, 解决了什么问题呢?

LevelDB的数据库"文件"是一个文件夹, 里面包含大量的文件. 这是把复杂度甩锅给操做系统的作法, 但不少系统资源是有限的. 好比, file handle(文件句柄). 一个程序若是开了1W个file handle会浪费大量资源. 这里作个LRU cache, 只有经常使用的SSTable才会开一个活跃的file handle.

另外就是索引的问题. LSMT是没有主索引的, 只有在各个SSTable内有微缩版索引. 因此, 最最优的状况下也须要2次硬盘读写. 第一张SSTable就存着key, 先读微型索引, 而后二分法找到具体位置, 再读value.

TableCache把热点SSTable的微型索引预先放在内存里, 这样只要1次硬盘读取就能取到key. 这个优化对于LSMT的数据库来讲尤其重要, 由于极可能会不止查询一张SSTable. 状况会劣化很是快.

总结, TableCache既承担管理资源(file handle)的做用, 又加速索引的读取.

------

TableCache的实现有点出人意料, LRU, hash, 这都很日常. 但让你不敢相信的是工程师对这个cache作了sharding... 我当时一直觉得是sharing而不是sharding... 看了半天都不知道哪里share了.

 361-369,

virtual Handle* Insert(const Slice& key, void* value, size_t charge, void (*deleter)(const Slice& key, void* value)) { const uint32_t hash = HashSlice(key); return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter); } virtual Handle* Lookup(const Slice& key) { const uint32_t hash = HashSlice(key); return shard_[Shard(hash)].Lookup(key, hash); } 

就是这个hash table作了两遍hash, 先把key分片一遍, 而后再扔给真正的hash table cache(有锁)去lookup.

这么作的逻辑是能够减小锁的使用率和提高并发, 我当时以为这个太取巧了.

因而获得了万能的数据结构无锁改造法(大雾). 开了10个线程就把key sharding到10个一样的数据结构上面. 从统计上来讲, 这个数据结构就多线程无锁了啊. ( ̄△ ̄;)

------

cache会返回一个Table*(SSTable的内存对应),  105-119,

Status TableCache::Get(const ReadOptions& options, uint64_t file_number, uint64_t file_size, const Slice& k, void* arg, void (*saver)(void*, const Slice&, const Slice&)) { Cache::Handle* handle = NULL; // Cache::Handle* 至关于 void*, Cache::Handle什么都没定义 Status s = FindTable(file_number, file_size, &handle); if (s.ok()) { Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table; s = t->InternalGet(options, k, arg, saver); cache_->Release(handle); } return s; } 

而后就调用InternalGet(其中用到了Bloom Filter, BlockCache)获得value啦. SSTable的整个存储格式也考虑了不少细节, 压缩了数据. 这个等我分析到Compaction的时候细说.

 

 

 

 

 

---恢复内容结束---

相关文章
相关标签/搜索