谈谈rocksdb里面的block cache的生命周期管理

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值