Memcached内存存储

早就据说过Memcached独特的内存管理方式,写着篇文章的目的就是了解Memcached的内存管理,学习其源代码.数组

1.什么是Slab Allocator

memcached默认状况下采用了名为Slab Allocator的机制分配、管理内存,Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以指望彻底解决内存碎片问题。并且,slab allocator还有重复使用已分配的内存的目的。 也就是说,分配到的内存不会释放,而是重复利用。缓存

2.Slab Allocation的主要术语

Page        分配给Slab的内存空间,默认是1MB,分配给Slab以后根据slab的大小切分红chunk
Chunk       用于缓存记录的内存空间
Slab Class  特定大小的chunk的组

3.Slab初始化

在Memcached启动时候会调用slab的初始化代码(详见memcached.c中main函数调用slabs_init函数).memcached

slabs_init函数声明:函数

1
2 3 4 5 6 7 
/** Init the subsystem. 1st argument is the limit on no. of bytes to allocate,  0 if no limit. 2nd argument is the growth factor; each slab will use a chunk  size equal to the previous slab's chunk size times this factor.  3rd argument specifies if the slab allocator should allocate all memory  up front (if true), or allocate memory in chunks as it is needed (if false) */ void slabs_init(const size_t limit, const double factor, const bool prealloc); 

其中limit表示memcached最大使用内存;factor表示slab中chunk size的增加因子,slab中chunk size的大小等于前一个slab的chunk size乘以factor;学习

memcached.c中main函数调用slabs_init函数:ui

1
slabs_init(settings.maxbytes, settings.factor, preallocate); 

其中settings.maxbytes默认值为64M,启动memcached使用选项-m设置;settings.factor默认为1.25,启动memcached时候使用-f设置;preallocate指的是启动memcached的时候默认为每种类型slab预先分配一个page的内存,默认是false;this

1
2 3 4 5 
settings.maxbytes = 64 * 1024 * 1024; /* default is 64MB */ ... settings.factor = 1.25; ... preallocate = false 

slabs_init函数实现:spa

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 
/**  * Determines the chunk sizes and initializes the slab class descriptors  * accordingly.  */ void slabs_init(const size_t limit, const double factor, const bool prealloc) {  int i = POWER_SMALLEST - 1;  //真实占用大小=对象大小+48  unsigned int size = sizeof(item) + settings.chunk_size;   mem_limit = limit;   //开启预分配,则首先将limit大小(默认64M)的内存所有申请  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;  } else {  fprintf(stderr, "Warning: Failed to allocate requested memory in"  " one large chunk.\nWill allocate in smaller chunks\n");  }  }   //清空全部的slab  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;  if (settings.verbose > 1) {  fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",  i, slabclass[i].size, slabclass[i].perslab);  }  }   //最大chunksize的一个slab,chunksize为settings.item_size_max(默认1M)  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);  }  }   //开启了预分配,则为每种slab都分配一个page的空间  if (prealloc) {  slabs_preallocate(power_largest);  } } 

其中settings.chunk_size默认为48:.net

settings.chunk_size = 48;         /* space for a modest key and value */

POWER_LARGEST指slab种类的最大值,默认只为200,在memcached.c中设置3d

#define POWER_LARGEST  200

settings.item_size_max就是每一个page的大小,默认1M,在memcached.c中初始化:

settings.item_size_max = 1024 * 1024; /* The famous 1MB upper limit. */

默认不开启预分配,由于不少时候Memcached只存储一种类型的数据(即其大小相对比较固定),这时候其余类型的预分配的slab空间就会浪费.

预分配的逻辑就是从最小的slab开始,为每类slab分配一个Page大小的空间(空间不足时中止分配):

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 
static void slabs_preallocate (const unsigned int maxslabs) {  int i;  unsigned int prealloc = 0;   /* pre-allocate a 1MB slab in every size class so people don't get  confused by non-intuitive "SERVER_ERROR out of memory"  messages. this is the most common question on the mailing  list. if you really don't want this, you can rebuild without  these three lines. */   for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) {  if (++prealloc > maxslabs)  return;  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);  }  }  } 

