• 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中,所有的getmerge操作均是抽象成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页的文档,结合着读代码,会对理解代码,有非常大的帮助。

  • 相关阅读:
    架构之美阅读笔记05
    架构之美阅读笔记04
    已经导入到VS工具箱中的DevExpress如何使用
    C#中遇到的方法总结
    vs下C# WinForm 解决方案里面生成的文件都是什么作用?干什么的?
    ssh关于含有外键的传值中无法识别正确的action的原因和解决办法
    MVC模式在Java Web应用程序中的实例分析
    浅谈对MVC的理解
    简述23种设计模式
    浅谈对可用性和易用性的认识以及对如何增加系统功能的理解
  • 原文地址:https://www.cnblogs.com/duanxz/p/5137940.html
Copyright © 2020-2023  润新知