1. 程式人生 > >Memcached原始碼分析之儲存機制Slabs(7)

Memcached原始碼分析之儲存機制Slabs(7)

文章列表:

《Memcached原始碼分析 - Memcached原始碼分析之總結篇(8)》

前言

這一章我們重點講解Memcached的儲存機制Slabs。Memcached儲存Item的程式碼都是在slabs.c中來實現的。

在解讀這一章前,我們必須先了解幾個概念。

Item 快取資料儲存的基本單元

1. Item是Memcached儲存的最小單位

2. 每一個快取都會有自己的一個Item資料結構

3. Item主要儲存快取的key、value、key的長度、value的長度、快取的時間等資訊。

4. HashTable和LRU連結串列結構都是依賴Item結構中的元素的。

5. 在Memcached中,Item扮演著重要的角色。

//item的具體結構
typedef struct _stritem {
    //記錄下一個item的地址,主要用於LRU鏈和freelist鏈
    struct _stritem *next;
    //記錄下一個item的地址,主要用於LRU鏈和freelist鏈
    struct _stritem *prev;
    //記錄HashTable的下一個Item的地址
    struct _stritem *h_next;
    //最近訪問的時間,只有set/add/replace等操作才會更新這個欄位
    //當執行flush命令的時候,需要用這個時間和執行flush命令的時間相比較,來判斷是否失效
    rel_time_t      time;       /* least recent access */
    //快取的過期時間。設定為0的時候,則永久有效。
    //如果Memcached不能分配新的item的時候,設定為0的item也有可能被LRU淘汰
    rel_time_t      exptime;    /* expire time */
    //value資料大小
    int             nbytes;     /* size of data */
    //引用的次數。通過這個引用的次數,可以判斷item是否被其它的執行緒在操作中。
    //也可以通過refcount來判斷當前的item是否可以被刪除,只有refcount -1 = 0的時候才能被刪除
    unsigned short  refcount;
    uint8_t         nsuffix;    /* length of flags-and-length string */
    uint8_t         it_flags;   /* ITEM_* above */
    //slabs_class的ID。
    uint8_t         slabs_clsid;/* which slab class we're in */
    uint8_t         nkey;       /* key length, w/terminating null and padding */
    /* this odd type prevents type-punning issues when we do
     * the little shuffle to save space when not using CAS. */
    //資料儲存結構
    union {
        uint64_t cas;
        char end;
    } data[];
    /* if it_flags & ITEM_CAS we have 8 bytes CAS */
    /* then null-terminated key */
    /* then " flags length\r\n" (no terminating null) */
    /* then data with terminating \r\n (no terminating null; it's binary!) */
} item;

slabclass 劃分資料空間

1. Memcached在啟動的時候,會初始化一個slabclass陣列,該陣列用於儲存最大200個slabclass_t的資料結構體。

2. Memcached並不會將所有大小的資料都會放置在一起,而是預先將資料空間劃分為一系列的slabclass_t。

3. 每個slabclass_t,都只儲存一定大小範圍的資料。slabclass陣列中,前一個slabclass_t可以儲存的資料大小要小於下一個slabclass_t結構可以儲存的資料大小。

4. 例如:slabclass[3]只儲存大小介於120 (slabclass[2]的最大值)到 150 bytes的資料。如果一個數據大小為134byte將被分配到slabclass[3]中。

5. memcached預設情況下下一個slabclass_t儲存資料的最大值為前一個的1.25倍(settings.factor),這個可以通過修 改-f引數來修改增長比例。

//slabclass的結構
typedef struct {
	//當前的slabclass儲存最大多大的item
    unsigned int size;
    //每一個slab上可以儲存多少個item.每個slab大小為1M, 可以儲存的item個數根據size決定。
    unsigned int perslab;

    //當前slabclass的(空閒item列表)freelist 的連結串列頭部地址
    //freelist的連結串列是通過item結構中的item->next和item->prev連建立連結串列結構關係
    void *slots;           /* list of item ptrs */
    //當前總共剩餘多少個空閒的item
    //當sl_curr=0的時候,說明已經沒有空閒的item,需要分配一個新的slab(每個1M,可以切割成N多個Item結構)
    unsigned int sl_curr;   /* total free items in list */

    //總共分配多少個slabs
    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 */

    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    //總共請求的總bytes
    size_t requested; /* The number of requested bytes */
} slabclass_t;
//定義一個slabclass陣列,用於儲存最大200個的slabclass_t的結構。
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

slab 記憶體分配單位

1. Memcached的記憶體分配是以slab為單位的。預設情況下,每個slab大小為1M。

2. slabclass陣列初始化的時候,每個slabclass_t都會分配一個1M大小的slab。

3. 當某個slabclass_t結構上的記憶體不夠的時候(freelist空閒列表為空),則會分配一個slab給這個slabclass_t結構。

4. 一旦slab分配後,不可回收。

5. slab會被切分為N個小的記憶體塊,這個小的記憶體塊的大小取決於slabclass_t結構上的size的大小。例如slabclass[0]上的size為103,則每個小的記憶體塊大小為103byte。

6. 這些被切割的小的記憶體塊,主要用來儲存item。但是,儲存的item,可能會比切割出來的記憶體塊會小。因為這是為了防止記憶體碎片,雖然有一些記憶體的浪費

slabclass和slab、item以及free list之間的關係:


通過item的size來選擇slab_class的資料儲存空間:


儲存機制的原始碼分析

slabs_clsid - 查詢slabclass的ID

slabs_clsid方法,主要通過item的長度來查詢應該適合存放到哪個slabsclass_t上面。

//通過item的size,選擇當前的item適合放在哪個slab class中
unsigned int slabs_clsid(const size_t size) {
    int res = POWER_SMALLEST; //從id = 1開始查詢

    //slabclass這個結構上的size會儲存該class適合多大的item儲存
    //例如
    //slabclass[0] 儲存96byte
    //slabclass[1] 儲存120byte
    //slabclass[2] 儲存150byte
    //則,如果儲存的item等於109byte,則儲存在slabclass[1]上
    if (size == 0)
        return 0;
    while (size > slabclass[res].size)
        if (res++ == power_largest)     /* won't fit in the biggest slab */
            return 0;
    return res;
}

slabs_init - slabclass的初始化

slabs_init方法主要用於初始化slabclass陣列結構。

//slabclass初始化
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size;

    mem_limit = limit;

    //這邊是否初始化的時候,就給每一個slabclass_t結構分配一個slab記憶體塊
    //預設都會分配
    if (prealloc) {
        /* Allocate everything in a big chunk with malloc */
        mem_base = malloc(mem_limit);
        if (mem_base != NULL) {
            mem_current = mem_base;
            mem_avail = mem_limit;
        } else {
            fprintf(stderr, "Warning: Failed to allocate requested memory in"
                    " one large chunk.\nWill allocate in smaller chunks\n");
        }
    }

    memset(slabclass, 0, sizeof(slabclass));
    //factor 預設等於1.25 ,也就是說前一個slabclass允許儲存96byte大小的資料,
    //則下一個slabclass可以儲存120byte
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);

        //每個slabclass[i]儲存最大多大的item
        slabclass[i].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        size *= factor;
        if (settings.verbose > 1) {
            fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                    i, slabclass[i].size, slabclass[i].perslab);
        }
    }

    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
    if (settings.verbose > 1) {
        fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                i, slabclass[i].size, slabclass[i].perslab);
    }

    /* for the test suite:  faking of how much we've already malloc'd */
    {
        char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
        if (t_initial_malloc) {
            mem_malloced = (size_t)atol(t_initial_malloc);
        }

    }

    if (prealloc) {
        slabs_preallocate(power_largest);
    }
}
//分配記憶體
static void slabs_preallocate (const unsigned int maxslabs) {
    int i;
    unsigned int prealloc = 0;

    /* pre-allocate a 1MB slab in every size class so people don't get
       confused by non-intuitive "SERVER_ERROR out of memory"
       messages.  this is the most common question on the mailing
       list.  if you really don't want this, you can rebuild without
       these three lines.  */
    //給每一個slabclass_t結構分配一個預設的slab
    for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) {
        if (++prealloc > maxslabs)
            return;
        if (do_slabs_newslab(i) == 0) {
            fprintf(stderr, "Error while preallocating slab memory!\n"
                "If using -L or other prealloc options, max memory must be "
                "at least %d megabytes.\n", power_largest);
            exit(1);
        }
    }

}

