分布式缓存系统 Memcached 内存管理机制

时间:2022-06-16 02:39:08

在前面slab数据存储部分分析了Memecached中记录数据的具体存储机制,从中可以看到所采用的内存管理机制——slab内存管理,这也正是linux所采用的内存高效管理机制,对于Memchached这样的内存cache服务器,内存高效管理是其最重要的任务之一。

Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。

Memecached 中的slab 分配器也正是使用这一思想来构建一个在空间和时间上都具有高效性的内存分配器。下图 给出了 slab 结构的高层组织结构。在最高层是cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个kmem_cache 结构的引用(称为一个 cache),它定义了一个要管理的给定大小的对象池。在需要某个特定大小的内存对象时,首先从cache_chian中找到最佳大小的一个kmem_cahce,然后再在对应的kem_cahe中按某种算法(如首先利用空闲对象,没有则按LRU机制释放已用或过期对象)最终获得所需的大小的空间。具体结构如下图所示:

分布式缓存系统 Memcached  内存管理机制

从该图可看出,这与前面所分析的Memcached的Item的存储结构图正是一致的。此处的cache_chain对应前面的slabclass数组(管理各种大小的slab集合),而kmem_cahe对应slabclass中的某个元素(slab_list链表)(管理某个特定大小的slab链表)。在删除Item时,也不会将所对应的内存还给操作系统,而只是从对应的已分配中链表中去掉,转而加到对应的空闲链表slots中以供后续循环利用。

memcached中内存分配机制主要理念:

1. 先为分配相应的大块内存,再在上面进行无缝小对象填充

2. 懒惰检测机制,Memcached不花过多的时间在检测各个item对象是否超时,当get获取数据时,才检查item对象是否应该删除,你不访问,我就不处理。

3. 懒惰删除机制,在memecached中删除一个item对象的时候,并不是从内存中释放,而是单单的进行标记处理,再将其指针放入slot回收插糟,下次分配的时候直接使用。

Memcached首次默认分配64M的内存,之后所有的数据都是在这64M空间进行存储,在Memcached启动之后,不会对这些内存执行释放操作,这些内存只有到Memcached进程退出之后会被系统回收。下面分析Memcached的内存主要操作函数,按逐级调用顺序给出。

/*//内存初始化,settings.maxbytes是Memcached初始启动参数指定的内存值大小,settings.factor是内存增长因子  
slabs_init(settings.maxbytes, settings.factor, preallocate);  
  
#define POWER_SMALLEST 1  //最小slab编号  
#define POWER_LARGEST  200 //首次初始化200个slab  
  
//实现内存池管理相关的静态全局变量  
static size_t mem_limit = 0;//总的内存大小  
static size_t mem_malloced = 0;//初始化内存的大小,这个貌似没什么用  
static void *mem_base = NULL;//指向总的内存的首地址  
static void *mem_current = NULL;//当前分配到的内存地址  
static size_t mem_avail = 0;//当前可用的内存大小  
  
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];//定义slab结合,总共200个 */

/**
 * Determines the chunk sizes and initializes the slab class descriptors
 * accordingly.
 初始化整个slabcalss数组
 limit:Memcached的总的内存的大小。 
 factor:chunk大小增长因子
 */
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
 //size表示申请空间的大小,其值由配置的chunk_size(指item中的数据部分大小)和单个item的大小来指定
    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);//分配限定的空间,mem_base为总内存起始地址
        if (mem_base != NULL) {
            mem_current = mem_base;//mem_current为当前分配空间地址
            mem_avail = mem_limit;//可用(总分配空间中还未分配给Item的部分)
        } else {
            fprintf(stderr, "Warning: Failed to allocate requested memory in"
                    " one large chunk.\nWill allocate in smaller chunks\n");
        }
    }
  //置空slabclass数组  
    memset(slabclass, 0, sizeof(slabclass));  //sizeof(slabclass)为整个数组大小,而非指针大小
    //开始分配,i<200 && 单个chunk的size<=单个item最大大小/内存增长因子  
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
  //确保item总是8byte对齐
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);//没对齐,则补齐

