LevelDB學習筆記 (3): 長文解析memtable、跳錶和記憶體池Arena

周小倫發表於2021-07-18

LevelDB學習筆記 (3): 長文解析memtable、跳錶和記憶體池Arena

1. MemTable的基本資訊

我們前面說過leveldb的所有資料都會先寫入memtable中,在leveldb中每個 LevelDB 例項最多會維護兩個 MemTable: mem_imm_mem_ 可以讀寫,imm_ 只讀。分別對應了memtable和immutable table。

1.1 首先去看一下db/memtable.h

下面是基本的構造資訊

class MemTable {
 public:
  // 必須顯示呼叫
  explicit MemTable(const InternalKeyComparator& comparator);
  // 禁止拷貝構造和拷貝賦值
  MemTable(const MemTable&) = delete;
  MemTable& operator=(const MemTable&) = delete;

  // 增加引用計數
  void Ref() { ++refs_; }

  //引用計數減一,當引用計數為0時,銷燬當前物件。
  void Unref() {
    --refs_;
    assert(refs_ >= 0);
    if (refs_ <= 0) {
      delete this;
    }
  }

下面是memtable的一些基本操作和需要的基本資料結構

有對於memtable的資料寫入和讀取。分別為add和get

以及定義需要儲存資料的跳錶結構和對應的key的比較器

這裡可以發現對應key的add操作是需要對應的seqnumber這個就相當於對於每次更新的config number、version number的意思。這樣我們可以一直獲取的是最新的資料。。

 // add 和 get操作
  void Add(SequenceNumber seq, ValueType type, const Slice& key,
           const Slice& value);
  bool Get(const LookupKey& key, std::string* value, Status* s);

 private:
  friend class MemTableIterator;
  friend class MemTableBackwardIterator;
  // 針對於key的比較操作
  struct KeyComparator {
    const InternalKeyComparator comparator;
    explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
    int operator()(const char* a, const char* b) const;
  };
  // 跳錶用於儲存資料
  typedef SkipList<const char*, KeyComparator> Table;

  ~MemTable();  // Private since only Unref() should be used to delete it

  KeyComparator comparator_;
  int refs_;
  //  記憶體分配器
  Arena arena_;
  Table table_;
};

}  // namespace leveldb

1.2 跳錶SkipList

由於memtable中的資料都是放到跳錶中的。所以在深入理解memtable之前我們必須要先來看一下跳錶這個資料結構,看過redis原始碼解析的同學應該對跳錶這個資料結構很熟悉了。而且在演算法導論中也有學到過這個資料結構。跳錶於1990 年,由William Pugh 發表其論文:Skip Lists: A Probabilistic Alternative to Balanced Trees

說是跳錶其實就是多個單連結串列連線起來,對於一個單連結串列我們的search操作的時間複雜度是\(0(n)\) 。但是如果有一些輔助連結串列的幫助就可以幫我們減少這一時間複雜度

image-20210705140949224

經過下面無聊的證明,跳錶對於查詢的時間複雜度可以變成\(O(logn)\)

image-20210705141149050

SkipList查詢流程

對於查詢72的流程如下

  1. 首先在第一層遍歷14、79發現72大於14小於79因此進入第二層從14繼續搜尋
  2. 第二層發現50 < 72 < 79所以進入第三層從50開始繼續搜尋
  3. 第三層發現66 < 72 < 79所以進入第四層從66開始繼續搜尋
  4. 在第四層就會找到72.這就是整個的搜尋過程

image-20210707130856031

1.3 LevelDB的SkipList

記憶體順序學習

首先leveldb中是通過了原子操作和記憶體順序做到了無鎖併發。這裡先學習一下記憶體順序(Memory Order)

學習部落格?c++11多執行緒初探

知乎討論?如何理解 C++11 的六種 memory order?

https://github.com/apache/incubator-brpc/blob/master/docs/cn/atomic_instructions.md#memory-fence

在 C11/C++11 中,引入了六種不同的 memory order,可以讓程式設計師在併發程式設計中根據自己需求儘可能降低同步的粒度,以獲得更好的程式效能。這六種 order 分別是:

relaxed, acquire, release, consume, acq_rel, seq_cst

引用自Senlin's Blog

其中memory_order_relaxed編譯器僅保證load和stroe是原子操作,除此之外並不提供任何跨執行緒的同步。 如果某個操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程式計數器是一種典型的應用場景:

#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f);
    }
    for (auto& t : v) {
        t.join();
    }
    assert(cnt == 10000);    // never failed
    return 0;
}

而對於Release-Acquire ordering這種模型store()使用memory_order_release,而load()使用memory_order_acquire。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:

  • store()之前的所有讀寫操作,不允許被移動到這個store()的後面。
  • load()之後的所有讀寫操作,不允許被移動到這個load()的前面。

  除此之外,還有另一種效果:假設 Thread-1 store()的那個值,成功被 Thread-2 load()到了,那麼 Thread-1 在store()之前對記憶體的所有寫入操作,此時對 Thread-2 來說,都是可見的。

比如下面的例子

std::atomic<bool> ready{ false };
int data = 0;
//thread 1 
void producer()
{
    data = 100;                                       // A
    ready.store(true, std::memory_order_release);     // B
}
//thread 2
void consumer()
{
    while (!ready.load(std::memory_order_acquire))    // C
        ;
    assert(data == 100); // never failed              // D
}

std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
  1. 這個執行流程是對於程式1他會保證A不被移到B的後面

  2. 對於程式2而言C也不會移到D的後面

  3. 而當程式2讀到ready為true的時候,就表示程式1中對於data的寫入對於程式2是可見的因此對於D的執行則永遠不會failed

1. 跳錶的定義

  1. 有指定的最大層數
  2. 為節點分配空間的arena
  3. 指向表頭的指標head
  4. max_height_表示目前跳錶最高多少層
  5. rnd_用來生成每個節點層數的隨機數
 private:
  enum { kMaxHeight = 12 };

  inline int GetMaxHeight() const {
    return max_height_.load(std::memory_order_relaxed);
  }
  Node* NewNode(const Key& key, int height);

  // Immutable after construction
  Comparator const compare_;
  Arena* const arena_;  // Arena used for allocations of nodes
  Node* const head_;
  std::atomic<int> max_height_;  // Height of the entire list

  // Read/written only by Insert().
  Random rnd_;
};

跳錶的建構函式

這裡通過傳入的比較器和對應的分配器進行構造

但是這裡有一些需要注意的點。

template <typename Key, class Comparator>
SkipList<Key, Comparator>::SkipList(Comparator cmp, Arena* arena)
    : compare_(cmp),
      arena_(arena),
      head_(NewNode(0 /* any key will do */, kMaxHeight)),
      max_height_(1),
      rnd_(0xdeadbeef) {
  for (int i = 0; i < kMaxHeight; i++) {
    head_->SetNext(i, nullptr);
  }
}

1. NewNode函式

另外就是給將head_初始化,分配一個空節點,初始化跳錶高度為1,初始化隨機函式(分配一個種子)。

最後for迴圈裡,將頭結點的每一層都初始化。

這裡注意NewNode利用了一個叫做placement New的操作。來實現對於Node的構造

關於Placement New

實際上每個Node都所擁有的大小是會很高度掛鉤,因為這涉及到了next陣列的大小。

typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
    const Key& key, int height) {
  char* const node_memory = arena_->AllocateAligned(
      sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
  return new (node_memory) Node(key);
}

2. Node函式

Node函式中有節點需要用到的一些幫助函式

比如Next和SetNext。這裡就用到了上面所說的Release-Acquire ordering在無所鎖的情況下實現原子性的讀寫操作

並且保證當執行緒1寫入next[n]的時候執行緒2會線上程1寫完才會讀到這個值

template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}

  Key const key;

  // Accessors/mutators for links.  Wrapped in methods so we can
  // add the appropriate barriers as necessary.
  Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].store(x, std::memory_order_release);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_relaxed);
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_relaxed);
  }

 private:
  // 記錄了該節點在每一層的next節點
  // Array of length equal to the node height.  next_[0] is lowest level link.
  std::atomic<Node*> next_[1];
};

2. 跳錶實現之-初始化

在db_impl找到了對於mem的初始化。也就是會有對於skiplist的初始化

當我們執行

  leveldb::DB *db;  leveldb::Options opts;  opts.create_if_missing = true;  leveldb::Status status = leveldb::DB::Open(opts, "../tmp/testdb1", &db);

這裡的DB:open會執行下面這一行來初始化MemTable

image-20210707161159528

而在memtable.cc中的MemTable的建構函式中通過initial list實現了對於skipList的構造

image-20210707161458858

3. 跳錶實現之-插入操作

插入整體的思路還是比較簡單的,但是有一些小細節需要注意

1. next_[1]的使用

這裡用next_[1]這樣的char陣列來替代了char*,但本質還是一個單連結串列。

如下面這樣的結構就是

b->c->d->g

image-20210707212727598

2. 插入之前要先找到當前key對應於每一層的所有前驅節點

  1. 首先從最高層開始尋找,並且讓x等於頭節點。同時獲取頭節點在最高層的next節點
  2. 如果這個節點比要插入的節點小。則要插入的節點要放到這個節點後面,同時繼續在當前層搜尋
  3. 如果遇到不符合判斷的節點即要插入節點要放在這個節點的前面,那麼我們就找了要插入節點的前驅節點
  4. 同時讓層數--去上一層繼續尋找
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
                                              Node** prev) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    Node* next = x->Next(level);
    if (KeyIsAfterNode(key, next)) {
      // Keep searching in this list
      x = next;
    } else {
      if (prev != nullptr) prev[level] = x;
      if (level == 0) {
        return next;
      } else {
        // Switch to next list
        level--;
      }
    }
  }
}

3. 邏輯上儲存和物理上儲存的區別

image-20210707222007226

整體區別如上圖。leveldb基本上是完全引用的形式實現了多層的skiplist,這樣是非常節省記憶體的。他這樣的實現得益於在newNode時候的一些小技巧

 char* const node_memory = arena_->AllocateAligned(
      sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
  return new (node_memory) Node(key);

這裡以上圖為例子對於14這個節點他會被分別一個Node + 多一個next_的地址

因為他需要14->next_[1]這個來儲存42這個節點所在的地址。

4. 整個的insert函式

這個時候再來看insert就比較簡單了,配合註釋應該可以看懂的

template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  //這個函式的作用有(2)
  // 1. 找到第一個大於等於key的節點
  // 2. 就是初始化prev陣列,讓它分別儲存要插入的key在不同level中的前驅
  Node* x = FindGreaterOrEqual(key, prev);
  // Our data structure does not allow duplicate insertion
	// 要麼x為空 要麼x >key 
  assert(x == nullptr || !Equal(key, x->key));

  int height = RandomHeight();
  // 如果想要插入的隨機高度  > 當前的最大高度
  // 則更新最大高度
  if (height > GetMaxHeight()) {
    for (int i = GetMaxHeight(); i < height; i++) {
      prev[i] = head_;
    }
    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (nullptr), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since nullptr sorts after all
    // keys.  In the latter case the reader will use the new node.
    max_height_.store(height, std::memory_order_relaxed);
  }

  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }

這個函式最後還是有一些小技巧

這裡就是單連結串列的插入但是有兩個要注意的地方

  1. 這裡對於設定x的next指標並沒有用barrier操作,也就是說它的順序是任意的。
  2. 但是第二個對於prev[i]->next = x 這一操作這裡用了memory_order_release也就是說對於上一個操作必須在這個操作之前,這也是為什麼上一個操作可以不用barrier的原因
  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }

4. 跳錶實現之-迭代器與查詢

對於跳錶的遍歷leveldb專門定義了迭代器

SkipList<key,Comparator>::Iterator iter(&list)

下面是SkipList迭代器對應的一些函式

  class Iterator {
   public:
    // Initialize an iterator over the specified list.
    // The returned iterator is not valid.
    explicit Iterator(const SkipList* list);

    // 確保當前node不為空
    bool Valid() const;

    // 返回當前位置的key
    const Key& key() const;
   	// node_ = node_.Next(0)
    void Next();
    // node _ = node的前驅
    void Prev();
    // 查詢第一個 key >= target的node
    void Seek(const Key& target);

    // Position at the first entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    void SeekToFirst();
    // Position at the last entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    void SeekToLast();

   private:
    const SkipList* list_;
    Node* node_;
    // Intentionally copyable
  };

1. Prev和FindLessThan

Prev函式是讓node = node->prev也就是把node置為它的前驅。由於我們是單連結串列,所以這裡要找前驅實際上需要一個遍歷。

這裡我們用到了FindLessThan這個函式

  1. 還是從最高層出發
  2. 找到第一個next節點大於等於key的節點
  3. 返回這個節點即為所求
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindLessThan(const Key& key) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    assert(x == head_ || compare_(x->key, key) < 0);
    Node* next = x->Next(level);
    if (next == nullptr || compare_(next->key, key) >= 0) {
      if (level == 0) {
        return x;
      } else {
        // Switch to next list
        level--;
      }
    } else {
      x = next;
    }
  }
}

所以Prev就非常容易的實現了

inline void SkipList<Key, Comparator>::Iterator::Prev() {
  // Instead of using explicit "prev" links, we just search for the
  // last node that falls before key.
  assert(Valid());
  node_ = list_->FindLessThan(node_->key);
  if (node_ == list_->head_) {
    node_ = nullptr;
  }
}

2. 利用FindGreaterOrEqual實現Seek函式

template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {
  node_ = list_->FindGreaterOrEqual(target, nullptr);
}

因為FindGreaterOrEqual函式就是返回大於等於target的第一個節點。這就是seek函式所需要的

5. 跳錶實現之-雜七雜八

1. 隨機高度函式

  1. rnd_.Next()隨機生成一個數
  2. 然後對4取餘,也就是有1/4的概率高都增長
template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
  // Increase height with probability 1 in kBranching
  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxHeight);
  return height;
}

2. Contains函式

contains函式的功能是判斷當前的skiplist中是否有key值存在

  1. 利用了之前說過的FindGreaterOrEqual可以說是封裝的非常好了
template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::Contains(const Key& key) const {
  Node* x = FindGreaterOrEqual(key, nullptr);
  if (x != nullptr && Equal(key, x->key)) {
    return true;
  } else {
    return false;
  }
}

2. MemTable的Add操作

在看Add之前要先看一下,對於每一條資料是以什麼樣的格式放入table中的

1. MemTable的內部編碼引用自

image-20210708114254329

MemTable 中儲存的資料是 key 和 value 編碼成的一個字串,由四個部分組成:

  1. klength: 變長的 32 位整數(varint 的編碼),表示 internal key 的長度。
  2. internal key: 長度為 klength 的字串。
  3. vlength: 變長的 32 位整數(varint 的編碼),表示 value 的長度。
  4. value: 長度為 vlength 的字串。

對應在程式碼裡就是

這裡的編碼主要有兩種

  1. 第一種是對klength和vlength的EncodeVarint32編碼
  2. 第二種是對sequcenceNumber + type的EncodeFixed64編碼
  3. 對於key和value的data部分則直接儲存
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8; // +8 = (sequence 7 bytes + 1 bytes type)
const size_t encoded_len = VarintLength(internal_key_size) +
                           internal_key_size + VarintLength(val_size) +
                           val_size;