do_slabs_alloc - 分配一個item

1. Memcached分配一個item,會先檢查freelist空閒的列表中是否有空閒的item,如果有的話就用空閒列表中的item。

2. 如果空閒列表沒有空閒的item可以分配,則Memcached會去申請一個slab(預設大小為1M)的記憶體塊,如果申請失敗,則返回NULL,表明分配失敗。

3. 如果申請成功,則會去將這個1M大小的記憶體塊,根據slabclass_t可以儲存的最大的item的size,將slab切割成N個item,然後放進freelist(空閒列表中)

4. 然後去freelist(空閒列表)中取出一個item來使用。

//分配一個Item
void *slabs_alloc(size_t size, unsigned int id) {
    void *ret;
    //分配Item前需要上執行緒鎖
    pthread_mutex_lock(&slabs_lock);
    //size:需要分配的item的長度
    //id:需要分配在哪個slab class上面
    ret = do_slabs_alloc(size, id);
    pthread_mutex_unlock(&slabs_lock);
    return ret;
}
//分配一個Item
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;
    }

    //獲取slabclass
    p = &slabclass[id];
    assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0);

    /* fail unless we have space at the end of a recently allocated page,
       we have something on our freelist, or we could allocate a new page */
    //p->sl_curr 說明是否有空閒的item list
    //如果沒有空閒的item list,則取分配一個新的slab,如果分配失敗,返回NULL
    if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) {
        /* We don't have more memory available */
        ret = NULL;
    //如果有free item lits,則從空閒的列表中取一個Item
    } 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;
    }

    if (ret) {
        p->requested += size;
        MEMCACHED_SLABS_ALLOCATE(size, id, p->size, ret);
    } else {
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, id);
    }

    return ret;
}
分配一個新的slab:
//分配一塊新的item塊
static int do_slabs_newslab(const unsigned int id) {
	//獲取slabclass
    slabclass_t *p = &slabclass[id];
    //分配一個slab,預設是1M
    //分配的slab也可以根據 該slabclass儲存的item的大小 * 可以儲存的item的個數 來計算出記憶體塊長度
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;
    char *ptr;

    //這邊回去分配一塊slab記憶體塊
    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;
    }

    //將slab記憶體記憶體塊切割成N個item,放進freelist中
    memset(ptr, 0, (size_t)len);
    split_slab_page_into_freelist(ptr, id);

    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}
將slab記憶體塊進行切割:
//將slab 切割成N個item
static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int x;
    for (x = 0; x < p->perslab; x++) {
        //將指標傳遞給ptr,free操作並不是真正意義上的釋放記憶體塊,只是將記憶體塊放到free list(空閒列表上面)
        do_slabs_free(ptr, 0, id);
        //這邊使用p->size,來設定每個item記憶體塊的大小
        //實際儲存的時候,item的size都會小於p->size
        ptr += p->size;
    }
}

slabs_free - 釋放一個item

釋放item後,會將item放進free list(空閒列表中)。

//釋放一個Item
void slabs_free(void *ptr, size_t size, unsigned int id) {
    pthread_mutex_lock(&slabs_lock);
    //ptr:item的指標
    //size:item的大小
    //id:在哪個slab class上面
    do_slabs_free(ptr, size, id);
    pthread_mutex_unlock(&slabs_lock);
}
//釋放一個item
static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
    slabclass_t *p;
    item *it;

    assert(((item *)ptr)->slabs_clsid == 0);
    assert(id >= POWER_SMALLEST && id <= power_largest);
    if (id < POWER_SMALLEST || id > power_largest)
        return;

    MEMCACHED_SLABS_FREE(size, id, ptr);
    p = &slabclass[id];

    it = (item *)ptr;
    it->it_flags |= ITEM_SLABBED;
    //放進空閒列表  freelist
    it->prev = 0;
    it->next = p->slots;
    if (it->next) it->next->prev = it;
    p->slots = it;

    p->sl_curr++;
    p->requested -= size;
    return;
}