BlueStore原始碼分析之Stupid分配器

java06051515發表於2020-02-05

前言

前面介紹了 BlueStore的BitMap分配器,我們知道新版本的 Bitmap分配器的優勢在於 使用連續的記憶體空間從而儘可能更多的命中CPU Cache以提高分配器效能。在這裡我們瞭解一下基於區間樹的 Stupid分配器(類似於Linux Buddy記憶體管理演算法),並對比分析一下其優劣。

目錄

夥伴演算法

Linux記憶體管理演算法為了能夠快速響應請求,儘可能的提高記憶體利用率同時減少外部記憶體碎片,引入了夥伴系統演算法 Buddy-System。該演算法將所有的空閒頁分組為11個連結串列,每個連結串列分別包含 1、2、4、8、16、32、64、128、256、512、1024個連續的頁框塊,每個頁框塊的第一個記憶體頁的實體地址是該塊大小的整數倍。 夥伴的特點是:兩個塊大小相同、兩個塊地址連續、第一塊的第一個頁框的實體地址是兩個塊總大小的整數倍(同屬於一個大塊,第1塊和第2塊是夥伴,第3塊和第4塊是夥伴,但是第2塊和第3塊不是夥伴)。具體記憶體分配和記憶體釋放可自行Google。

優點:

  • 較好的解決外部碎片問題,不能完全解決。
  • 針對大記憶體分配設計,可以快速的分配連續的記憶體。

缺點:

  • 合併的要求過於嚴格,只能是滿足夥伴關係的塊才可以合併。
  • 一塊連續的記憶體中僅有一個頁面被佔用,就導致整個記憶體不具備合併的條件。
  • 演算法頁面連續性差,DMA申請大塊連續實體記憶體空間可能失敗,此時需要 CMA(Contiguous Memory Allocator, 連續記憶體分配器)。
  • 浪費空間,可以透過slab、kmem_cache等解決。

資料結構

Stupid分配器使用了區間樹組織資料結構,高效管理 Extent(offset, length)

class StupidAllocator : public Allocator {
    CephContext* cct;
    // 分配空間用的互斥鎖
    std::mutex lock;
    // 空閒空間總大小
    int64_t num_free;
    // 最後一次分配空間的位置
    uint64_t last_alloc;
    // 區間樹陣列,初始化的時候,free陣列的長度為10,即有十顆區間樹
    std::vector<interval_set_t> free;
    // extent: offset, length
    typedef mempool::bluestore_alloc::pool_allocator<
        pair<const uint64_t, uint64_t>>
        allocator_t;
    // 有序的 btree map,按順存放extent。
    typedef btree::btree_map<uint64_t, uint64_t, std::less<uint64_t>,
                             allocator_t> interval_set_map_t;
    // 區間樹,主要的操作有 insert、erase等。
    typedef interval_set<uint64_t, interval_set_map_t> interval_set_t;
};

每顆區間樹的下標為 index(0-9),index(1-9)表示的空間大小為: [2^(index-1) * bdev_block_size, 2^(index) * bdev_block_size)

  • free[0]: [0, 4k)
  • free[1]: [4k, 8k)
  • free[2]: [8k, 16k)
  • free[3]: [16k, 32k)
  • free[4]: [32k, 64k)
  • free[5]: [64k, 128k)
  • free[6]: [128k, 256k)
  • free[7]: [256k, 512k)
  • free[8]: [512k, 1024k)
  • free[9]: [1024k, 2048k)

初始化

初始化Stupid分配器後,呼叫者會向Allocator中加入或者刪除空閒空間。

// 增加空閒空間
void StupidAllocator::init_add_free(uint64_t offset, uint64_t length) {
    std::lock_guard<std::mutex> l(lock);
    // 向 free 中插入空閒空間
    _insert_free(offset, length);
    // 更新空閒空間大小
    num_free += length;
}
// 刪除空閒空間
void StupidAllocator::init_rm_free(uint64_t offset, uint64_t length)
{
    std::lock_guard<std::mutex> l(lock);
    interval_set_t rm;
    rm.insert(offset, length);
    for (unsigned i = 0; i < free.size() && !rm.empty(); ++i) {
        interval_set_t overlap;
        overlap.intersection_of(rm, free[i]);
        // 刪除相應空間
        if (!overlap.empty()) {
            free[i].subtract(overlap);
            rm.subtract(overlap);
        }
    }
    num_free -= length; // 更新可用空間
}

插入刪除

區間樹實現程式碼:

insert函式程式碼:

#L445

erase函式程式碼:

#L516