char* buf = arena_.Allocate(encoded_len); // 分配記憶體 大小為encoded_len 
char* p = EncodeVarint32(buf, internal_key_size); //
std::memcpy(p, key.data(), key_size);
p += key_size;
EncodeFixed64(p, (s << 8) | type);
p += 8;
p = EncodeVarint32(p, val_size);
std::memcpy(p, value.data(), val_size);

EncoderVarint32函式

  1. 首先把字元陣列轉為uint8_t型別的陣列,這樣我們接下來才能表示數字。可以看到,在判斷語句中對數字所需要的位數進行了判斷,如果可以用7個位來表示,那就直接賦值,如果可以用14個位來表示(剩下兩位要用來表示數字是否結束),那就把數字的前7位放到dst[0]中,在把剩餘的7位右移,放到dst[1]中去。如果可以用21個位來表示,那麼前七個位放到dst[0],中間七位放到dst[1],最後七位放到dst[2]。
  2. 舉例說來,假設我們的dst是一個長度位5的字元陣列,value等於129。那麼dst在記憶體空間中是這樣表示的dst[4] dst[3] dst[2] dst[1] dst[0],每個佔據8位,value的二進位制表示為0000 0000 1000 0001。ptr首先指向dst[0],現在我們通過(ptr++) = 128 | value把最後七位0000 0001放到dst[0]中去,此處發生了截斷,因為ptr是uint8_t型別,而value是uint32_t,所以只會把value的最後七位放到dst[0]中去,放完之後dst[0]等於1000 0001,注意此處最高位的1並不是數字,而是代表數字沒有結束,要繼續操作。接下來我們把中間七位00 0000 1放到dst[1]中去,首先右移七位得到0000 0001,然後直接賦值即可。

版權宣告:本文為CSDN博主「饅頭2870」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/sjc2870/article/details/112342573

char* EncodeVarint32(char* dst, uint32_t v) {
  // Operate on characters as unsigneds
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  static const int B = 128;
  if (v < (1 << 7)) {
    *(ptr++) = v;
  } else if (v < (1 << 14)) {
    *(ptr++) = v | B;
    *(ptr++) = v >> 7;
  } else if (v < (1 << 21)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = v >> 14;
  } else if (v < (1 << 28)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = v >> 21;
  } else {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = (v >> 21) | B;
    *(ptr++) = v >> 28;
  }
  return reinterpret_cast<char*>(ptr);
}

EncodeFixed64函式

這個對於64位的固定編碼就比較通俗易懂了。

依次取低8位放到buffer[i]中

inline void EncodeFixed64(char* dst, uint64_t value) {
  uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);

  // Recent clang and gcc optimize this to a single mov / str instruction.
  buffer[0] = static_cast<uint8_t>(value);
  buffer[1] = static_cast<uint8_t>(value >> 8);
  buffer[2] = static_cast<uint8_t>(value >> 16);
  buffer[3] = static_cast<uint8_t>(value >> 24);
  buffer[4] = static_cast<uint8_t>(value >> 32);
  buffer[5] = static_cast<uint8_t>(value >> 40);
  buffer[6] = static_cast<uint8_t>(value >> 48);
  buffer[7] = static_cast<uint8_t>(value >> 56);
}

EncodeVarint64

emmm這個程式碼等價於上面的EncoderVarint32函式只不過比上面的更簡潔更大佬

char* EncodeVarint64(char* dst, uint64_t v) {
  static const int B = 128;
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  while (v >= B) {
    *(ptr++) = v | B;
    v >>= 7;
  }
  *(ptr++) = static_cast<uint8_t>(v);
  return reinterpret_cast<char*>(ptr);
}

2. Add

