IT技术博客大学习 共学习 共进步

Memcached内存管理机制浅析

basic coder 2012-03-31 23:49:17 浏览 5,081 次

    Memcached的内存管理在网上也可以搜集到不少不错的文章,新浪的这篇《Memcached深度分析》讲得不错,读别人的文章还是不如自己直接去读源码分析源码来得直接,这里写一下我阅读Memcached源码时对于Memcached内存管理机制的理解。

    Memcached的代码结构很简单,从main()函数入口进去之后便是几个模块的初始化函数,和内存管理相关的主要有两个函数,一个是assoc_init(),这个是用来初始化哈希表的,关于这个哈希表的作用留在外面讨论,另一个是slabs_init(),该函数用来初始化slab,下面先来讨论一下slab机制。

1. 内存slab的管理
1.1 slabs的初始化

    Memcached把内存分为一个个的slab,每个slab又分成一个个的chunk,系统会定义一个slab_class数组,其中每个元素是都是一个对该slab的描述,包括这个slab里面的每个chunk的大小,这个slab里面包含多少个chunk等信息,先把slab分布情况打印出来看看,对Memcached的内存分配有个大体的认识,然后再去读代码可能会好一些。

$ memcached -vv

     slab class 1: chunk size 80 perslab 13107

     slab class 2: chunk size 104 perslab 10082

     slab class 3: chunk size 136 perslab 7710

     slab class 4: chunk size 176 perslab 5957

     slab class 5: chunk size 224 perslab 4681

     slab class 6: chunk size 280 perslab 3744

     slab class 7: chunk size 352 perslab 2978

     slab class 8: chunk size 440 perslab 2383

     slab class 9: chunk size 552 perslab 1899

     slab class 10: chunk size 696 perslab 1506

     slab class 11: chunk size 872 perslab 1202

     slab class 12: chunk size 1096 perslab 956

     slab class 13: chunk size 1376 perslab 762

     slab class 14: chunk size 1720 perslab 609

     slab class 15: chunk size 2152 perslab 487

     slab class 16: chunk size 2696 perslab 388

     slab class 17: chunk size 3376 perslab 310

     slab class 18: chunk size 4224 perslab 248

     slab class 19: chunk size 5280 perslab 198

     slab class 20: chunk size 6600 perslab 158

     slab class 21: chunk size 8256 perslab 127

     slab class 22: chunk size 10320 perslab 101

     slab class 23: chunk size 12904 perslab 81

     slab class 24: chunk size 16136 perslab 64

     slab class 25: chunk size 20176 perslab 51

     slab class 26: chunk size 25224 perslab 41

     slab class 27: chunk size 31536 perslab 33

     slab class 28: chunk size 39424 perslab 26

     slab class 29: chunk size 49280 perslab 21

     slab class 30: chunk size 61600 perslab 17

     slab class 31: chunk size 77000 perslab 13

     slab class 32: chunk size 96256 perslab 10

     slab class 33: chunk size 120320 perslab 8

     slab class 34: chunk size 150400 perslab 6

     slab class 35: chunk size 188000 perslab 5

     slab class 36: chunk size 235000 perslab 4

     slab class 37: chunk size 293752 perslab 3

     slab class 38: chunk size 367192 perslab 2

     slab class 39: chunk size 458992 perslab 2

     slab class 40: chunk size 573744 perslab 1

     slab class 41: chunk size 717184 perslab 1

     slab class 42: chunk size 1048576 perslab 1

    这是Memcached的默认配置,chunk size是按照CHUNK_ALIGN_BYTES对齐的,chunk size相比于前一个slab中的chunk size有一个上升因子factor,1.4.7里面factor的默认值是1.25,我们可以看到按默认配置slab总共分成了42类。

     先给出一个我用Dia画的Memcached的内存分配图,Dia不如Visio好用,凑合着画了一个,如果有理解不对的地方欢迎大家指出。

    

    接下来看一下slabs_init()的代码,还是只保留关键代码,节省版面。

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;
    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;
        } 
    }
    memset(slabclass, 0, sizeof(slabclass));
    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].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        size *= factor;
    }
    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
