memcached源码剖析系列之内存存储机制(二)

在上一节中已经分析了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系统内存的次数,降低函数开销和内存碎片产生几率,虽然这种方式会造成一些冗余浪费,但是这种浪费在大型系统应用中是微不足道的。


你可能感兴趣的:(memcached)