隨後的add就是把我們編碼之後的資料insert到skiplist中就可以了

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
                   const Slice& value) {
  // Format of an entry is concatenation of:
  //  key_size     : varint32 of internal_key.size()
  //  key bytes    : char[internal_key.size()]
  //  value_size   : varint32 of value.size()
  //  value bytes  : char[value.size()]
  size_t key_size = key.size();
  size_t val_size = value.size();
  size_t internal_key_size = key_size + 8;
  const size_t encoded_len = VarintLength(internal_key_size) +
                             internal_key_size + VarintLength(val_size) +
                             val_size;
  char* buf = arena_.Allocate(encoded_len);
  char* p = EncodeVarint32(buf, internal_key_size);
  std::memcpy(p, key.data(), key_size);
  p += key_size;
  EncodeFixed64(p, (s << 8) | type);
  p += 8;
  p = EncodeVarint32(p, val_size);
  std::memcpy(p, value.data(), val_size);
  assert(p + val_size == buf + encoded_len);
  table_.Insert(buf);
}

這裡有要注意的就是SequenceNumber

快照就是根據SequenceNumber的值來實現的,有了這個數字,就可以保證,讀到的值就是一定的。

 key       value   sequencenumber

 a            1        3

 a            2        4

 a            3        5

如果指定了sequencenumber=4,那麼讀到a的值就是2,而不是3;

3. MemTable的Get操作

1. db_impl.cc中的Get操作

MemTable的get操作是在整個leveldb讀操作中的一種,因為會先去看MemTable中是否有資料,這裡先從整個leveldb的get操作入手。來分析整體的呼叫過程

  1. 先忽略掉這裡的快照操作,如果在options沒有指定快照的話,那麼就回去獲取最新的sequence number
  2. 獲取互斥鎖
  3. 獲取本次讀操作的 Sequence Number:如果 ReadOptions 引數的 snapshot 不為空,則使用這個 snapshot 的 sequence number;否則,預設使用 LastSequence(LastSequence 會在每次寫操作後更新)。
  4. 對於MemTable, Immutable Memtable 和 Current Version(SSTable) 增加引用計數,避免在讀取過程中被後臺執行緒進行 compaction 時“垃圾回收”了。
  5. 釋放互斥鎖,不同執行緒的請求可以併發執行讀取操作,上面加鎖是因為要修改引用次數,是一個寫操作。
  6. 構造 LookupKey
  7. 從 MemTable 查詢
Status DBImpl::Get(const ReadOptions& options, const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  SequenceNumber snapshot;
  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
  } else {
    snapshot = versions_->LastSequence();
  }
  MemTable* mem = mem_;
  MemTable* imm = imm_;
  Version* current = versions_->current();
  mem->Ref();
  if (imm != nullptr) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats;
  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) {
      // Done
    }
    mutex_.Lock();
  }

  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction();
  }
  mem->Unref();
  if (imm != nullptr) imm->Unref();
  current->Unref();
  return s;
}

2.LookupKey的實現

通過user_key和sequence構造leveldb:LookupKey用於LevelDB內部查詢的介面

image-20210708190708879

  1. klength 的型別是 varint32,儲存的是user_key + tag的長度
  2. userkey 就是 Get 介面傳入的 key 引數。
  3. tag 是 7 位元組 sequence number 和 1 位元組 value type
  4. 一個 varint32 最多需要 5 個位元組,tag 需要 8 位元組。所以一個 LookupKey 最多需要分配[ usize + 13 位元組]記憶體(usize 是 userkey 的 size)。
LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
  size_t usize = user_key.size();
  size_t needed = usize + 13;  // A conservative estimate
  char* dst;
  if (needed <= sizeof(space_)) {
    dst = space_;
  } else {
    dst = new char[needed];
  }
  start_ = dst;
  dst = EncodeVarint32(dst, usize + 8);
  kstart_ = dst;
  std::memcpy(dst, user_key.data(), usize);
  dst += usize;
  EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
  dst += 8;
  end_ = dst;
}