最核心的實現是向區間樹中插入以及刪除區間,程式碼如下:

區間樹插入Extent

// 根據區間的長度,選取將要存放的區間樹,長度越大,bin值越大。
unsigned StupidAllocator::_choose_bin(uint64_t orig_len)
{
    uint64_t len = orig_len / cct->_conf->bdev_block_size;
    // cbits = (sizeof(v) * 8) - __builtin_clzll(v)
    // __builtin_clzll 返回前置的0的個數
    // cbits 結果是最高位1的下標(從0開始),len越大,值越大
    int bin = std::min(int)cbits(len), (int)free.size() - 1);
    return bin;
}
void StupidAllocator::_insert_free(uint64_t off, uint64_t len)
{
    // 計算該段空閒空間屬於哪個區間樹
    unsigned bin = _choose_bin(len);
    while (true) {
        // 空閒空間插入區間樹
        free[bin].insert(off, len, &off, &len);
        unsigned newbin = _choose_bin(len);
        if (newbin == bin)
            break;
        // 插入資料後,可能合併區間,導致區間長度增大,可能要調整bin,此時需要將舊的刪除,然後插入新的bin
        // 區間合併有兩種情況:一是合併在原有區間前面;而是合併在原有區間後面。
        free[bin].erase(off, len);
        bin = newbin;
    }
}

回顧第一節夥伴演算法, 兩種合併的方式是有區別的

  • 夥伴演算法要求比較嚴格,參考第一節。
  • Stupid Extent合併比較鬆散,只要滿足兩個Extent空間連續就可以。

區間樹刪除Extent

區間樹刪除Extent比較簡單,在原來Extent刪除傳入的Extent,然後計算最終Extent是否落入其他區間樹,如果落入則從此區間樹刪除,加入新的區間樹。

空間分配

空間分配的函式定義如下:

allocate(uint64_t want_size, uint64_t alloc_unit, 
        uint64_t max_alloc_size, int64_t hint,PExtentVector* extents);
allocate_int(uint64_t want_size, uint64_t alloc_unit, int64_t hint,
                         uint64_t* offset, uint32_t* length)

其中 hint是一個很重要的引數,表示分配的起始地址要儘量大於hint的值。

核心流程為4個2層for迴圈大致為:優先從hint地址依次向高階區間樹開始分配長度大於等於 want_size的連續空間,如果沒有,則優先從hint地址依次向低階區間樹開始分配長度大於等於 alloc_unit的連續空間(長度會大於alloc_unit)。

簡單的空間分配圖如下:


BlueStore原始碼分析之Stupid分配器


詳細的空間分配流程圖如下:

BlueStore原始碼分析之Stupid分配器


空間回收

空間釋放的函式定義如下:

release(const interval_set<uint64_t> &release_set)

流程很簡單,先加鎖,然後迴圈呼叫 _insert_free插入到對應區間樹裡面,會涉及到相鄰空閒空間的合併,但是會導致分配空間碎片的問題。

優劣分析

CPU Cache

Stupid底層使用BtreeMap來儲存一系列的Extent,記憶體不一定是連續的,同時在分配空間遍歷區間樹時,雖然區間樹裡面的Extent是有序的,但是由於記憶體不一定是連續或者相鄰的兩個Extent記憶體跨度可能很大,都會導致CPU-Cache預讀不到下一個Extent,從而不能很好的利用CPU-Cache。

Bitmap分配器在BlueStore初始化時就初始化好了3層,而且大小是固定的,同時分配空間是依次順序分配,從而可以充分的利用CPU-Cache的功能。從而提高分配器的效能。

偽空間碎片

基於Extent的Stupid分配器存在偽空間碎片( 物理空間是連續的,但是分配器中卻不連續)問題:

一個24K的連續空間,經過6次4K分配和亂序的6次4K釋放後,可能會變成 8K + 4K + 8K + 4K四塊空間。

其中兩個4K的區間由於和周邊塊大小一樣,所以落到不同的區間樹中,導致很難被合併,24K的連續空間變成了四塊不連續空間。

Bitmap分配器由於初始化時就分配好了3層所有記憶體,而且3層都是有序的的同時分配空間是順序遍歷的,在釋放空間的時候設定相應位就可以,不影響連續性,所以不存在這個問題。

據Bitmap作者的 效能對比實驗來看,Bitmap分配器要好於Stupid,等Bitmap穩定後,可以設定BlueStore的預設分配器為Bitmap。

作者:史明亞


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2674588/,如需轉載,請註明出處,否則將追究法律責任。

相關文章