[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的時候細說.

 

 

 

 

 

---恢復內容結束---

相關文章
相關標籤/搜索