memcached源碼剖析系列之內存存儲機制

http://www.cnblogs.com/moonlove/tag/memcached/

 一 內存分配管理機制

    memcached是一個高性能的,分佈式內存對象緩存系統,用於在動態系統中減少數據庫負載,提升性能。memcached有一個很有特色的內存管理方式,爲了提高效率,默認情況下采用了名爲Slab Allocator的機制分配管理內存空間。   

    memcached文檔中關於slab allocator有這麼一段話:

    the primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issues totally by using fixed-size memory chunks coming from a few predetermined size classes.

    由此,我們可以看出,memcached使用預申請內存並分組成特定塊的方式,旨在解決內存碎片的問題。

    Memcached的內存管理方式還是比較簡單易懂的,使用的是slab->chunk的組織方式管理內存。Slab是Memcached進行內存申請的最小單位,默認一般爲1MB,可使用命令行參數進行自定義設置。然後使用分塊機制將slab分成一定大小分成若干個chunks。如下圖所示(此圖來源於網絡):

 

二 源碼分析     

關鍵數據結構

(1)settings結構體原型:

/* When adding a setting, be sure to update process_stat_settings */
/**
 * Globally accessible settings as derived from the commandline.
 */
struct settings {
    //最大內存, 默認64M,最大2G。通過-m 設定
    size_t maxbytes;
    //最大連接數,默認1024 通過-c設定
    int maxconns;
    //tcp 端口號,通過-p 設置
    int port;
    //ucp 端口號,通過-U 設置
    int udpport;
    //監聽IP或SOCKET地址 ,通過-l設定
    char *inter;
    //是否輸出debug信息。由-v,-vvv參數設定
    int verbose;
    //時間設定,當使用flsuh時,只需要修改本值,當取出的值時間小於本值時,將被忽略。
    rel_time_t oldest_live; /* ignore existing items older than this */
    //當內存存滿時,是否淘汰老數據。默認是是。可用-M修改爲否。此時內容耗盡時,新插入數據時將返回失敗。
    int evict_to_free;
    //socket模式,使用-s設定。
    char *socketpath;   /* path to unix socket if using local socket */
    //socket文件的文件權限,使用-a設定
    int access;  /* access mask (a la chmod) for unix domain socket */
    //slab分配增量因子,默認圍1.25, 可通過-f設定
    double factor;          /* chunk size growth factor */
    //給一個key+value+flags 分配的最小字節數。 默認值爲48. 可通過-n修改。
    int chunk_size;
    //工作線程數。默認圍4, 可通過-t設定
    int num_threads;        /* number of worker (without dispatcher) libevent threads to run */
    //狀態詳情的key前綴
    char prefix_delimiter;  /* character that marks a key prefix (for stats) */
    //開啓狀態詳情記錄
    int detail_enabled;     /* nonzero if we're collecting detailed stats */
    //每個event處理的請求數
    int reqs_per_event;     /* Maximum number of io to process on each  io-event. */
//開啓cas,"cas"是一個存儲檢查操作。用來檢查髒數據的存操作。在取出數據後,如果沒有其他人修改此數據時,本進程才能夠存儲數據。默認爲開啓。需要版本:1.3+
    bool use_cas;
    //使用協議, 試過-B參數設定。 可能值爲:ascii, binary, or auto, 版本: 1.4.0+
    enum protocol binding_protocol;
    //等待處理的排隊隊列長度。默認值爲1024.
    int backlog;
     //單個item最大字計數。默認1M。可通過-I參數修改。在1.4.2版本之後,這個值可以大於1M,必須小於128M。但memcached會拋出警告,大於1M將導致整體運行內存的增加和內存性能的降低。 版本: 1.4.2+
    int item_size_max;        /* Maximum item size, and upper end for slabs */
    //是否開啓sasl
    bool sasl;              /* SASL on/off */
};

(2)item結構體原型:

