LevelDB Cache實現機制分析


     幾天前淘寶量子恆道在博客上分析了HBase的Cache機制,本篇文章,結合LevelDB 1.7.0版本的源碼,分析下LevelDB的Cache機制。

  • 概述

     LevelDB是Google開源的持久化KV單機存儲引擎,據稱是HBase的鼻祖Bigtable的重要組件tablet的開源實現。針對存儲面對的普遍隨機IO問題,LevelDB采用merge-dump的方式,將邏輯場景的隨機寫請求轉換成順序寫log和寫memtable的操作,由后台線程根據策略將memtable持久化成分層的sstable。針對讀請求,LevelDB會首先查找內存中的memtable和imm(不可變的memtable),然后逐層查找sstable。

     為了加快查找速度,LevelDB在內存中采用Cache的方式,在sstable中采用bloom filter的方式,盡最大可能減少隨機讀操作。

     LevelDB的Cache分為兩種,分別是table cache和block cache。table cache緩存的是sstable的索引數據,類似於文件系統中對inode的緩存;block cache是緩存的block數據,block是sstable文件內組織數據的單位,也是從持久化存儲中讀取和寫入的單位;由於sstable是按照key有序分布的,因此一個block內的數據也是按照key緊鄰排布的(有序依照使用者傳入的比較函數,默認按照字典序),類似於Linux中的page cache。

     block默認大小為4k,由LevelDB調用open函數時傳入的options.block_size參數指定;LevelDB的代碼中限制的block最小大小為1k,最大大小為4M。對於頻繁做scan操作的應用,可適當調大此參數,對大量小value隨機讀取的應用,也可嘗試調小該參數;

     block cache默認實現是一個8M大小的LRU cache,為了減少鎖開銷,該LRU cache還分成了16個shard。此參數由options.block_cache設定,即可改變緩存大小,也可根據自己的應用需求,提供新的緩存策略。注意,此處的大小是未壓縮的block大小。針對大塊文件的讀寫遍歷等需求,為了避免讀入的塊把之前的熱數據都淘汰掉,可以在ReadOptions里設置哪些讀取不需要進cache,如以下代碼所示:

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

     table cache默認大小是1000,注意此處緩存的是1000個sstable文件的索引信息,而不是1000個字節。table cache的大小由options.max_open_files確定,其最小值為20-10,最大值為50000-10。

  • 源碼分析

     1.默認的Cache是一個分Shard的LRU Cache,代碼片段如下:

     leveldb-1.7.0/util/cache.cc

136 class LRUCache {
137  public:
138   LRUCache();
139   ~LRUCache();
140 
141   // Separate from constructor so caller can easily make an array of LRUCache
142   void SetCapacity(size_t capacity) { capacity_ = capacity; }
143 
144   // Like Cache methods, but with an extra "hash" parameter.
145   Cache::Handle* Insert(const Slice& key, uint32_t hash,
146                         void* value, size_t charge,
147                         void (*deleter)(const Slice& key, void* value));
148   Cache::Handle* Lookup(const Slice& key, uint32_t hash);
149   void Release(Cache::Handle* handle);
150   void Erase(const Slice& key, uint32_t hash);
151 
152  private:
153   void LRU_Remove(LRUHandle* e);
154   void LRU_Append(LRUHandle* e);
155   void Unref(LRUHandle* e);
156 
157   // Initialized before use.
158   size_t capacity_;
159 
160   // mutex_ protects the following state.
161   port::Mutex mutex_;
162   size_t usage_;
163   uint64_t last_id_;
164 
165   // Dummy head of LRU list.
166   // lru.prev is newest entry, lru.next is oldest entry.
167   LRUHandle lru_;
168 
169   HandleTable table_;
170 };

     1) capacity_是Cache大小,其單位可以自行指定(如table cache,一個sstable文件的索引信息是一個單位,而block cache,一個byte是一個單位);

     2)lru_是一個雙向鏈表,如注釋說明,lru_.prev是最新被訪問的條目,lru_.next是最老被訪問的條目。在訪問cache中的一個數據時,會順次執行LRU_Remove和LRU_Append函數,將條目移到lru_.prev的位置;

     3)table_是LevelDB自己實現的一個hash_map,其實現也在cache.cc文件中,據作者說,在特定的編譯環境下性能更優,如與g++ 4.4.3內置的hashtable相比,隨機讀性能可以提升5%;

     4)insert操作會根據capacity_的大小,順着lru_.next講老的條目Release掉;

     2. block 的讀取邏輯,代碼片段如下:

     leveldb-1.7.0/table/table.cc