這裡除了上面提到過的兩種編碼方式還多了一個PackSequenceAndType

其實就是對sequenceNumber << 8 | Type 的一個封裝

static uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
  assert(seq <= kMaxSequenceNumber);
  assert(t <= kValueTypeForSeek);
  return (seq << 8) | t;
}

3.對Memtable的get操作

最後再來看memtable中的get操作

  1. 這裡其實就是利用迭代器在skipList中找到對應的key
  2. 然後就是對編碼資訊的解碼過程
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
  Slice memkey = key.memtable_key();
  Table::Iterator iter(&table_);
  iter.Seek(memkey.data());
  if (iter.Valid()) {
    const char* entry = iter.key();
    uint32_t key_length;
    const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);
    if (comparator_.comparator.user_comparator()->Compare(
            Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
      // Correct user key
      const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
      switch (static_cast<ValueType>(tag & 0xff)) {
        case kTypeValue: {
          Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
          value->assign(v.data(), v.size());
          return true;
        }
        case kTypeDeletion:
          *s = Status::NotFound(Slice());
          return true;
      }
    }
  }
  return false;
}

程式碼過程分析

  1. key.memtable_key()

    實際上就是把剛才建立的lookupkey返回

      // Return a key suitable for lookup in a MemTable.
      Slice memtable_key() const { return Slice(start_, end_ - start_); }
    
  2. GetVarint32Ptr

    這裡的解碼就像編碼一樣有兩種情況。

    • 對應了要解碼的數字小於等於127 也就是(result & 128) == 0) 的時候直接返回
    inline const char* GetVarint32Ptr(const char* p, const char* limit,
                                      uint32_t* value) {
      if (p < limit) {
        uint32_t result = *(reinterpret_cast<const uint8_t*>(p));
        if ((result & 128) == 0) {
          *value = result;
          return p + 1;
        }
      }
      return GetVarint32PtrFallback(p, limit, value);
    }
    
    
    • 否則的話呼叫GetVarint32PtrFallback

    以129為例子

    129會這樣來儲存 |0000 0001| 1000 0001|

    剛開始 *p = 10000001

    所以 *p & 128 == 1隨後就會執行result |= ((byte & 127) << shift); result = 1

    p++之後 *p = 0000 0100

    隨後byte無法進入if中而進入esle中

    result |= (byte << shift) = 129

    隨後返回即可

    const char* GetVarint32PtrFallback(const char* p, const char* limit,
                                       uint32_t* value) {
      uint32_t result = 0;
      for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) {
        uint32_t byte = *(reinterpret_cast<const uint8_t*>(p));
        p++;
        if (byte & 128) {
          // More bytes are present
          result |= ((byte & 127) << shift);
        } else {
          result |= (byte << shift);
          *value = result;
          return reinterpret_cast<const char*>(p);
        }
      }
      return nullptr;
    
  3. value type

    如果對應的型別是 kTypeDeletion,表示該 key/value 已經從 Memtable 中刪除,這時候將 NotFound 儲存在狀態碼中,並且返回。

3. MemTable的刪除操作

追蹤一次刪除操作的發生。

DB:delete -> writeBatch::Delete()

這裡的刪除並不是真的刪除元素,而是新增一條刪除記錄來實現的。真正的刪除操作會在後臺程式中完成,也是為了減少io增大效率。

void WriteBatch::Delete(const Slice& key) {
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  rep_.push_back(static_cast<char>(kTypeDeletion));
  PutLengthPrefixedSlice(&rep_, key);
}

1. 首先這裡會新增一個kTypeDeletion + key的字首

然後在WriteBatch::Iterate