typedef struct _stritem {
    struct _stritem *next;
    struct _stritem *prev;
    struct _stritem *h_next;    /* hash chain next */
    rel_time_t      time;       /* least recent access */
    rel_time_t      exptime;    /* expire time */
    int             nbytes;     /* size of data */
    unsigned short  refcount;
    uint8_t         nsuffix;    /* length of flags-and-length string */
    uint8_t         it_flags;   /* ITEM_* above */
    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;

(3)slabclass_t結構體原型

typedef struct {
    unsigned int size;      /* sizes of items */
    unsigned int perslab;   /* how many items per slab */
    void **slots;           /* list of item ptrs */
    unsigned int sl_total;  /* size of previous array */
    unsigned int sl_curr;   /* first free slot */
    void *end_page_ptr;         /* pointer to next free item at end of page, or 0 */
    unsigned int end_page_free; /* number of items remaining at end of last alloced page */
    unsigned int slabs;     /* how many slabs were allocated for this class */
    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 */
    size_t requested; /* The number of requested bytes */
} slabclass_t;

(4)memcatchd.c文件中定義的部分宏

#define POWER_SMALLEST 1
#define POWER_LARGEST  200
#define CHUNK_ALIGN_BYTES 8
#define DONT_PREALLOC_SLABS
#define MAX_NUMBER_OF_SLAB_CLASSES (POWER_LARGEST + 1)

2 分配算法的實現

 (1)memcatchd.c中main函數中運行狀態的初始化

int main()
{
    …
    settings_init();
    …
    //利用命令行參數信息,對setting進行設置
    while (-1 != (c = getopt(argc, argv,…)
    {…}
    …
    //settings.factor 初始化爲1.25,可以使用命令行參數-f進行設置
    slabs_init(settings.maxbytes, settings.factor, preallocate);
}

settings_init()是初始化全局變量settings函數,在memcatchd.c文件實現

static void settings_init(void) {
    settings.use_cas = true;
    settings.access = 0700;
    settings.port = 11211;
    settings.udpport = 11211;
    /* By default this string should be NULL for getaddrinfo() */
    settings.inter = NULL;
    settings.maxbytes = 64 * 1024 * 1024; /* default is 64MB */
    settings.maxconns = 1024;         /* to limit connections-related memory to about 5MB */
    settings.verbose = 0;
    settings.oldest_live = 0;
    settings.evict_to_free = 1;       /* push old items out of cache when memory runs out */
    settings.socketpath = NULL;       /* by default, not using a unix socket */
    settings.factor = 1.25;
    settings.chunk_size = 48;         /* space for a modest key and value */
    settings.num_threads = 4;         /* N workers */
    settings.num_threads_per_udp = 0;
    settings.prefix_delimiter = ':';
    settings.detail_enabled = 0;
    settings.reqs_per_event = 20;
    settings.backlog = 1024;
    settings.binding_protocol = negotiating_prot;
    settings.item_size_max = 1024 * 1024; /* The famous 1MB upper limit. */
}

    從該設置setting的初始化函數可看出,settings.item_size_max = 1024 * 1024; 即每個slab默認的空間大小爲1MB,settings.factor = 1.25; 默認設置item的size步長增長因子爲1.25。使用命令行參數對setting進行定製後,調用slabs_init函數,根據配置的setting來初始化slabclass。slabs_init函數於Slabs.c文件中實現:

// slabs管理器初始化函數:limit默認64MB,prealloc默認false,可使用命令行參數’L’進行設置。
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;	//#define POWER_SMALLEST 1;i初始化爲0
    //item(_stritem):storing items within memcached
    unsigned int size = sizeof(item) + settings.chunk_size;//chunk_size:48 
    mem_limit = limit;  //limit默認64MB
	//預分配爲真時:
    if (prealloc) {
        /* Allocate everything in a big chunk with malloc */
        mem_base = malloc(mem_limit);
        if (mem_base != NULL) {
			//mem_current:靜態變量,記錄分配內存塊的基地址
			//mem_avail:可用內存大小
            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");
        }
    }
    //static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];
    //#define MAX_NUMBER_OF_SLAB_CLASSES (POWER_LARGEST + 1)
    //#define POWER_LARGEST  200
    memset(slabclass, 0, sizeof(slabclass));
    // /* settings.item_size_max: Maximum item size, and upper end for slabs,默認爲1MB */
    //item核心分配算法
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
		//#define CHUNK_ALIGN_BYTES 8
        if (size % CHUNK_ALIGN_BYTES)    //確保size爲CHUNK_ALIGN_BYTES的倍數,不夠則向補足
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
        slabclass[i].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;  //記錄每個slab中item的個數
        size *= factor;   //每次循環size的大小都增加factor倍
        if (settings.verbose > 1) {
            fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                    i, slabclass[i].size, slabclass[i].perslab);
        }
    }
    //補足一塊大小爲item_size_max的塊
     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);
        }
    }
#ifndef DONT_PREALLOC_SLABS  //已經定義了
    {
        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");

        if (pre_alloc == NULL || atoi(pre_alloc) != 0) {
            slabs_preallocate(power_largest);
        }
    }
#endif
}

    在memcached的內存管理機制中,使用了一個slabclass_t類型(類型聲明見上“關鍵數據結構”一節)的數組slabclass對劃分的slab及進行統一的管理

slabclass的聲明:static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

    每一個slab被劃分爲若干個chunk,每個chunk裏保存一個item,每個item同時包含了item結構體、key和value(注意在memcached中的value是隻有字符串的)。slab按照自己的id分別組成鏈表,這些鏈表又按id掛在一個slabclass數組上,整個結構看起來有點像二維數組。

    在定位item時,使用slabs_clsid函數,傳入參數爲item大小,返回值爲classid:

/*
 * Figures out which slab class (chunk size) is required to store an item of
 * a given size.
  * Given object size, return id to use when allocating/freeing memory for object
 * 0 means error: can't store such a large object
 */
unsigned int slabs_clsid(const size_t size) {
    int res = POWER_SMALLEST;
    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;  //返回第一個大於size的索引值
}

    根據返回的索引值即可定位到滿足該size的slabclass項。從源碼中可以看出:chunk的size初始值爲sizeof(item)+settings.chunk_size(key 和 value所使用的最小空間,默認爲48);chunk的大小以factor的倍數進行增長,最高爲slab的最大值的一半,最後一個slab的大小爲slab的最大值,這也是memcached所能允許分配的最大的item值。


 

本小節到此結束,在下一小節中將繼續分析memcached的存儲機制並分析該機制的優缺點。

注:本系列文章基於memcached-1.4.6版本進行分析。

reference:

[1] http://blog.developers.api.sina.com.cn/?p=124&cpage=1#comment-1506

[2] http://kb.cnblogs.com/page/42732/




上一節中已經分析了memcached的內存分配管理初始化機制,在這節中我們將詳細分析memcached中slab的管理與分配機制。

slabclass[MAX_NUMBER_OF_SLAB_CLASSES]數組是slab管理器(類型見上節),是memcached內存管理的核心數據結構,起着非常重要的作用。

slabclass[i]的內存示意圖如下圖所示:

 

(1)       sizeperslab保存着每個slab分配的chunk的大小,及可分配的chunk數。

(2)       slablist是一個二維指針,指向一個指針列表,列表的長度爲list_size * sizeof(void*),列表中的一項指向一個slab

(3)       end_page_ptr是一個指向最新分配的slab的指針。

源碼:

(1)do_slabs_newslab()函數實現

//爲該id的slab鏈分配一個新的slab
static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = p->size * p->perslab; 
    char *ptr;
	//grow_slab_list():如果slabs已經用完了,增長鏈表的長度
	//memory_allocate():爲新slab分配memory
    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);
	//p->end_page_ptr:指向新分配的slab,p->end_page_free爲新slab空餘items數
    p->end_page_ptr = ptr;
    p->end_page_free = p->perslab;

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

    return 1;
}

      這個函數的作用是當一個slab用光後,又有新的item要插入這個id,那麼它就會重新申請新的slab,申請新的slab時,對應idslab鏈表就要增長(由grow_slab_list()函數來實現),這個鏈表是成倍增長的,初始化值爲16。

(2)grow_slab_list()函數實現

static int grow_slab_list (const unsigned int id) {
    slabclass_t *p = &slabclass[id];
	//p->slabs:已經分配的slab數,p->list_size:slab鏈表的長度
    if (p->slabs == p->list_size) {//表示slabs已經用完
        size_t new_size =  (p->list_size != 0) ? p->list_size * 2 : 16;
        void *new_list = realloc(p->slab_list, new_size * sizeof(void *));
        if (new_list == 0) return 0;
        p->list_size = new_size;
        p->slab_list = new_list;
    }
    return 1;
}

(3)memory_allocate()函數實現

static void *memory_allocate(size_t size) {
    void *ret;

    if (mem_base == NULL) {
        /* We are not using a preallocated large memory chunk */
        ret = malloc(size);
    } else {
        ret = mem_current;

        if (size > mem_avail) {
            return NULL;
        }

        /* mem_current pointer _must_ be aligned!!! */
        if (size % CHUNK_ALIGN_BYTES) {
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
        }

        mem_current = ((char*)mem_current) + size;
        if (size < mem_avail) {
            mem_avail -= size;
        } else {
            mem_avail = 0;
        }
    }
    return ret;
}

    該函數爲一個slab分配p->size * p->perslab大小的內存,並由slab_list中一個指針指向它。

    另外,memcached不會釋放掉已用完的item指針的內存,其使用結構體slabclass_t中的slots二維指針來保存釋放出來的item指針,sl_total表示總的數量,sl_curr表示的是目前可用的已經釋放出來的item數量。

    每一次要分配內存的時候,首先根據需要分配的內存大小在slabclass數組中查找索引最小的一個大於所要求內存的slab,如果slots不爲空,那麼就從這裏返回內存,否則去查找end_page_ptr,如果也沒有,那麼就只能返回NULL了.

    每一次釋放內存的時候,同樣的找到應該返回內存的slab元素,改寫前面提到的slot指針和sl_curr數。這個過程由do_slabs_alloc()和do_slabs_free()完成。

memcached的內存分配機制的缺點

memcached的內存分配是有冗餘的:

(1) 當一個slab不能被它所擁有的chunk大小整除時,slab尾部剩餘的空間就被丟棄了。

(2) memcached的另外一個內存冗餘發生在保存item的過程中,item總是小於或等於chunk大小的,當item小於chunk大小時,就又發生了空間浪費。

 

在memcached內存存儲機制剖析的前兩篇文章中,已分析過memcached的內存管理器初始化機制及slab的管理分配機制。接下來我們就來探討下對象item的分配管理及LRU機制。

1 item關鍵數據結構

(1)item結構體原型

複製代碼
typedef struct _stritem {

    struct _stritem *next;

    struct _stritem *prev;

    struct _stritem *h_next;    /* hash chain next */

    rel_time_t      time;       /* least recent access */

    rel_time_t      exptime;    /* expire time */

    int             nbytes;     /* size of data */

    unsigned short  refcount;

    uint8_t         nsuffix;    /* length of flags-and-length string */

    uint8_t         it_flags;   /* ITEM_* above */

    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;

 
複製代碼

(2)全局數組

static item *heads[LARGEST_ID];

保存各個slab class所對應的item鏈表的表頭。

static item *tails[LARGEST_ID];

保存各個slab class所對應的item鏈表的表尾。

static unsigned int sizes[LARGEST_ID];

保存各個slab class所對應的items數目。

2 item分配機制的函數實現

(1)LRU機制

  在前面的分析中已介紹過,memcached不會釋放已分配的內存。記錄超時後,客戶端就無法再看見該記錄(invisible,透明),其存儲空間即可重複使用。Memcached採用的是Lazy Expiration,即memcached內部不會監視記錄是否過期,而是在get時查看記錄的時間戳,檢查記錄是否過期。這種技術被稱爲lazy(惰性)expiration。因此,memcached不會在過期監視上耗費CPU時間。

  memcached會優先使用已超時的記錄的空間,但即使如此,也會發生追加新記錄時空間不足的情況,此時就要使用名爲 Least Recently Used(LRU)機制來分配空間,即刪除“最近最少使用”的記錄。

(2)函數實現

Item的分配在函數do_item_alloc()中實現,函數原型爲:

item *do_item_alloc(char *key, const size_t nkey, const int flags, const rel_time_t exptime, const int nbytes);

參數含義:

* key       - The key

* nkey     - The length of the key

* flags     - key flags

*exptime  –item expired time

* nbytes  - Number of bytes to hold value and addition CRLF terminator

 函數的具體實現如下,由於do_item_alloc()太長,這裏只貼出部分關鍵代碼:

複製代碼
item *do_item_alloc(char *key, const size_t nkey, const int flags, const rel_time_t exptime, const int nbytes) {

    uint8_t nsuffix;

    item *it = NULL;

    char suffix[40];

    size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);

         //settings.use_cas:?cas"是一個存儲檢查操作,用來檢查髒數據的存操作。

         if (settings.use_cas) {

        ntotal += sizeof(uint64_t);

    }

    unsigned int id = slabs_clsid(ntotal);//獲得slabclass索引值

    if (id == 0)

        return 0;

    /* do a quick check if we have any expired items in the tail.. */

    int tries = 50;

    item *search;

         //在item鏈表中遍歷過期item

    for (search = tails[id];

         tries > 0 && search != NULL;

         tries--, search=search->prev) {

        if (search->refcount == 0 &&

            (search->exptime != 0 && search->exptime < current_time)) {

           …….

}

    }

         //沒有過期數據時,採用LRU算法,淘汰老數據

    if (it == NULL && (it = slabs_alloc(ntotal, id)) == NULL) {

        /*

        ** Could not find an expired item at the tail, and memory allocation

        ** failed. Try to evict some items!

        */

        tries = 50;

        /* If requested to not push old items out of cache when memory runs out,

         * we're out of luck at this point...

         */

                   // 當內存存滿時,是否淘汰老數據。默認爲真。可用-M修改爲否。此時內容耗盡時,新插入數據時將返回失敗。

          ……

        it = slabs_alloc(ntotal, id); //返回新分配的slab的第一個item

                   //item分配失敗,做最後一次努力

        if (it == 0) {

            itemstats[id].outofmemory++;

            /* Last ditch effort. There is a very rare bug which causes

             * refcount leaks. We've fixed most of them, but it still happens,

             * and it may happen in the future.

             * We can reasonably assume no item can stay locked for more than

             * three hours, so if we find one in the tail which is that old,

             * free it anyway.

             */

            tries = 50;

            for (search = tails[id]; tries > 0 && search != NULL; tries--, search=search->prev) {

                                     //search->time:最近一次訪問的時間

                                     if (search->refcount != 0 && search->time + TAIL_REPAIR_TIME < current_time) {

                    ……

            }

            it = slabs_alloc(ntotal, id);

            if (it == 0) {

                return NULL;

            }

        }

    }

    …….

    it->next = it->prev = it->h_next = 0;

    it->refcount = 1;     /* the caller will have a reference */

    DEBUG_REFCNT(it, '*');

    it->it_flags = settings.use_cas ? ITEM_CAS : 0;

    it->nkey = nkey;

    it->nbytes = nbytes;

         //零長數組

    memcpy(ITEM_key(it), key, nkey);

    it->exptime = exptime;

    memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);

    it->nsuffix = nsuffix;

    return it;

}
複製代碼

  該函數首先調用item_make_header()函數計算出該item的總長度,如果髒數據檢查標誌設置的話,添加sizeof(uint64_t)的長度,以便從slabclass獲得索引值(使用slabs_clsid()函數返回)。接着從後往前遍歷item鏈表,注意全局數組heads[LARGEST_ID]tails[LARGEST_ID]保存了slabclass對應Id的鏈表頭和表尾。

  從源碼中我們可以看出,有三次遍歷循環,每次最大遍歷次數爲50(tries表示),//在item鏈表中遍歷過期item,如果某節點的item設置了過期時間並且該item已過期,則回收該item,,調用do_item_unlink()把它從鏈表中取出來。

  若向前查找50次都沒有找到過期的item,則調用slabs_alloc()分配內存,如果alloc失敗,接着從鏈表尾開始向前找出一些沒有人用的refcount=0的item,調用do_item_unlink(),再用slabs_alloc()分配內存,如果還失敗,只能從鏈表中刪除一些正在引用但過期時間小於current_time – CURRENT_REPAIR_TIME的節點,這個嘗試又從尾向前嘗試50次,OK,再做最後一次嘗試再去slabs_alloc()分配內存,如果這次還是失敗,那就徹底放棄了,內存分配失敗。

  Memcached的內存管理方式是非常精巧和高效的,它很大程度上減少了直接alloc系統內存的次數,降低函數開銷和內存碎片產生機率,雖然這種方式會造成一些冗餘浪費,但是這種浪費在大型系統應用中是微不足道的。





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