slabclass[i].size = size;//slab中chunk的大小设为补齐的大小
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;//每个slab中的chunk数量
        size *= factor;//下一个slab中的chunk扩大factor倍
        if (settings.verbose > 1) {//如果有打开调试信息,则输出调试信息 
            fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                    i, slabclass[i].size, slabclass[i].perslab);
        }
    }
 
 //slab数组中的最后一个slab,此时chunk大小增加为1M,因此只有一个chunk
    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) {
  //真正分配空间:分配每个slab的内存空间,传入最大已经初始化的最大slab编号 
        slabs_preallocate(power_largest);
    }
}

//分配每个slabclass数组元素的内存空间  
static void slabs_preallocate (const unsigned int maxslabs) {  
    int i;  
    unsigned int prealloc = 0;  
  
    for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) {  
        if (++prealloc > maxslabs)  
            return;  
        //执行分配操作,对第i个slabclass执行分配操作  
        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);  
        }  
    }  
}

//为第id个slabclass执行分配操作  
static int do_slabs_newslab(const unsigned int id) {  
    slabclass_t *p = &slabclass[id];//p指向第i个slabclass  
    int len = settings.slab_reassign ? settings.item_size_max:p->size*p->perslab;  //len为一个slabclass的大小
    char *ptr;  
    //grow_slab_list初始化slabclass的slab_list,而slab_list中的指针指向每个slab  
    //memory_allocate从内存池申请1M的空间  
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||  
        (grow_slab_list(id) == 0) ||  
        ((ptr = memory_allocate((size_t)len)) == 0)) { //优先从Memchahed内存池中分配,如果内存池为空则从系统分配给定大小内存 
  
        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);  
        return 0;  
    }  
  
    memset(ptr, 0, (size_t)len);  
    //将申请的1M空间按slabclass的size进行切分  
    split_slab_page_into_freelist(ptr, id);  
  
    p->slab_list[p->slabs++] = ptr; 
    mem_malloced += len;//增加已经分配出去的内存数  
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);  
  
    return 1;  
}

//将同级别slab的空间且分为该大小的chunk
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++) {
        do_slabs_free(ptr, 0, id);//创建空闲item  
        ptr += p->size;//指针前移item的大小
    }
}

//创建空闲item ,挂载到对应slabclass的空闲链表中  
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);//判断id有效性  
    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;  
    it->prev = 0;  
    it->next = p->slots;//挂载到slabclass的空闲链表中  
    if (it->next) it->next->prev = it;  
    p->slots = it;  
  
    p->sl_curr++;//空闲item个数+1  
    p->requested -= size;//已经申请到的空间数量更新  
    return;  
}

至此,从创建slabclass数组,到最底层的创建空闲item并挂载到对应的slabclass的空闲链表slots的头部, 的操作完成。

Memcached的内存池由起始地址指针、当前地址指针、剩余可用空间等变量维护,每次内存池操作只需要相应的改变这些变量即可。

以下为内存池分配操作函数:

//优先从Memcached的内存池分配size大小的空间  
static void *memory_allocate(size_t size) {  
    void *ret;  
  
    if (mem_base == NULL) {//如果内存池没创建,则从系统分配  
        ret = malloc(size);  
    } else {  
        ret = mem_current;  
        //size大于剩余的空间  
        if (size > mem_avail) {  
            return NULL;  
        }  
  
        //按8字节对齐  
        if (size % CHUNK_ALIGN_BYTES) {  
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);  
        }  
        //扣除size个空间  
        mem_current = ((char*)mem_current) + size;  
        if (size < mem_avail) {  
            mem_avail -= size;//更新剩余空间大小  
        } else {  
            mem_avail = 0;  
        }  
    }  
  
    return ret;  
}

由此可见,内存池的实现其实是很简单的。当然这里只给出了内存池中内存分配的操作,而内存释放操作也类似。总之,内存池的操作,需要维护内存池的几个指针变量和空间指示变量即可。