Slice input(rep_);
if (input.size() < kHeader) {
  return Status::Corruption("malformed WriteBatch (too small)");
}
input.remove_prefix(kHeader);
Slice key, value;
int found = 0;
while (!input.empty()) {
  found++;
  char tag = input[0];
  input.remove_prefix(1);
  switch (tag) {
		//.......
    //檢測到了key的字首有kTypeDeletion隨後呼叫
    // handler->Delete去skipList中標記key為刪除
    case kTypeDeletion:
      if (GetLengthPrefixedSlice(&input, &key)) {
        handler->Delete(key);

2. 在 handler->Delete

實際上這個函式呼叫了mem_->Add函式.指定其中的keyType = KTypeDeletion

void Delete(const Slice& key) override {
    mem_->Add(sequence_, kTypeDeletion, key, Slice());
    sequence_++;
  }

這樣當下一次獲取當前key的資料的時候,最新的sequenceNumber已經標記它為刪除

4. MemTable-->ImmuTable的轉變

image-20210717204446992

找了半天發現是在這裡實現的MemTable -> ImmuTable的轉變。具體的邏輯會在後面進行分析,這裡的內容已經足夠多了。

image-20210717204646817

但還是配圖,是在這裡實現了轉變

imm_ = mem_ ; // 把原來的memtable -> immutable 
mem_ = new MemTable() // 建立一個新的memTable

5. 記憶體池Arena

最後花點時間在MemTable用到的記憶體池上。

首先看Arena類的四個私有變數

// Allocation state
  char* alloc_ptr_;
  size_t alloc_bytes_remaining_;

  // Array of new[] allocated memory blocks
  std::vector<char*> blocks_;

  // Total memory usage of the arena.
  //
  // TODO(costan): This member is accessed via atomics, but the others are
  //               accessed without any locking. Is this OK?
  std::atomic<size_t> memory_usage_;

圖片引自於

image-20210717210100880

其中如上圖所示,blocks_是一個包含指向各個塊的char *陣列,而alloc_ptr_則表示了當前塊的未使用記憶體區域的起始地址。同時用alloc_bytes_remaining_來表示當前塊還剩多少記憶體是沒有被使用的

而我們整個的分配過程是從Allocate(size_t bytes)函式開始的

  1. 如果要分配的記憶體大小 < 當前塊剩餘記憶體大小的話則直接分配
  2. 否則要呼叫AllocateFallback
inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don't need
  // them for our internal use).
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}

AllocateFallback(bytes)

如果當前塊的剩餘記憶體區域不足以進行分配的話則需要呼叫這個函式

static const int kBlockSize = 4096;

  1. 如果需要分配的大小 > 1KB的話則單獨申請一個bytes的快分配給它
  2. 否則就是申請一個正常大小的塊4kb然後在新的塊分配他
char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    // Object is more than a quarter of our block size.  Allocate it separately
    // to avoid wasting too much space in leftover bytes.
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // We waste the remaining space in the current block.
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}

AllocateNewBlock(block_bytes)函式

  1. 分配一個制定大小的新塊,給它加入blocks_陣列中
  2. 在總的使用記憶體變數中加入新分配的記憶體
char* Arena::AllocateNewBlock(size_t block_bytes) {
  char* result = new char[block_bytes];
  blocks_.push_back(result);
  memory_usage_.fetch_add(block_bytes + sizeof(char*),
                          std::memory_order_relaxed);
  return result;

最後看AllocateAligned(size_t bytes) 函式

這個是類allocate函式只不過制定了對奇要求。

  1. 第一行是獲取當前系統的對其規格。一般來講x86-64機器的(void*)大小就是8bytes;
  2. 唯一的區別就是needed = bytes + slop slop是為了對奇所需要多加的bytes
char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
  static_assert((align & (align - 1)) == 0,
                "Pointer size should be a power of 2");
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);
  size_t needed = bytes + slop;
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop;
    alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
    // AllocateFallback always returned aligned memory
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
  return result;
}

關於Arena的記憶體釋放

可以發現整個Arena中沒有類free釋放,它的整個釋放都在整個物件的解構函式完成

Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }

而整個Arena的解構函式會在memTable物件被釋放的時候進行釋放。

相關文章