154 Iterator* Table::BlockReader(void* arg,
155                              const ReadOptions& options,
156                              const Slice& index_value) {
157   Table* table = reinterpret_cast<Table*>(arg);
158   Cache* block_cache = table->rep_->options.block_cache;
159   Block* block = NULL;
160   Cache::Handle* cache_handle = NULL;
161 
162   BlockHandle handle;
          ...... 
168   if (s.ok()) {
169     BlockContents contents;
170     if (block_cache != NULL) {
          ......
175       cache_handle = block_cache->Lookup(key);
176       if (cache_handle != NULL) {
177         block = reinterpret_cast<Block*>(block_cache->Value(cache_handle));
178       } else {
179         s = ReadBlock(table->rep_->file, options, handle, &contents);
180         if (s.ok()) {
181           block = new Block(contents);
182           if (contents.cachable && options.fill_cache) {
183             cache_handle = block_cache->Insert(
184                 key, block, block->size(), &DeleteCachedBlock);
185           }
186         }
187       }
188     } else {
189       s = ReadBlock(table->rep_->file, options, handle, &contents);
190       if (s.ok()) {
191         block = new Block(contents);
192       }
193     }
194   }
195 
196   Iterator* iter;
197   if (block != NULL) {
198     iter = block->NewIterator(table->rep_->options.comparator);
199     if (cache_handle == NULL) {
200       iter->RegisterCleanup(&DeleteBlock, block, NULL);
201     } else {
202       iter->RegisterCleanup(&ReleaseBlock, block_cache, cache_handle);
203     }
204   } else {
205     iter = NewErrorIterator(s);
206   }
207   return iter;
208 }

     1) 首先從block cache中查找block,如果找不到,直接從持久化存儲中讀取,獲取到一個新的block,插入block cache;

     2) 對於查到的block,返回對應的迭代器(LevelDB中,所有的get\merge操作均是抽象成iterator實現的);

     3)如果仔細讀代碼,iter->RegisterCleanup函數實現會有點繞,這個函數在iter析構時被調用,執行注冊的ReleaseBlock,ReleaseBlock調用cache_handle的Unref方法,對cache中緩存的block減少一個引用計數;cache執行insert函數時,會給所有的LRUHandle的引用計數設成2,其中1用於LRUCache自身,在執行cache的Release操作時被Unref,從而真正釋放。

     3.table cache的讀取邏輯,代碼片段如下:

     leveldb-1.7.0/db/table_cache.cc 

 45 Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
 46                              Cache::Handle** handle) {
 47   Status s;
 48   char buf[sizeof(file_number)];
 49   EncodeFixed64(buf, file_number);
 50   Slice key(buf, sizeof(buf));
 51   *handle = cache_->Lookup(key);
 52   if (*handle == NULL) {
 53     std::string fname = TableFileName(dbname_, file_number);
 54     RandomAccessFile* file = NULL;
 55     Table* table = NULL;
 56     s = env_->NewRandomAccessFile(fname, &file);
 57     if (s.ok()) {
 58       s = Table::Open(*options_, file, file_size, &table);
 59     }
 60 
 61     if (!s.ok()) {
 62       assert(table == NULL);
 63       delete file;
 64       // We do not cache error results so that if the error is transient,
 65       // or somebody repairs the file, we recover automatically.
 66     } else {
 67       TableAndFile* tf = new TableAndFile;
 68       tf->file = file;
 69       tf->table = table;
 70       *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
 71     }
 72   }
 73   return s;
 74 }

      和block cache類似,首先查找cache,如果找不到,直接從硬盤中讀取。注意代碼70行Insert函數的第3個參數,1表示每個sstable的索引信息在cache總占用1個單位的capacity_。其他內容不再贅述。

  • 總結

     LevelDB是Jeff Dean, Sanjay Ghemawat的作品,實在是值得大家仔細品讀。Cache機制非常簡單,相信大家通過這篇文章能夠非常清楚的了解其cache實現,其思路其實和文件系統的cache是一樣的。另外,淘寶已經在Tair線上環境中大量使用了LevelDB存儲引擎,推薦那岩寫的《LevelDB實現解析》,35頁的文檔,結合着讀代碼,會對理解代碼,有非常大的幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM