memcached-slab內存管理


一、Memcache內存分配機制

        關於這個機制網上有很多解釋的,我個人的總結如下。

Page為內存分配的最小單位。

Memcached 的內存分配以page為單位,默認情況下一個page是1M,可以通過-I參數在啟動時指定。如果需要申請內存 時,memcached會划分出一個新的page並分配給需要的slab區域。page一旦被分配在重啟前不會被回收或者重新分配(page ressign已經從1.2.8版移除了)
Memcache內存分配策略 - 風 - gaify_yang

Slabs划分數據空間。

Memcached 並不是將所有大小的數據都放在一起的,而是預先將數據空間划分為一系列slabs,每個slab只負責一定范圍內的數據存儲。如 下圖,每個slab只存儲大於其上一個slab的size並小於或者等於自己最大size的數據。例如:slab 3只存儲大小介於137 到 224 bytes的數據。如果一個數據大小為230byte將被分配到slab 4中。從下圖可以看出,每個slab負責的空間其實是不等的,memcached默認情況下下一個slab的最大值為前一個的1.25倍,這個可以通過修 改-f參數來修改增長比例。
Memcache內存分配策略 - 風 - gaify_yang

Chunk才是存放緩存數據的單位。

Chunk 是一系列固定的內存空間,這個大小就是管理它的slab的最大存放大小。例如:slab 1的所有chunk都是104byte,而slab 4的所有chunk都是280byte。chunk是memcached實際存放緩存數據的地方,因為chunk的大小固定為slab能夠存放的最大值, 所以所有分配給當前slab的數據都可以被chunk存下。如果時間的數據大小小於chunk的大小,空余的空間將會被閑置,這個是為了防止內存碎片而設 計的。例如下圖,chunk size是224byte,而存儲的數據只有200byte,剩下的24byte將被閑置。
Memcache內存分配策略 - 風 - gaify_yang

Slab的內存分配。

Memcached在啟動時通過-m指定最大使用內存,但是這個不會一啟動就占用,是隨着需要逐步分配給各slab的。
         如果一個新的緩存數據要被存放,memcached首先選擇一個合適的slab,然后查看該slab是否還有空閑的chunk,如果有則直接存放進去;如 果沒有則要進行申請。slab申請內存時以page為單位,所以在放入第一個數據,無論大小為多少,都會有1M大小的page被分配給該slab。申請到 page后,slab會將這個page的內存按chunk的大小進行切分,這樣就變成了一個chunk的數組,在從這個chunk數組中選擇一個用於存儲 數據。如下圖,slab 1和slab 2都分配了一個page,並按各自的大小切分成chunk數組。
Memcache內存分配策略 - 風 - gaify_yang

Memcached內存分配策略。

綜合上面的介紹,memcached的內存分配策略就是:按slab需求分配page,各slab按需使用chunk存儲。
這里有幾個特點要注意,

Memcached分配出去的page不會被回收或者重新分配Memcached申請的內存不會被釋放slab空閑的chunk不會借給任何其他slab使用

Memcache內存分配策略 - 風 - gaify_yang

 

      知道了這些以后,就可以理解為什么總內存沒有被全部占用的情況下,memcached卻出現了丟失緩存數據的問題了。

 

眾所周知,memcache采用slab allocator管理內存,啟動由-m指定可用的最大內存值(默認64M),默認是使用時再分配,-k可提前分配內存並鎖定;
每個slab包含若干大小為1M的內存頁,這些內存又被分割成多個chunk,每個chunk存儲一個item;
在mc啟動初始化時,每個slab都預分配一個1M的內存頁,由 slabs_preallocate 完成(也可將相應代碼注釋掉關閉預分配功能);
chunk的增長因子由-f指定,默認1.25,起始大小為48字節;






item
為實際存儲數據的結構體, 由兩部分組成,屬性信息和數據部分,數據部分包括cas,key和真實的value信息。
  • typedef struct _stritem {  
  •     struct _stritem *next;//item在slab中存儲時,是以雙鏈表的形式存儲的,next即后向指針  
  •     struct _stritem *prev;//prev為前向指針  
  •     struct _stritem *h_next;//Hash桶中元素的鏈接指針  
  •     rel_time_t      time; //最近訪問時間  
  •     rel_time_t      exptime;//過期時間  
  •     int             nbytes;//數據大小  
  •     unsigned short  refcount;//引用次數  
  •     uint8_t         nsuffix;    
  •     uint8_t         it_flags;    
  •     uint8_t         slabs_clsid;//標記item屬於哪個slabclass下  
  •     uint8_t         nkey;       //key的長度  
  •     union {  
  •         uint64_t cas;  
  •         char end;  
  •     } data[];//真實的數據信息  
  • } item;  





slab
何時分配新的slab? 1 bigger item; 2 no free chunk
If the new item is bigger than the size of any existing blocks, then a new slab is created, divided up into blocks of a suitable size. If an existing slab with the right block size already exists, but there are no free blocks, a new slab is created. 
內存頁一旦被分配給slab,在memcache生命周期內不再更改,在內存總量確定的情形下,其它slab可能出現餓死現象;
為此1.4.11引入slab automove
The algorithm is slow and conservative. If a slab class is seen as having the highest eviction count 3 times 10 seconds apart, it will take a page from a slab class which has had zero evictions in the last 30 seconds and move the memory.
若某個slab在10秒內出現3次eviction,則從過去30秒內沒有出現eviction的slab里挪取一個內存頁使用;
分為手工分配和自動分配:
手工:  -o slab_reassign;在mc運行時輸入echo "slabs reassign 1 4" | nc localhost 11211
自動: -o slab_reassign, slab_automove;mc每10秒進行一次重分配,手工暫停echo   "slabs automove 0" | nc localhost 11211

typedef struct {  
    unsigned int size;          /* 每個item大小, sizes of items */  
    unsigned int perslab;       /* 每個page中包含多少個item , how many items per slab */  
  
    void **slots;               /* 空閑的item指針, list of item ptrs */  
    unsigned int sl_total;      /* 以分配空閑的item 個數, size of previous array */  
    unsigned int sl_curr;       /* 當前空閑的item位置(也就是實際空閑item個數),從后往前的, first free slot */  
  
    void *end_page_ptr;         /* 指向最后一個頁面中空閑的item開始位置, pointer to next free item at end of page, or 0 */  
    unsigned int end_page_free; /* 最后一個頁面,item個數, number of items remaining at end of last alloced page */  
  
    unsigned int slabs;         /* 實際使用slab(page)個數 how many slabs were allocated for this class */  
  
    void **slab_list;           /* 所有page的指針, array of slab pointers */  
    unsigned int list_size;     /* 已經分配page指針個數,size of prev array */  
  
    unsigned int killing;       /* index+1 of dying slab, or zero if none */  
    size_t requested;           /* 所有被使用了的內存的大小, The number of requested bytes */  
} slabclass_t;  




LRU和expired item
memcache並不會監視和清理過期數據,而是在客戶端get時檢查,稱為lazy expiration。
item被檢測到超時並不會被刪除,而是放入slab->slots頭部;
do_item_get --
   --判斷該item是否過期
   do_item_unlink(it, hv);//將item從hashtable和LRU鏈中移除             
   do_item_remove(it);//刪除item 
do_item_remove
  item_free(it);//釋放item  
    slabs_free(it, ntotal, clsid);//slabclass結構執行釋放 
            do_slabs_free(ptr, size, id);//執行釋放 
以下是do_slabs_free的代碼,將expired item放入slab->slots的頭部       
    it = (item *)ptr;  
    it->it_flags |= ITEM_SLABBED;//修改item的狀態標識,修改為空閑  
    it->prev = 0;//斷開數據鏈表  
    it->next = p->slots;  
    if (it->next) it->next->prev = it;  
    p->slots = it; 

問:expired item何時被重用?
1 slab在新加item時會先查看LRU隊尾;
2 如果隊尾的item恰巧超時則重用,否則執行slabs_alloc;這一過程循環5次,若還沒有找到可用item,則再次調用 slabs_alloc;
slabs_alloc依次嘗試  a slab->slot即expired item鏈表;  b slab->end_page_ptr 最后一個頁面的空閑item; c 分配新的內存頁
也就是說,只有LRU最后的5個元素狀態為expired時,才有機會直接重用LRU,否則會依次嘗試expired item list和slab的最后一個內存頁的free item;

以下是代碼實現
 
當客戶端執行add操作,即往slab添加item時,調用 do_item_alloc;
do_item_alloc
    mutex_lock(&cache_lock);//執行LRU鎖 存儲時,會嘗試從LRU中選擇合適的空間的空間  
    int tries = 5;//如果LRU中嘗試5次還沒合適的空間,則執行申請空間的操作 
    search = tails[id];//第id個LRU表的尾部 
    
    /* We walk up *only* for locked items. Never searching for expired
    for (; tries > 0 && search != NULL; tries--, search=search->prev) {
        uint32_t hv = hash(ITEM_key(search), search->nkey, 0);//獲取分段鎖 

        if ((search->exptime != 0 && search->exptime < current_time)  || (search->time <= oldest_live && oldest_live <= current_time)) { //過期時間的判斷  
            it = search; 
        } else if ((it = slabs_alloc(ntotal, id)) == NULL) {//申請合適的slabclass  
          ......
        } 
        break;  
    } 

    if (!tried_alloc && (tries == 0 || search == NULL))//5次循環查找,未找到合適的空間  
        it = slabs_alloc(ntotal, id);//則從內存池申請新的空間 

    return it;  

注:每次新分配item時,先進行5次循環:檢查LRU尾部item是否過期,過期則重用,否則嘗試slabs_alloc申請新內存;  若5次后仍未獲取可用內存,則再次嘗試slabs_alloc申請內存;

slabs_alloc
        pthread_mutex_lock(&slabs_lock);
    ret = do_slabs_alloc(size, id);
    pthread_mutex_unlock(&slabs_lock);

do_slabs_alloc(const size_t size, unsigned int id)
    p = &slabclass[id];  
    /* 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 */  
    // 1 最后面的頁面有空間 2 空閑的地方有空間 3 分配一個新的頁面  
    if (! (p->end_page_ptr != 0 || 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 */  
        //從空閑(回收)地方分配  
        ret = p->slots[--p->sl_curr];  
    } else {  
        /* if we recently allocated a whole page, return from that */  
        assert(p->end_page_ptr != NULL);  
        ret = p->end_page_ptr;  
        if (--p->end_page_free != 0) {  
            p->end_page_ptr = ((caddr_t)p->end_page_ptr) + p->size;  
        } else {  
            p->end_page_ptr = 0;  
        }  
    }  

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

do_slabs_newslab--即為該slab新分配一個數據頁;
    int len = p->size * p->perslab; 
    memory_allocate((size_t)len))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM