Memcached slab 分配策略

Memcached 自帶了一個內存分配模塊slab,自己在用戶層實現了內存的分配,而不是完全依賴於系統的 malloc。這篇文章,來看看 Memcached slab 內存分配算法是怎麼做的。

一個內存分配算法要考慮算法的效率,管理內存所佔的空間和內存碎片的問題。這個是三個衡量點往往不能個個都得到滿足,每個實現都會各有所長。slab 能較好的規避內存碎片的問題,但也帶來了一定的內存浪費,算法的效率還不錯。

Memcached slab 概述

在 Memcached 中,爲鍵值對分配空間的時候都會調用 do_item_alloc() 函數,真正設計 slab 的是slabs_alloc() 這個函數:

void *slabs_alloc(size_t size, unsigned int id);

size 是所需分配空間的實際大小,id 是這個空間大小所對應的數量級。

slab class

slab 爲空間的大小劃分了數量級。在 memecached 初始化的時候可以設置 chrunk 和 factor 屬性,前者是一個底數,後者是一個因子,前一個數量級乘於因子已得到新的數量級,依次可以推算下一級的數量級。

來看看內存管理的結構體 slabclass_t:

typedef struct {
    // 每個內存塊大小
    unsigned int size; /* sizes of items */
    // 每個slab 內存塊的數量
    unsigned int perslab; /* how many items per slab */
    // 空閒的內存塊會組成一個鏈表
    void *slots; /* list of item ptrs */
    // 當前空閒內存塊的數量
    unsigned int sl_curr; /* total free items in list */
    // slab 數量
    unsigned int slabs; /* how many slabs were allocated for this class */
    // slab 指針
    void **slab_list; /* array of slab pointers */
    unsigned int list_size; /* size of prev array */
    ......
} slabclass_t;

現在對於某一級別的 slab 有如下印象圖:

對於不同的 class 有如下印象圖:

內存分配的過程

來看看 slab 內存分配入口函數做了什麼?

void *slabs_alloc(size_t size, unsigned int id) {
    void *ret;
    // 每次內存的分配都需要加鎖
    pthread_mutex_lock(&slabs_lock);
    ret = do_slabs_alloc(size, id);
    pthread_mutex_unlock(&slabs_lock);
    return ret;
}

do_slabs_alloc() 實際上會先檢測是否有空閒的內存塊,有則返回空閒的內存塊;否則,會調用do_slabs_newslab() 分配新的內存。

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
    item *it = NULL;
    // 所需分配空間的數量級別不合法
    if (id < POWER_SMALLEST || id > power_largest) {
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0);
        return NULL;
    }
    p = &slabclass[id];
    assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0);
    // 如果指定的slab 內還有空閒的內存塊,返回空閒的內存塊,否則調用
    // do_slabs_newslab()
    // do_slabs_newslab() 爲指定的slab 分配更多的空間
    if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) {
        /* We don't have more memory available */
        ret = NULL;
    } else if (p->sl_curr != 0) {
        /* return off our freelist */
        it = (item *)p->slots;
        p->slots = it->next;
    if (it->next) it->next->prev = 0;
        p->sl_curr--;
        ret = (void *)it;
    }
    ......
    return ret;
}

我們來看看 do_slabs_newslab() 是怎麼做的:首先會看 slab_list 是否已經滿了,如果滿 了則 resize slab_list 並分配空間,將新分配的空間初始化後切割插入到空閒鏈表中。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    // 計算需要分配內存的大小
    int len = settings.slab_reassign ? settings.item_size_max
    : p->size * p->perslab;
    char *ptr;
    // 擴大slab_list,並分配內存
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
            ((ptr = memory_allocate((size_t)len)) == 0)) {
        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }
    // 將新分配的內存初始化,並切割插入到空閒鏈表中
    memset(ptr, 0, (size_t)len);
    split_slab_page_into_freelist(ptr, id);
    // 調整slab_list 指針指向新分配的空間
    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);
    return 1;
}

do_slabs_newslab() 之前:

do_slabs_newslab() 之後:

slab 能較好的規避內存碎片的問題,但也帶來了一定的內存浪費,算法的效率還不錯。現在能夠較好的理解這一句話。因爲 slab 內存分配算法預先分配了一大塊的連續緊湊的內存空間,只一點能將內存的使用都限定在緊湊連續的空間內;但很明顯它會帶來一定的浪費,因爲每個 slab class 內的每個內存塊大小都是固定的,數據的大小必須小於等於內存塊的大小。

lru 機制

Memcached slab 還有一個超時淘汰的機制,當發現某個 slab class 內無空間可分配的時候,並不是立即去像上面所說的一樣去擴展空間,而是嘗試從已經被使用的內存塊中尋找是否有已經超時的塊,如果超時了,則原有的數據會被刪除,這個內存塊被作爲結果內存分配的結果。

那如何快速找到這個塊呢?對於某個 slab class,所有已使用和空閒的內存塊都會被組織成一個鏈表,

static item *heads[LARGEST_ID];
static item *tails[LARGEST_ID];

這兩個全局變量就保存這些鏈表的頭指針和尾指針。對於新的數據插入,會更新 heads[classid],對於超時被剔除的數據刪除操作,會更新 tails[classid]。

下面圖解上述的過程:

轉自:http://wiki.jikexueyuan.com/project/redis/slab.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章