** Leveldb中的缓存机制** 这篇文章将讲解LevelDB缓存模块的相关内容,本文内容比较简单,读者只需按照顺序阅读即可。
leveldb中的缓存分为两种:
sstable的table_cache_
(类DBImpl
中)
Block的block_cache_
(可选,在options
中)
sstable的Cache就是缓存(Key:file_number
Value:TableAndFile
),其中TableAndFile
是关于sstable的元数据,记录的是sst在内存中的table
结构体信息和RandomAccessFile
的文件描述符的信息。所以table_cache_
缓存的不是sst文件的内容,而是sst文件的信息。
Block的Cache缓存(Key:table的cache_id和Block的offset拼接而成 Value:Block结构体),所以block_cache
缓存的是真正的内容。
我们在LevelDB的源代码中分别看一下这两种Cache.
Table Cache 首先我们从Version::Get
方法出发,这里调用了一个table_cache_
的Get方法,这个Get方法会调用FindTable
的函数。
1 2 3 state ->s = state->vset ->table_cache_ -> Get (*state->options , f-> number, f ->file_size , state-> ikey, &state -> saver, SaveValue);
1 2 3 4 5 6 7 8 9 10 11 12 13 Status TableCache::Get (const ReadOptions& options, uint64_t file_number, uint64_t file_size, const Slice& k, void * arg, void (*handle_result)(void *, const Slice&, const Slice&)) { Cache::Handle* handle = nullptr; 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, handle_result); cache_->Release(handle); } return s; }
我们再来看一下这个FindTable
函数里面是什么:
这里面,首先会在cache_
里面寻找是否存在这个file_number
的缓存信息。如果不存在,那么就要新建一个TableAndFile
,把(file_number
,TableAndFile
)插入进table_cache_
的缓存中。这样下次命中的时候,就可以利用缓存知道sst的元数据信息,不需要重新解析sstable文件了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Status TableCache::FindTable (uint64_t file_number, uint64_t file_size, Cache::Handle** handle) { Status s; char buf[sizeof (file_number)]; EncodeFixed64(buf, file_number); Slice key (buf, sizeof (buf)) ; *handle = cache_->Lookup(key); if (*handle == nullptr) { std ::string fname = TableFileName(dbname_, file_number); RandomAccessFile* file = nullptr; Table* table = nullptr; s = env_->NewRandomAccessFile(fname, &file); if (!s.ok()) { std ::string old_fname = SSTTableFileName(dbname_, file_number); if (env_->NewRandomAccessFile(old_fname, &file).ok()) { s = Status::OK(); } } if (s.ok()) { s = Table::Open(options_, file, file_size, &table); } if (!s.ok()) { assert(table == nullptr); delete file; } else { TableAndFile* tf = new TableAndFile; tf->file = file; tf->table = table; *handle = cache_->Insert(key, tf, 1 , &DeleteEntry); } } return s; }
Block Cache 同样是TableCache::Get
函数,里面有这样一个函数s = t->InternalGet(options, k, arg, handle_result);
InternalGet
中又调用了BlockReader
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 BlockContents contents;if (block_cache != nullptr) { char cache_key_buffer[16 ]; EncodeFixed64(cache_key_buffer, table->rep_->cache_id); EncodeFixed64(cache_key_buffer + 8 , handle.offset()); Slice key (cache_key_buffer, sizeof (cache_key_buffer)) ; cache_handle = block_cache->Lookup(key); if (cache_handle != nullptr) { block = reinterpret_cast<Block*>(block_cache->Value(cache_handle)); } else { s = ReadBlock(table->rep_->file, options, handle, &contents); if (s.ok()) { block = new Block(contents); if (contents.cachable && options.fill_cache) { cache_handle = block_cache->Insert(key, block, block->size(), &DeleteCachedBlock); } } } } else { s = ReadBlock(table->rep_->file, options, handle, &contents); if (s.ok()) { block = new Block(contents); } }
BlockReader
函数会判断options
里面是否开起来block层级的缓存,如果开起来,它就会把sst的cache_id(构建sst的时候单独递增的一个成员)和block的offset作为Key,把Block作为Value插入到缓存中。
LRU缓存的实现 LRU(Least recently used)是一种资源淘汰策略,中文解释为最近最少使用,通俗理解就是当缓存容量达到阈值时,淘汰掉缓存中最长时间没有被访问的资源 。
下图展示了LevelDB中缓存模块的结构,最外层ShardedLRUCache由多个LRUCache组成,LRUCache是一个LRU缓存单元,它由一个哈希表HandleTable和两个双向环形链表LRUHandle List组成。
下面我们从内至外剖析上述缓存模块的各个单元。
LRUHandle LRUHandle是LRU缓存中的基本数据单元,存储了数据的基本信息。LRUHandle中的键存储(key_dada
)的是文件序号,与SSTable文件对象一一对应,引用计数refs表示节点被引用数,初使时为1,表示被缓存引用,用户访问节点时refs加1,释放时refs减1,当节点被缓存淘汰且无用户引用时将被销毁。其余属性含义请看下列注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct LRUHandle { void * value; void (*deleter)(const Slice&, void * value); LRUHandle* next_hash; LRUHandle* next; LRUHandle* prev; size_t charge; size_t key_length; bool in_cache; uint32_t refs; uint32_t hash; char key_data[1 ]; };
LRUHandle List就是以LRUHandle为节点的双向环形链表。
HandleTable HandleTable是链表存储式的哈希表,其存储节点为LRUHandle。每次节点查询时先根据目标键的哈希值找到对应的哈希桶,再沿着哈希桶中的链表查找目标哈希值的节点。
1 2 3 4 5 6 7 8 9 LRUHandle** FindPointer (const Slice& key, uint32_t hash) { LRUHandle** ptr = &list_[hash & (length_ - 1 )]; while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) { ptr = &(*ptr)->next_hash; } return ptr; }
HandleTable的装填因子为1 ,当存储的数据量大于哈表表长度时就会进行扩容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void Resize () { uint32_t new_length = 4 ; while (new_length < elems_) { new_length *= 2 ; } LRUHandle** new_list = new LRUHandle*[new_length]; memset (new_list, 0 , sizeof (new_list[0 ]) * new_length); uint32_t count = 0 ; for (uint32_t i = 0 ; i < length_; i++) { LRUHandle* h = list_[i]; while (h != nullptr) { LRUHandle* next = h->next_hash; uint32_t hash = h->hash; LRUHandle** ptr = &new_list[hash & (new_length - 1 )]; h->next_hash = *ptr; *ptr = h; h = next; count++; } } delete[] list_; list_ = new_list; length_ = new_length; } };
LRUCache LRUCache由一个哈希表和两个双向环形链表组成,是对LRU资源淘汰机制的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class LRUCache { size_t capacity_; mutable port::Mutex mutex_; size_t usage_ GUARDED_BY (mutex_) ; LRUHandle lru_ GUARDED_BY (mutex_) ; LRUHandle in_use_ GUARDED_BY (mutex_) ; HandleTable table_ GUARDED_BY (mutex_) ; };
LRUCache中哈希表table_用于存储了所有缓存数据,这些缓存数据被两个双向环形链表分为正在被访问的缓存数据(in_use_)和未被访问的缓存数据(lru_),当缓存容量满后lru_中数据可能会被淘汰。lru_和in_use_都有一个空的头节点 ,越靠近头节点的元素越早被添加进链表中,所以当需要淘汰lru_中的数据时,靠近头节点的元素将优先被淘汰掉。
数据查询时,若缓存数据未命中,从原始数据中获取的数据将被插入缓存中,此时这个数据是被用户引用的,所以初始化数据节点LRUHandle时引用计数为1,随后将节点插入哈希表table_和使用链表in_use_中。若新数据插入后缓存空间已满,将从in_use_中移除溢出的数据 ,因为越靠近头节点的元素越早添加进链表中,对于in_use_链表而言就是越靠近头节点的数据越长时间没有被使用到 ,因此将从头节点开始遍历后继节点将溢出部分 的数据节点移除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Cache::Handle* LRUCache::Insert (const Slice& key, uint32_t hash, void * value, size_t charge, void (*deleter)(const Slice& key, void * value)) { MutexLock l (&mutex_) ; LRUHandle* e = reinterpret_cast<LRUHandle*>(malloc (sizeof (LRUHandle) - 1 + key.size())); e->value = value; e->deleter = deleter; e->charge = charge; e->key_length = key.size(); e->hash = hash; e->in_cache = false ; e->refs = 1 ; std ::memcpy (e->key_data, key.data(), key.size()); if (capacity_ > 0 ) { e->refs++; e->in_cache = true ; LRU_Append(&in_use_, e); usage_ += charge; FinishErase(table_.Insert(e)); } else { e->next = nullptr; } while (usage_ > capacity_ && lru_.next != &lru_) { LRUHandle* old = lru_.next; assert(old->refs == 1 ); bool erased = FinishErase(table_.Remove(old->key(), old->hash)); if (!erased) { assert(erased); } } return reinterpret_cast<Cache::Handle*>(e); }
缓存数据淘汰时依次从哈希表和链表中将数据移除,缓存中不再持有该数据,数据节点引用计数减1,此时节点可能存在两种状态:
该节点未被用户引用,引用计数为0,节点将被销毁
该节点正在被用户引用,引用计数不为0,节点等待用户释放后再进行销毁
1 2 3 4 5 6 7 8 9 10 11 12 bool erased = FinishErase(table_.Remove(old->key(), old->hash));bool LRUCache::FinishErase (LRUHandle* e) { if (e != nullptr) { LRU_Remove(e); e->in_cache = false ; usage_ -= e->charge; Unref(e); } return e != nullptr; }
当用户从缓存中查询数据时,将从哈希表中查询出对应数据节点,若命中则该节点可能存在以下两种状态:
该节点未被其他用户引用,此时节点引用计数为1,节点添加至使用链表in_use_中,引用计数加1
该节点正在被其他用户引用,此时节点引用计数大于1,说明节点已经在使用链表in_use_中了,只需要将引用计数加1即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Cache::Handle* LRUCache::Lookup (const Slice& key, uint32_t hash) { MutexLock l (&mutex_) ; LRUHandle* e = table_.Lookup(key, hash); if (e != nullptr) { Ref(e); } return reinterpret_cast<Cache::Handle*>(e); }void LRUCache::Ref (LRUHandle* e) { if (e->refs == 1 && e->in_cache) { LRU_Remove(e); LRU_Append(&in_use_, e); } e->refs++; }
当用户进行节点释放后,引用计数减1,此时节点可能存在以下几种状态:
该节点已经被缓存淘汰,用户释放后节点引用计数为0,在淘汰时节点已经从哈希表和相应链表中移除了,此时只需要进行节点销毁
该节点已经被缓存淘汰,但是正在被其他用户引用,引用计数仍然大于0,不做任何处理,当所有用户都释放后将命中上一条状态
该节点未被淘汰,且没有被其他用户引用,此时引用计数为1(缓存引用),节点将从使用链表in_use_中移除并添加至未使用链表lru_中
该节点未被淘汰,但是正在被其他用户引用,此时引用计数大于1,不做任何处理
ShardedLRUCache ShardedLRUCache中包含多个LRUCache,它的主要功能是将缓存数据通过哈希分片存储到不同的LRU缓存模块中。哈希分片经常用于集群系统的横向扩容场景,这里主要为了避免将缓存数据存储在一个缓存模块中以降低资源竞态,提高数据访问的并发度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static const int kNumShardBits = 4 ;static const int kNumShards = 1 << kNumShardBits;class ShardedLRUCache : public Cache { private: LRUCache shard_[kNumShards]; static uint32_t Shard (uint32_t hash) { return hash >> (32 - kNumShardBits); } public: Handle* Insert (const Slice& key, void * value, size_t charge, void (*deleter)(const Slice& key, void * value)) override { const uint32_t hash = HashSlice(key); return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter); } }