do_slabs_newslab的工做就是为某一个slab分配空间,并将空间划分乘固定大小的chunk:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 
static int do_slabs_newslab(const unsigned int id) {  slabclass_t *p = &slabclass[id];  int len = settings.slab_reassign ? settings.item_size_max  : 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)) { //申请内存   MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);  return 0;  }   memset(ptr, 0, (size_t)len);  //将内存划分乘chunk  split_slab_page_into_freelist(ptr, id);   //维护slab链表  p->slab_list[p->slabs++] = ptr;  mem_malloced += len;  MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);   return 1; } 

split_slab_page_into_freelist的主要控制就是Page划分乘chunk并清空:

1
2 3 4 5 6 7 8 
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);  ptr += p->size;  } } 

memcached的内存分配策略就是:按slab需求分配page,各slab按需使用chunk存储.

按需分配的意思就是某一类slab没有对象可存,就不会分配(非preallocate模式),某类slab存储对象不少,就会分配多个slab造成链表.

这里有几个特色要注意:

1.Memcached分配出去的page不会被回收或者从新分配;
2.Memcached申请的内存不会被释放;
3.slab空闲的chunk不会借给任何其余slab使用(新版本memcached有slab_reassign,slab_automove的功能);

slab内存结构图,二维数组链表:

4.往Slab中缓存记录

memcached根据收到的数据的大小,选择最适合数据大小的slab. memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk, 而后将数据缓存于其中.

代码以下:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 
/*  * 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; //最小slab编号   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; } 

参数是待存储对象的大小,根据这个大小,从最小的Chunk Size开始查找,找到第一个(即最小的)能放下size大小的对象的Chunk.找不到(size大于最大的Chunk Size)返回0(这就是为何slab class从1开始而不是从0开始).

若是某个Slab没有剩余的Chunk了,系统便会给这个Slab分配一个新的Page以供使用,若是没有Page可用,系统就会触发LRU机制,经过删除冷数据来为新数据腾出空间,这里有一点须要注意的是:LRU不是全局的,而是针对Slab而言的.

slab内存分配示例:

5.Slab Allocator的缺点

因为Slab Allocator分配的是特定长度的内存,所以没法有效利用分配的内存。 例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了。

6.Memcached减小内存浪费

4.1:调整growth factor

(1).估算咱们item的大小
key键长+suffix+value值长+结构大小(48字节)
(2).逐步调整growth factor,使得某个slab的大小和咱们的item大小接近(必须大于咱们item的大小)

7.过时数据

(1).LRU过时策略;
(2).在slab级别上执行LRU策略;
(3).查看是否过去是在get的时候,即懒惰(lazy)检查;

8.memcached-tool脚本

memcached-tool脚本能够方便地得到slab的使用状况 (它将memcached的返回值整理成容易阅读的格式),能够从下面的地址得到脚本: http://www.netingcn.com/demo/memcached-tool.zip

使用方法也极其简单:

1
perl memcached-tool server_ip:prot option 

好比:

1
2 3 4 5 
perl memcached-tool 10.0.0.5:11211 display # shows slabs perl memcached-tool 10.0.0.5:11211 # same. (default is display) perl memcached-tool 10.0.0.5:11211 stats # shows general stats perl memcached-tool 10.0.0.5:11211 move 7 9 # takes 1MB slab from class #7  # to class #9. 

输出示例:

1
2 3 4 
# Item_Size Max_age 1MB_pages Count Full?  1 104 B 1394292 s 1215 12249628 yes  2 136 B 1456795 s 52 400919 yes  ... 

各列的含义为:

#           slab class编号
Item_Size   Chunk大小
Max_age     LRU内最旧的记录的生存时间
1MB_pages   分配给Slab的页数
Count       Slab内的记录数
Full?       Slab内是否含有空闲chunk