[原]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 关键数据结构
(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/