BlueStore原始碼分析之Stupid分配器
前言
前面介紹了
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)。
簡單的空間分配圖如下:
詳細的空間分配流程圖如下:
空間回收
空間釋放的函式定義如下:
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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- BlueStore原始碼分析之FreelistManager原始碼
- BlueStore原始碼分析之事物狀態機原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 原始碼分析之 HashMap原始碼HashMap
- 原始碼分析之 LinkedList原始碼
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- 死磕 jdk原始碼之HashMap原始碼分析JDK原始碼HashMap
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- lodash原始碼分析之isArguments原始碼
- Envoy原始碼分析之Dispatcher原始碼
- Fresco原始碼分析之DraweeView原始碼View
- Netty原始碼分析之LengthFieldBasedFrameDecoderNetty原始碼
- RecyclerView之SnapHelper原始碼分析View原始碼
- tornado 原始碼之 coroutine 分析原始碼
- lodash原始碼分析之isObjectLike原始碼Object
- OpenGL 之 GPUImage 原始碼分析GPUUI原始碼
- jdk原始碼分析之TreeMapJDK原始碼
- 原始碼分析Kafka之Producer原始碼Kafka
- DRF之Response原始碼分析原始碼
- Spring AOP之原始碼分析Spring原始碼
- JUC之ReentrantLock原始碼分析ReentrantLock原始碼
- JUC之CountDownLatch原始碼分析CountDownLatch原始碼
- Fresco原始碼分析之Hierarchy原始碼
- Dubbo之SPI原始碼分析原始碼
- MongoDB原始碼分析之MongosXFMongoDB原始碼
- Flutter原始碼分析之InheritedWidgetFlutter原始碼
- Redux原始碼分析之createStoreRedux原始碼
- Java原始碼分析:Guava之不可變集合ImmutableMap的原始碼分析Java原始碼Guava
- Spring原始碼分析——spring原始碼之obtainFreshBeanFactory()介紹Spring原始碼AIBean
- spring原始碼分析之freemarker整合Spring原始碼
- netty原始碼分析之pipeline(二)Netty原始碼
- Spring原始碼分析之IoC(一)Spring原始碼
- 原始碼分析 @angular/cdk 之 Portal原始碼Angular
- netty原始碼分析之pipeline(一)Netty原始碼
- Spring原始碼分析之IoC(二)Spring原始碼
- SpringMVC之原始碼分析--ViewResolver(五)SpringMVC原始碼View