#ifndef DONT_PREALLOC_SLABS
    {
        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
 
        if (pre_alloc == NULL || atoi(pre_alloc) != 0) {
			printf("prealloc memory.\\n");
            slabs_preallocate(power_largest);
        }
    }
#endif
}

    prealloc这个参数其实是跟后面的DONT_PREALLOC_SLABS这个宏是相关的,表示是否要在初始化的时候为slabs分配内存,如果需要预先为slabs分配内存,则先跟系统申请mem_limit字节的内存,之后的slab都是从这块内存上分配的,这块内存大小默认是64M,说起来当时犯了个很低级的错误,当时看到这里的时候发现mem_limit的默认值是1024 * 1024 * 64,于是断点在这里,发现malloc()没有返回NULL,当时想我一个2G的机器申请64G的内存到底是怎么分配成功的,纠结了好久才发现不是64G,是64M,所以读代码/写代码的时候还真得保持头脑清醒才行。。。

    然后再说prealloc,如果没有定义DONT_PREALLOC_SLABS这个宏的话,初始化的时候会先申请64M的内存,接着调用preallocate这个函数,看下这个函数的注释,然后我要吐槽一下我的英语,前两天被它的那句注释搞晕了。

#ifndef DONT_PREALLOC_SLABS
/* Preallocate as many slab pages as possible (called from slabs_init)
on start-up, so users don\'t get confused out-of-memory errors when
they do have free (in-slab) space, but no space to make new slabs.
if maxslabs is 18 (POWER_LARGEST - POWER_SMALLEST + 1), then all
slab types can be made. if max memory is less than 18 MB, only the
smaller ones will be made. */
static void slabs_preallocate (const unsigned int maxslabs);
#endif

    这句话的大体意思应该是:在启动的时候尽可能多地分配slabs页,所以用户不要因为内存足够却获得OOM而感到郁闷。。。

    我当时的理解时:在启动的时候尽可能多地分配slabs页,这样用户就不会因为内存足够却被提示OOM而感到郁闷了。。。

    我仔细地查阅源代码,发现如果prealloc,那么64M的内存用光之后并不会再去malloc新内存,跟我当时理解的这句注释的意思正好相反,仔细研究代码发现代码没有什么问题之后我反过来看了一眼这句注释,我觉得是我把这句英文理解错了吧。。。

    OK,也就是说如果开启了prealloc功能的话,那么很有可能在有空闲内存的情况下分配内存失败,另外提前为slabs分配内存也有可能会造成内存的浪费,有可能所有的item都不会使用某个slab class,这样这个slab class里面分配的内存就浪费掉了,DONT_PREALLOC_SLABS在1.4.7里面是默认定义的,也就是说prealloc功能是默认关闭的,于是就不考虑先prealloc了。

    slabs_init()接下来的代码就很简单了,对每个slab的chunk size进行对齐然后设置该slab class的相关成员变量的值。

1.2 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;
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

    size和perslab这两个字段已经说过了,slots这里存放的是空闲的slab列表,当调用do_slabs_free()这个函数之后,要释放的chunk就被放到这个数组的尾部,sl_curr数组尾部开始的第一个空闲的chunk,sl_total表示数组的总大小,当sl_curr大小等于sl_total的时候数组会通过realloc()进行扩容,容易是旧容量的2倍。

    end_page_ptr这个字段表示该slab里面的当前空闲的chunk地址,end_page_free,表示该slab中剩余的空闲chunk的数目,其它的几个字段按注释都很容易理解了。

