RocksDB. LRUCache源碼分析

Block Cache

RocksDB使用Block cache作為讀cache。用戶可以指定Block cache使用LRUCache,并可以指定cache的大小。
Block cache分為兩個,一個是用來緩存未被壓縮的block數據,另一個用來緩存壓縮的block數據。
處理讀請求時,先查找未壓縮的block cache,再查找壓縮的block cache。壓縮的block cache可以替換操作系統的page cache。
用法如下

std::shared_ptr<Cache> cache = NewLRUCache(capacity);
BlockBasedTableOptions table_options;
table_options.block_cache = cache;
Options options;
options.table_factory.reset(new BlockBasedTableFactory(table_options));
table_options.block_cache_compressed = another_cache;

LRUCache

默認RocksDB會創建一個容量為8M的LRUCache。
LRUCache內部對可以使用的容量進行了分片(Shard,下面習慣性地稱之為分桶),每個桶都維護了自己的LRU list和用于查找的hash table。
用戶可以指定幾個參數

  • capacity: cache的總容量
  • num_shard_bits:2^num_shard_bits為指定的分桶數量。如果不指定,則會計算得到一個分桶數,最大分桶數為64個。
默認計算分桶數量的方法
指定的capacity / min_shard_size,其中min_shard_size為每個shard最小的大小,為512KB
  • strict_capacity_limit:有一些場景,block cache的大小會大于指定的cache 容量,比如cache中的block都因為外部有讀或者Iterator引用而無法被淘汰,這些無法淘汰的block總大小超過了總容量。這種情況下,如果strict_capacity_limit為false,后續的讀操作仍然可以將數據插入到cache中??赡茉斐蓱贸绦騉OM。
該選項是限制每個分桶的大小,不是cache的總體使用大小。
  • high_pri_pool_ratio:LRUCache提供了優先級的功能,該選項可以指定高優先級的block在每個桶中可以占的比例。

類圖

LRUCache類圖

類圖展示了各個類中的主要成員和一些操作。

  • Cache
    定義了Cache的接口,包括Insert, Lookup, Release等操作。
  • ShardedCache
    支持對Cache進行分桶,分桶數量為2^num_shard_bits,每個桶的容量相等。
    分桶的依據是取key的hash值的高num_shard_bits位
(num_shard_bits_ > 0) ? (hash >> (32 - num_shard_bits_)) : 0;
  • LRUCache
    維護了一個shard數組,每個shard,即每個桶,都是一個LRUCacheShard用來cache分過來的key value。
  • CacheShard
    定義了一個桶的接口,包括Insert, Lookup, Release等操作,Cache的相關調用經過分桶處理后,都會調用指定桶的對應操作。
  • LRUCacheShard
    維護了一個LRU list和hash table,用來實現LRU策略,他們的成員類型都是LRUHandle。
  • LRUHandleTable
    hash table的實現,根據key再次做了分組處理,并且盡量保證每個桶中只有一個元素,元素類型為LRUHandle。提供了Lookup, Insert, Remove操作。
  • LRUHandle
    保存key和value的單元,并且包含前向和后續指針,可以組成雙向循環鏈表作為LRU list。
    保存了引用計數和是否在cache中的標志位。詳細說明如下

LRUHandle can be in these states:

  1. Referenced externally AND in hash table.
    In that case the entry is not in the LRU. (refs > 1 && in_cache == true)
  2. Not referenced externally and in hash table. In that case the entry is
    in the LRU and can be freed. (refs == 1 && in_cache == true)
  3. Referenced externally and not in hash table. In that case the entry is
    in not on LRU and not in table. (refs >= 1 && in_cache == false)

All newly created LRUHandles are in state 1. If you call LRUCacheShard::Release on entry in state 1, it will go into state 2.
To move from state 1 to state 3, either call LRUCacheShard::Erase or LRUCacheShard::Insert with the same key.
To move from state 2 to state 1, use LRUCacheShard::Lookup.
Before destruction, make sure that no handles are in state 1. This means that any successful LRUCacheShard::Lookup/LRUCacheShard::Insert have a matching RUCache::Release (to move into state 2) or LRUCacheShard::Erase (for state 3)

LRUCache結構如下

LRUCache結構

Insert的實現

從上到下逐層分析LRUCache的Insert實現。
RocksDB中,通過一個選項指定所使用的Cache大小,在open的時候傳給DB。

  auto cache = NewLRUCache(1 * 1024 * 1024 * 1024);
  BlockBasedTableOptions bbt_opts;
  bbt_opts.block_cache = cache;

調用Insert的一例如下

// 從option選項中獲取bock cache的指針
Cache* block_cache = rep->table_options.block_cache.get();
// ...
  if (block_cache != nullptr && block->value->cachable()) {
    s = block_cache->Insert(
        block_cache_key,
        block->value,
        block->value->usable_size(),
        &DeleteCachedEntry<Block>,
        &(block->cache_handle),
        priority);
...

LRUCache的Insert實現在它的基類ShardedCache中,先對key做hash,hash的方法類似murmur hash,然后根據hash值確定存入到哪一個桶(Shard)中??梢哉J為一個桶就是一個獨立的LRUCache,其類型是LRUCacheShard。

// util/sharded_cache.cc
Status ShardedCache::Insert(const Slice& key, void* value, size_t charge,
                            void (*deleter)(const Slice& key, void* value),
                            Handle** handle, Priority priority) {
  uint32_t hash = HashSlice(key);
  return GetShard(Shard(hash))
      ->Insert(key, hash, value, charge, deleter, handle, priority);
}

通過key確定分桶后,調用對應LRUCacheShard的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);

LRUCacheShard的三個主要的成員變量,Insert主要是對這三個成員變量的操作。

