目录
block cache整体的结构大家可以参考 https://www.jianshu.com/p/75b93a664ebe 我认为他写的不错。
建议大家先学习一下上面的文档,有助于了解本文所说的生命周期。
这里先说一下一些数据结构。
我们使用的是LruCache,里面会按照一定的容量进行分桶,每个桶就是一个LruCacheShard。
里面有两个关键的成员结构分别是LRUHandle和LRUHandleTable。其中前面那个就是一个双向链表,LRUHandle名字起的不好,我认为应该改名叫LRUDataNode;至于后面的LRUHandleTable就是一个典型的hashmap,用它来快速定位某个key是否存在于cache中。
然后具体的代码流程就可以参考 https://www.jianshu.com/p/75b93a664ebe 了。上面的图也是从他那里copy的。
下面说一点上面的博客没有提到的知识点。
静态数据结构
当high_pri_pool_ratio_为0,按顺序给某个lru分桶里面插入E1,E2,E3之后,整个链表里面其实有4个元素,关系如下图一:
如上,头结点(就是lru_)里面是没有数值的。
如何实现LRU算法(block cache里node的生命周期)
大家如果自己实现,那肯定就是读的时候把节点放到最前面,然后一旦容量不够了,就把最后一个删掉。
但是c++代码一个很重要的问题就是要考虑内存的释放与所有权的问题。
咱们就按照下面的逻辑来讲讲cache里面各个节点的声明周期
1 从cache里面找没有找到
2 从磁盘或者别的地方加载到了内存
3 从内存把数据放到缓存
4 第二次查询cache,查到了
5 最终容量不足,清理某个节点
Cache::Handle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash) {
MutexLock l(&mutex_);
LRUHandle* e = table_.Lookup(key, hash);
if (e != nullptr) {
assert(e->InCache());
if (!e->HasRefs()) {
// The entry is in LRU since it's in hash and has no external references
LRU_Remove(e);
}
e->Ref();
e->SetHit();
}
return reinterpret_cast<Cache::Handle*>(e);
}
第一次查询,e肯定等于null,就直接返回了,外部调用方判断是空,说明cache里面没有。
第二步是外部调用逻辑的事情,忽略
看第三步,怎么把数据放到缓存里。
把数据放入block cache
首先看外部调用,下面这段代码来自BlockBasedTable::PutDataBlockToCache
size_t charge = block_holder->ApproximateMemoryUsage();
Cache::Handle* cache_handle = nullptr;
s = block_cache->Insert(block_cache_key, block_holder.get(), charge,
&DeleteCachedEntry<TBlocklike>, &cache_handle,
priority);
上面第一个参数block_cache_key就是cache里面的key
第二个参数就是value
第三个是需要的容量
第四个是之后需要删除的函数
cache_handle是一个指针的地址,这个指针最终指向缓存那个节点的位置。
最后是优先级,咱们暂时不管。
注意cache_handle是一个指针,但是Insert里面传递的是指针的地址。
经过层次调用,最后每个分桶里面的代码如下:
Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
size_t charge,
void (*deleter)(const Slice& key, void* value),
Cache::Handle** handle, Cache::Priority priority) {
LRUHandle* e = reinterpret_cast<LRUHandle*>(
new char[sizeof(LRUHandle) - 1 + key.size()]);
... // 填充e
autovector<LRUHandle*> last_reference_list; // 保存被淘汰的成员
{
MutexLock l(&mutex_);
// 对LRU list进行成员淘汰
EvictFromLRU(charge, &last_reference_list);
if (usage_ - lru_usage_ + charge > capacity_ &&
(strict_capacity_limit_ || handle == nullptr)) {
if (handle == nullptr) {
// Don't insert the entry but still return ok, as if the entry inserted
// into cache and get evicted immediately.
last_reference_list.push_back(e);
} else {
delete[] reinterpret_cast<char*>(e); // 没搞清楚这里为什么立刻删除,而不是像上面那样加到last_reference_list中稍后一起删除
*handle = nullptr;
s = Status::Incomplete("Insert failed due to LRU cache being full.");
}
} else {
LRUHandle* old = table_.Insert(e);
usage_ += e->charge;
if (old != nullptr) {
old->SetInCache(false);
if (Unref(old)) {
usage_ -= old->charge;
// old is on LRU because it's in cache and its reference count
// was just 1 (Unref returned 0)
LRU_Remove(old);
last_reference_list.push_back(old);
}
}
if (handle == nullptr) {
LRU_Insert(e);
} else {
e->Ref();
// 当调用者调用Release方法后,会调用LRU_Insert方法,将该元素插入到LRU list中
*handle = reinterpret_cast<Cache::Handle*>(e);
}
s = Status::OK();
}
}
// 释放被淘汰的元素
for (auto entry : last_reference_list) {
entry->Free();
}
return s;
}
我们知道外部调用的时候handle是一个指针的地址,那它是不为空的。换句话说,外部还希望拿到这个数据在缓存里面的引用,所以看下面的代码:
if (handle == nullptr) {
LRU_Insert(e);
} else {
e->Ref();
// 当调用者调用Release方法后,会调用LRU_Insert方法,将该元素插入到LRU list中
*handle = reinterpret_cast<Cache::Handle*>(e);
}
就咱们上面的handle 是不为null的,所以我们看到并没有调用LRU_Insert。也就是说,我们虽然调用了cache的insert,但是数据并没有真正插到那个lru链表里。这里很重要,我第一次看有点超出我的想象。
什么时候真正放入LRU链表
那这个节点的内存什么时候加到lru链表里面呢?上面的注释里面已经写了就是调用者调用Release方法后。咱们具体看看。
以构建sst的index 迭代器为例,我们从从磁盘上拿到了数据,然后放到了缓存里(就是上面说的,其实并没有放到LRU链表里),最终用这块内容构建迭代器。相关代码如下:
const Status s =
GetOrReadIndexBlock(no_io, get_context, lookup_context, &index_block);
auto it = index_block.GetValue()->NewIndexIterator(
internal_comparator()->user_comparator(),
rep->get_global_seqno(BlockType::kIndex), iter, kNullStats, true,
index_has_first_key(), index_key_includes_seq(), index_value_is_full());
index_block.TransferTo(it);
上面的GetOrReadIndexBlock最终会走到前文提到的PutDataBlockToCache。
之后就是index_block.GetValue()->NewIndexIterator,用读到的内容构建迭代器也不是咱们讨论的范围,第三步:index_block.TransferTo(it);
void TransferTo(Cleanable* cleanable) {
if (cleanable) {
if (cache_handle_ != nullptr) {
assert(cache_ != nullptr);
cleanable->RegisterCleanup(&ReleaseCacheHandle, cache_, cache_handle_);
} else if (own_value_) {
cleanable->RegisterCleanup(&DeleteValue, value_, nullptr);
}
}
ResetFields();
}
注册了一个RegisterCleanup,这是什么,看名字就是知道注册了一个清理函数,如下:
static void ReleaseCacheHandle(void* arg1, void* arg2) {
Cache* const cache = static_cast<Cache*>(arg1);
assert(cache);
Cache::Handle* const cache_handle = static_cast<Cache::Handle*>(arg2);
assert(cache_handle);
cache->Release(cache_handle);
}
注意是index_block.TransferTo(it); 换句话说是给迭代器注册了清理函数。而迭代器本身继承了Cleanable类。
这里调用了block cache的Release方法。
两个问题
1 什么时候调用ReleaseCacheHandle
2 block cache的Release方法做了什么
先说什么时候调用ReleaseCacheHandle。
Cleanable::~Cleanable() { DoCleanup(); }
inline void DoCleanup() {
if (cleanup_.function != nullptr) {
(*cleanup_.function)(cleanup_.arg1, cleanup_.arg2);
for (Cleanup* c = cleanup_.next; c != nullptr;) {
(*c->function)(c->arg1, c->arg2);
Cleanup* next = c->next;
delete c;
c = next;
}
}
}
如上,原来是迭代器析构的时候,会自动调用之前注册的cleanup方法。
这是index_block.TransferTo(it);的相关逻辑。
那如果index_block没有进行trans呢?
index_block本身是CachableEntry,内部持有了block cache里面那段数据的handle。
~CachableEntry() {
ReleaseResource();
}
void ReleaseResource() {
if (LIKELY(cache_handle_ != nullptr)) {
assert(cache_ != nullptr);
cache_->Release(cache_handle_);
} else if (own_value_) {
delete value_;
}
}
嗯,看到了也是析构函数。
LRUCacheShard::Release
咱们说说上面多次提到的Release方法。
bool LRUCacheShard::Release(Cache::Handle* handle, bool force_erase) {
if (handle == nullptr) {
return false;
}
LRUHandle* e = reinterpret_cast<LRUHandle*>(handle);
bool last_reference = false;
{
MutexLock l(&mutex_);
last_reference = e->Unref();
if (last_reference && e->InCache()) {
// The item is still in cache, and nobody else holds a reference to it
if (usage_ > capacity_ || force_erase) {
// The LRU list must be empty since the cache is full
assert(lru_.next == &lru_ || force_erase);
// Take this opportunity and remove the item
table_.Remove(e->key(), e->hash);
e->SetInCache(false);
} else {
// Put the item back on the LRU list, and don't free it
LRU_Insert(e);
last_reference = false;
}
}
if (last_reference) {
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
assert(usage_ >= total_charge);
usage_ -= total_charge;
}
}
// Free the entry here outside of mutex for performance reasons
if (last_reference) {
e->Free();
}
return last_reference;
}
注意,当容量没有满的时候 就会走到下面这块
Put the item back on the LRU list, and don’t free it
这是才算是把这个数据块真正交给了cache管理,LRU链表里才真正管理了这个数据段。
上面的流程就是用户写入把数据写入block cache的相关逻辑。
具体来说可以理解为
调用方使用block cache的insert的时候,如果指定了handle,那也就是说调用方还想持有数据的引用,在这种case下,block cache其实并没有真正管理那个内存段,只有当调用方不用那段数据了,缓存才会接管那段数据的所有权。
从block cache读数据
再来看看读的逻辑
Cache::Handle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash) {
MutexLock l(&mutex_);
LRUHandle* e = table_.Lookup(key, hash);
if (e != nullptr) {
assert(e->InCache());
if (!e->HasRefs()) {
// The entry is in LRU since it's in hash and has no external references
LRU_Remove(e);
}
e->Ref();
e->SetHit();
}
return reinterpret_cast<Cache::Handle*>(e);
}
如果找到了,且这段数据没有被别人引用,就调用LRU_Remove。第一次看到这里头都大了。
每次查询不应该把数据移动到链表头么?
查询后的结构图如下:
上面的图就是,假定查找了E2,发现没有别人引用,就直接从原链表里面移除。然后外部持有E2的引用。
容量不够怎么处理
看下面的逻辑
void LRUCacheShard::EvictFromLRU(size_t charge,
autovector<LRUHandle*>* deleted) {
while ((usage_ + charge) > capacity_ && lru_.next != &lru_) {
LRUHandle* old = lru_.next;
// LRU list contains only elements which can be evicted
assert(old->InCache() && !old->HasRefs());
LRU_Remove(old);
table_.Remove(old->key(), old->hash);
old->SetInCache(false);
size_t old_total_charge = old->CalcTotalCharge(metadata_charge_policy_);
assert(usage_ >= old_total_charge);
usage_ -= old_total_charge;
deleted->push_back(old);
}
}
每次insert的时候都会调用EvictFromLRU
删除头最老节点的结构如下:
如上,E1被删除了。
总结
关于block cache的声明周期,说白了就是三个字:所有权。
谁接管谁负责!
一点题外话
写完上面那些文档后,我忽然想到一个现实中的例子,不是那么贴切,大家仅供一乐:
你叫小明,你有一个姐姐,你家户口本上,有你爸爸和你还有你姐姐,之后你姐姐出嫁了,虽然你们家户口本上还有你姐,但是你家里确实没有你姐这个人了,餐桌上,床上都没有她了,她的位置彻底空了。空到你家原本可以住4个人,现在有一个人走了,家里的资源完全可以再住一个人。
然后过了几天,你姐回来了,你问了一下,发现原来是你姐夫game over了,外面没有谁能再照顾你姐了,然后你姐又回来了,然后你发现原来的位置又被你姐占据了,餐桌上,床上都有她了。
然后之后又有人来你家询问你姐,然后你姐就又出去了,和那个人生活一段时间。
上面的询问,就是Lockup,问了就是要负责,而且你还特定告诉我姐在新的家里的位置,你不负责谁负责?!
你既然问了,且我姐还在,那你就得照顾她!
上面说的户口本,就是那个hasmap,家里的餐桌,床就是指的那个链表的存储空间。
关键一句话,不能在lockup里加上handler的参数,否则你就得负责。
关于pin_l0_filter_and_index_blocks_in_cache
在正常的get流程下
if pin_l0_filter_and_index_blocks_in_cache is false
pin也是false
就会立即调用index_block.Reset(); 也就是把数据放会lru cache里 等着被删除
if pin_l0_filter_and_index_blocks_in_cache is true
对于level为0的 block
pin是ture 那么数据就没有立即进入lrucache,那就不会被删除
参考资料
https://www.jianshu.com/p/75b93a664ebe