1.3 创建新的slab

    前面提到的slabs_preallocate()函数只不过是对每一个已初始化的slab_class调用do_slabs_newslab()函数为其分配一块slab内存空间,看下这个函数的代码。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = p->size * p->perslab;
    char *ptr;
 
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {
        return 0;
    }
 
    memset(ptr, 0, (size_t)len);
    p->end_page_ptr = ptr;
    p->end_page_free = p->perslab;
 
    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
 
    return 1;
}

    slab_class的slab_list字段保存的是已分配的slabs列表,该列表实际上是个数组,当数组中没有空闲位置时则会调用grop_slab_list()对数组进行扩容,接下来便会调用memory_allocate()给slab分配内存,这个函数会检测是否已经开始了prealloc功能,如果开启了便会在预分配的内存块上申请一块内存,当这块预分配的内存用完时并不会对其进行扩容,于是便返回分配内存失败,这也就造成了系统明明有剩余内存,Memcached却提示SERVER_ERROR out of memory,当然,如果没有开启prealloc功能,这个函数便会直接调用malloc()分配内存,接下来对各个指针进行初始化。刚分配的空闲slab,它的end_page_str指针是指向slab内存首部的,end_page_free字段代表slab内存中包含中的chunk数。

1.4 在slab上分配内存

    在某个slab_class上分配size大小的内存的函数是do_slabs_alloc(),这个函数有两个参数,要分配的内存字节数size,和该内存应该存在于哪个slab class上的slab class id. 这两个参数在是有相关性的,在调用该函数的时候class id一般是通过size来计算得出来的,先看一下这个函数:

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
 
    if (id < POWER_SMALLEST || id > power_largest) {
        return NULL;
    }
 
    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 */
    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 */
        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;
    }
 
    return ret;
}

    1.如果end_page_ptr等于0,并且sl_curr等于0,则表示slab中已经没有空闲内存,并且回收的chunk free list里面也没有可用内存了,于是这时候需要调用do_slabs_newslab()创建新的slab。

    2.如果sl_curr不等于0,则表示chunk free list中还有可用的内存,直接返回一个可用的chunk即可。

    3.如果chunk free list里面没有可用内存,而slab中还有空闲内存,则直接从slab中申请一个chunk的内存,然后将end_page_ptr后移。

2. 对象item的管理
2.1 item对象的分配

    存入系统的每个key-value对都会被转换成一个item,这个item中保存了相关的状态标志信息,当服务器收到一个set请求时便需要在内存中创建一个item,item的内存理所当然是在上面讨论过的slab分配器上分配的。item的存储使用了LRU的方法,把item链入一个链表中,其中全局变量heads[LARGEST_ID]和tails[LARGEST_ID]这两个数组保存各个slab class所对应的item链表的表头和表尾,item创建的函数do_item_alloc()太长,就不把代码贴出来了,描述一下它的过程。

    在这里先提一下前面提到的哈希表,哈希表是用来把item通过key散列到哈希表上的,这样就可以通过key来快速地定位item,在do_item_unlink()这个函数中,首先要把该item从哈希表中删除,然后再从list中移除,最后检测该item的refcount,如果refcount是0,则调用item_free()释放内存,item_free()再调用底层的slab_free()去释放内存,slab_free()只是do_slab_free()的线程安全版本,它在内部先加锁随后调用do_slab_free(),再之后解锁。

    OK,接着看item的分配过程,首先会从链表的尾开始往前找,如果某节点的item设置了过期时间并且该item已过期,则回收该item,调用do_item_unlink()把它从链表中取出来,刚才说过do_item_unlink()这个函数在refcount为0的时候会释放掉这个item,所以为了防止这个item内存被释放,先将它的refcount设置为1,若向前查找50次都没有找到符合要求的item,则循环断开。

    如果没有找到可以回收的item,然后就调用slabs_alloc()分配内存,如果内存也分配失败,就尝试着从链表尾开始向前找出一些没有人用的item(refcount=0),把它do_item_unlink()掉,这时候因为refcount=0,所以它相关的内存也会被释放还给slab分配器,这个尝试又从尾向前尝试50次,OK,slab分配器中可能又有可用内存了,再用slabs_alloc()分配内存,如果还失败。。。好吧,这次只能从链表中删除一些正在引用但过期时间小于current_time - CURRENT_REPAIR_TIME的节点,这个尝试又从尾向前尝试50次,OK,再做最后一次尝试再去slabs_alloc()分配内存,如果这次还是失败,那就彻底放弃了,内存分配失败。。。