class LRUCacheShard : public CacheShard {
...
private:
  LRUHandle lru_;  // 鏈表的頭結點
  LRUHandle* lru_low_pri_; // 指向低優先級部分的頭結點
  LRUHandleTable table_; // 自己實現的一個hash table
};

一個LRUCache的底層實現,是依賴一個鏈表和一個HashTable,鏈表用來維護成員的淘汰的順序和高低優先級,HashTable用來進行快速的查找。他們的成員類型是LRUHandle*。后面會詳細了解他們的功能,下面分析一下Insert的實現,分為下面幾步:

  1. 根據key value等參數構造LRUHandle
  2. 加shard級別鎖,開始對LRUCacheShard做修改
  3. 根據LRU策略和空間使用情況進行成員淘汰
  4. 根據容量選擇是否插入
  • 如果計算發現,插入后的總的使用量仍然大于cache的容量,并且傳入了需要嚴格限制flag,則不進行插入操作。這里有一個小點是,參數中handle是一個輸出參數,插入成功后,賦值為指向cache中的LRUHandle成員的指針。如果用戶傳入的handle為null,說明用戶不需要返回指向成員的handle指針,雖然插入失敗,但是不報錯;否則將handle賦值為null后報錯。
  • 如果釋放了足夠的空間,或者不需要嚴格限制空間使用,則開始進行插入操作。首先插入到hash table中,如果已經存在key對應的entry,則置換出來后,進行釋放。

插入后,如果handle為null,說明插入后沒人有持有對這個元素的引用,插入到LRU list中等待被淘汰。否則,暫時不將該元素插入到LRU list中,而是賦值給handle,等調用者釋放了對該元素的引用,再插入到LRU list中

  1. 釋放被淘汰的元素和插入失敗的元素。
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 {
        // 當調用者調用Release方法后,會調用LRU_Insert方法,將該元素插入到LRU list中
        *handle = reinterpret_cast<Cache::Handle*>(e);
      }
      s = Status::OK();
    }
  }
  // 釋放被淘汰的元素
  for (auto entry : last_reference_list) {
    entry->Free();
  }

  return s;
}

上面的流程中,有兩次插入,分別是hash table的插入和鏈表的插入。

LRUHandleTable

  • LRUHandleTable內部根據LRUHandle元素中的hash值分桶
  • 每個桶是一個鏈表
  • 插入時插入到鏈表的尾部。
  • 如果元素數量大于桶的數量,則對桶的數量調整為之前的兩倍,為的是讓一個桶的鏈表長度盡量小于等于1.
class LRUHandleTable {
 public:
  ...
  LRUHandle* Insert(LRUHandle* h);
  ...
private:
  ...
  uint32_t length_;  // 桶的數量,默認數量為16
  uint32_t elems_;  // 元素總數
  LRUHandle** list_; // 一維數組,數組的成員為每個鏈表的頭指針
};

hash table的Insert實現:

  1. 根據key和hash值,找到對應的桶,在桶中沿著鏈表找到key和hash值對應的節點
  2. 如果key和hash值對應的節點已經存在,則用新的節點替換老的節點,將老節點返回
  3. 如果key和hash值對應的節點不存在,則插入到鏈表結尾,并且判斷是否需要對hash table擴容
LRUHandle* LRUHandleTable::Insert(LRUHandle* h) {
  LRUHandle** ptr = FindPointer(h->key(), h->hash);
  LRUHandle* old = *ptr;
  // 如果key已經存在,則替換老的元素
  h->next_hash = (old == nullptr ? nullptr : old->next_hash);
  *ptr = h;
  // 如果key不存在,則判斷元素總數和桶的數量是否需要擴容
  if (old == nullptr) {
    ++elems_;
    if (elems_ > length_) {
      Resize();
    }
  }
  return old;
}

LRUHandle** LRUHandleTable::FindPointer(const Slice& key, uint32_t hash) {
  LRUHandle** ptr = &list_[hash & (length_ - 1)]; // 根據hash值找到對應的桶
  // 沿著鏈表比較key和hash值,直到鏈表尾部
  while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
    ptr = &(*ptr)->next_hash;
  }
  return ptr;
}

LRU list

LRU list按優先級分為兩個區,高優先級區和低優先級區

  • lru_為鏈表頭,也是高優先級的表頭
  • lru_low_pri_為低優先級的表頭
  • 當high_pri_pool_ratio_為0時,表示不分優先級,則高優先級和低優先級的表頭都是LRU list的表頭

插入時,不論是高優先級插入還是低優先級插入,都插入到表頭位置。
Insert實現如下

void LRUCacheShard::LRU_Insert(LRUHandle* e) {
  assert(e->next == nullptr);
  assert(e->prev == nullptr);
  if (high_pri_pool_ratio_ > 0 && e->IsHighPri()) {
    // Inset "e" to head of LRU list.
    e->next = &lru_;
    e->prev = lru_.prev;
    e->prev->next = e;
    e->next->prev = e;
    e->SetInHighPriPool(true);
    high_pri_pool_usage_ += e->charge;
    MaintainPoolSize();
  } else {
    // Insert "e" to the head of low-pri pool. Note that when
    // high_pri_pool_ratio is 0, head of low-pri pool is also head of LRU list.
    e->next = lru_low_pri_->next;
    e->prev = lru_low_pri_;
    e->prev->next = e;
    e->next->prev = e;
    e->SetInHighPriPool(false);
    lru_low_pri_ = e;
  }
  lru_usage_ += e->charge;
}

以上就是LRUCache的插入流程。


寫的比較匆忙, 很多細節也沒有提到, 有寫的不對或者不明確的地方, 請指正, 謝謝!

參考資料:
https://github.com/facebook/rocksdb/wiki/Block-Cache

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容