http://www.cnblogs.com/moonlove/tag/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/
在上一節中已經分析了memcached的內存分配管理初始化機制,在這節中我們將詳細分析memcached中slab的管理與分配機制。
slabclass[MAX_NUMBER_OF_SLAB_CLASSES]數組是slab管理器(類型見上節),是memcached內存管理的核心數據結構,起着非常重要的作用。
slabclass[i]的內存示意圖如下圖所示:
(1) size和perslab保存着每個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時,對應id的slab鏈表就要增長(由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系統內存的次數,降低函數開銷和內存碎片產生機率,雖然這種方式會造成一些冗餘浪費,但是這種浪費在大型系統應用中是微不足道的。