3. Memcached的哈希表
3.1 Memcached用到的哈希算法

    Memcached用到的哈希算法比较复杂,算法地址在http://burtleburtle.net/bob/hash/doobs.html,Memcached维护了两个哈希表,primary_hashtable和old_hashtable,primary_hashtable是当前正在使用的哈希表,当表没有进行扩张时从这张表中插入或者查找,old_hashtable用于哈希表扩张的时候使用,它指向旧的哈希表,当哈希表中的item数大于表的大小的3/2时,则哈希表进行扩张,此时插入和查找等操作都是在old_hashtable中进行的。

3.2 数据项插入哈希表

    数据项插入哈希表时用了assoc_insert()这个函数,下面看下它的代码

int assoc_insert(item *it) {
    uint32_t hv;
    unsigned int oldbucket;
 
    assert(assoc_find(ITEM_key(it), it->nkey) == 0);  /* shouldn\'t have duplicately named things defined */
 
    hv = hash(ITEM_key(it), it->nkey, 0);
    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it->h_next = old_hashtable[oldbucket];
        old_hashtable[oldbucket] = it;
    } else {
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];
        primary_hashtable[hv & hashmask(hashpower)] = it;
    }
 
    hash_items++;
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_expand();
    }
 
    MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
    return 1;
}

    如果expanding是true,哈希表正在扩张,则把item插入到old_hashtable中,否则则插入到primary_hashtable中,然后检测item数是否大于hashsize * 3 / 2,如果是,则进行扩张,哈希表的查找删除等操作也大致类似,不拿出来说了。

3.3 哈希表的扩张

    哈希表的扩张其实是异步进行的,Memcached在初始化时在main()函数中会调用start_assoc_maintenance_thread()函数来开启一个线程对哈希表进行定期维护,线程函数通过对条件变量的wait进行睡眠,当被激活时发现expanding为true,则对哈希表进行扩张,把旧表的元素复制到新表中,然后释放旧表的内存空间,搞定后再睡去。。。触发哈希表扩张事件的函数是assoc_expand()

static void assoc_expand(void) {
    old_hashtable = primary_hashtable;
 
    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
    if (primary_hashtable) {
        if (settings.verbose > 1)
            fprintf(stderr, "Hash table expansion starting\\n");
        hashpower++;
        expanding = true;
        expand_bucket = 0;
        pthread_cond_signal(&maintenance_cond);
    } else {
        primary_hashtable = old_hashtable;
        /* Bad news, but we can keep running. */
    }
}

    这个函数让把old_hashtable指向primary_hashtable,之后给primary_hashtable重新分配内存空间,然后把expanding标志设为true,接着激活maintenace_cond信号,maintenace线程被唤醒开始异步地把old_hashtable中的元素拷贝到primary_hashtable中来。

    OK,这是我对Memcached内存管理机制的一个简单的探索和了解,如有谬误的地方,欢迎大家批评指正。

建议继续学习

  1. 分布式缓存系统 Memcached 入门 (阅读 16,042)
  2. 30分钟3300%性能提升――python+memcached网页优化小记 (阅读 13,581)
  3. Cacti 添加 Memcached 监控 (阅读 9,162)
  4. Redis和Memcached的区别 (阅读 7,941)
  5. memcached 源码阅读笔记 (阅读 5,262)
  6. 启用memcached压缩注意事项 (阅读 5,122)
  7. 深入理解Linux内存管理机制(一) (阅读 4,882)
  8. Memcached and MySQL (阅读 4,361)
  9. Memcached的线程模型及状态机 (阅读 4,362)
  10. 又一个PHP低概率Core的分析(PHP内存管理) (阅读 4,260)