Memcached源码分析 - 网络模型(1)
Memcached源码分析 - 命令解析(2)
Memcached源码分析 - 数据存储(3)
Memcached源码分析 - 增删改查操作(4)
Memcached源码分析 - 内存存储机制Slabs(5)
Memcached源码分析 - LRU淘汰算法(6)
Memcached源码分析 - 消息回应(7)
开篇
这篇文章的目的是想把Memcached的内存管理机制讲解清楚,在前面的文章中我们已经提交到Item是Memcached中存储的数据单元,而Item的内存分配策略就是本章的重点了。
Memcached的存储机制Slabs,所有Item相关的内存分配都由Slabs来实现的。
Item回顾
- Item是Memcached存储的最小单位
- 每一个缓存都会有自己的一个Item数据结构
- Item主要存储缓存的key、value、key的长度、value的长度、缓存的时间等信息。
- HashTable和LRU链表结构都是依赖Item结构中的元素的。
- 在Memcached中,Item扮演着重要的角色。
typedef struct _stritem {
/* Protected by LRU locks */
struct _stritem *next;
struct _stritem *prev;
/* Rest are protected by an item lock */
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;
Memcached默认配置
static void settings_init(void) {
settings.use_cas = true;
settings.access = 0700;
settings.port = 11211;
settings.udpport = 0;
/* 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.oldest_cas = 0; /* supplements accuracy of oldest_live */
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. */
settings.slab_page_size = 1024 * 1024; /* chunks are split from 1MB pages. */
settings.slab_chunk_size_max = settings.slab_page_size / 2;
settings.sasl = false;
settings.maxconns_fast = true;
settings.lru_crawler = false;
settings.lru_crawler_sleep = 100;
settings.lru_crawler_tocrawl = 0;
settings.lru_maintainer_thread = false;
settings.lru_segmented = true;
settings.hot_lru_pct = 20;
settings.warm_lru_pct = 40;
settings.hot_max_factor = 0.2;
settings.warm_max_factor = 2.0;
settings.inline_ascii_response = false;
settings.temp_lru = false;
settings.temporary_ttl = 61;
settings.idle_timeout = 0; /* disabled */
settings.hashpower_init = 0;
settings.slab_reassign = true;
settings.slab_automove = 1;
settings.slab_automove_ratio = 0.8;
settings.slab_automove_window = 30;
settings.shutdown_command = false;
settings.tail_repair_time = TAIL_REPAIR_TIME_DEFAULT;
settings.flush_enabled = true;
settings.dump_enabled = true;
settings.crawls_persleep = 1000;
settings.logger_watcher_buf_size = LOGGER_WATCHER_BUF_SIZE;
settings.logger_buf_size = LOGGER_BUF_SIZE;
settings.drop_privileges = false;
#ifdef MEMCACHED_DEBUG
settings.relaxed_privileges = false;
#endif
}
slabclass 空间分配
slabclass_t结构的核心字段已经进行注释了,核心void *slots表示空间的chunk列表,其实也就是item列表。
typedef struct {
unsigned int size; //当前的slabclass存储最大多大的item
unsigned int perslab; //每一个slab上可以存储多少个item.每个slab大小为1M, 可以存储的item个数根据size决定。
void *slots; //空闲列表,其实是连续的内存分配,但是还是通过item结构中的item->next和item->prev连建立链表结构关系
unsigned int sl_curr; //当sl_curr=0的时候,说明已经没有空闲的item,需要分配一个新的slab(每个1M,可以切割成N多个Item结构)
unsigned int slabs; //总共分配多少个slabs
void **slab_list; //分配的slab链表
unsigned int list_size; /* size of prev array */
size_t requested; //总共请求的总bytes
} slabclass_t;
从这里开始进入slabclass内存初始化的核心逻辑,整体过程如下:
slabclass 划分数据空间
- Memcached在启动的时候,会初始化一个slabclass数组,该数组用于存储最大63个slabclass_t的数据结构体。
- Memcached并不会将所有大小的数据都会放置在一起,而是预先将数据空间划分为一系列的slabclass_t。
- 每个slabclass_t,都只存储一定大小范围的数据。slabclass数组中,前一个slabclass_t可以存储的数据大小要小于下一个slabclass_t结构可以存储的数据大小。
- 例如:slabclass[3]只存储大小介于120 (slabclass[2]的最大值)到 150 bytes的数据。如果一个数据大小为134byte将被分配到slabclass[3]中。
- memcached默认情况下下一个slabclass_t存储数据的最大值为前一个的1.25倍(settings.factor),这个可以通过修 改-f参数来修改增长比例。
slab 内存分配单位
- Memcached的内存分配是以slab为单位的。默认情况下,每个slab大小为1M。
- slabclass数组初始化的时候,每个slabclass_t都会分配一个1M大小的slab。
- 当某个slabclass_t结构上的内存不够的时候(freelist空闲列表为空),则会分配一个slab给这个slabclass_t结构。
- 一旦slab分配后,不可回收。
- slab会被切分为N个小的内存块,这个小的内存块的大小取决于slabclass_t结构上的size的大小。例如slabclass[0]上的size为103,则每个小的内存块大小为103byte。
- 这些被切割的小的内存块,主要用来存储item。但是,存储的item,可能会比切割出来的内存块会小。因为这是为了防止内存碎片,虽然有一些内存的浪费。
对应到源码当中:
- memset(slabclass, 0, sizeof(slabclass));负责初始化slabclass[MAX_NUMBER_OF_SLAB_CLASSES]数组。
- while循环设置slabclass[i].size和slabclass[i].perslab,以size *= factor递增。
- slabs_preallocate开始进入slabclass的slab初始化。
- do_slabs_newslab负责初始化指定的slabclass的第一个slab的初始化,int len =p->size * p->perslab;memory_allocate((size_t)len)。
- p->slab_list当中保存slabclass当中所有的slab信息,这里保存第一个。
- split_slab_page_into_freelist当中针slabclass当中的第一个slab进行初始化,item个数是由p->perslab指定的。
- do_slabs_free当中只是进行了类型强转而已,把指针转换为item而已。
- do_slabs_free_chunked就是把slab当中的所有chunk也就是item进行初始化关联到p->slots链表当中。
#define POWER_SMALLEST 1
#define SLAB_GLOBAL_PAGE_POOL 0
#define POWER_LARGEST 256 /* actual cap is 255 */
#define MAX_NUMBER_OF_SLAB_CLASSES (63 + 1)
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];
void slabs_init(const size_t limit, const double factor, const bool prealloc, const uint32_t *slab_sizes) {
int i = POWER_SMALLEST - 1;
// 设置每个slabclass中item的size大小
unsigned int size = sizeof(item) + settings.chunk_size;
memset(slabclass, 0, sizeof(slabclass));
// 每次以1.5倍的大小增加slabclass中item的size大小
while (++i < MAX_NUMBER_OF_SLAB_CLASSES-1) {
if (slab_sizes != NULL) {
if (slab_sizes[i-1] == 0)
break;
size = slab_sizes[i-1];
} else if (size >= settings.slab_chunk_size_max / factor) {
// 如果已经达到了最大值settings.slab_chunk_size_max
break;
}
// 执行对齐操作
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
//每个slabclass[i]存储最大多大的item,size表示item的大小
slabclass[i].size = size;
slabclass[i].perslab = settings.slab_page_size / slabclass[i].size;
// 这里以1.5倍的速率增加slabclass中item的大小
if (slab_sizes == NULL)
size *= factor;
}
// 根据settings.slab_chunk_size_max的值设置最多多少个slabclass的个数
power_largest = i;
slabclass[power_largest].size = settings.slab_chunk_size_max;
slabclass[power_largest].perslab = settings.slab_page_size / settings.slab_chunk_size_max;
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = (size_t)atol(t_initial_malloc);
}
}
// 进入slabclass的分配
if (prealloc && do_slab_prealloc) {
slabs_preallocate(power_largest);
}
}
static void slabs_preallocate (const unsigned int maxslabs) {
int i;
unsigned int prealloc = 0;
// 遍历每个slabclass进行初始化,这里会综合考虑最大的支持的slabclass和根据内存分配设置的合适的个数,
//这里的POWER_SMALLEST的值是1,slabclass的数组的第一个元素是特殊意义的
for (i = POWER_SMALLEST; i < MAX_NUMBER_OF_SLAB_CLASSES; i++) {
if (++prealloc > maxslabs)
return;
// 进行slabclass的分配
if (do_slabs_newslab(i) == 0) {
exit(1);
}
}
}
// 这里的id就是slabclass的下标,这里已经进入到单个slabclass初始化过程了
static int do_slabs_newslab(const unsigned int id) {
slabclass_t *p = &slabclass[id];
slabclass_t *g = &slabclass[SLAB_GLOBAL_PAGE_POOL];
// 这里我们在计算slab的大小
int len = (settings.slab_reassign || settings.slab_chunk_size_max != settings.slab_page_size)
? settings.slab_page_size
: p->size * p->perslab;
char *ptr;
if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0
&& g->slabs == 0)) {
mem_limit_reached = true;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}
// 通过memory_allocate函数分配len大小的内存空间
if ((grow_slab_list(id) == 0) ||
(((ptr = get_page_from_global_pool()) == NULL) &&
((ptr = memory_allocate((size_t)len)) == 0))) {
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}
// 初始化该块内存为0
memset(ptr, 0, (size_t)len);
split_slab_page_into_freelist(ptr, id);
// slab_list保存slabs个数的slab的指针
p->slab_list[p->slabs++] = ptr;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);
return 1;
}
//初始化单个slabclass下第一个slab当中的item。
static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
slabclass_t *p = &slabclass[id];
int x;
//遍历slab下的perslab个数的size大小的内存块,将所有的内存块挂到free_list当中
for (x = 0; x < p->perslab; x++) {
do_slabs_free(ptr, 0, id);
ptr += p->size;
}
}
//初始化slab当中单个item的内存空间
static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
slabclass_t *p;
item *it;
p = &slabclass[id];
it = (item *)ptr;
if ((it->it_flags & ITEM_CHUNKED) == 0) {
//省略相关代码
} else {
do_slabs_free_chunked(it, size);
}
return;
}
// 采用链表的头部插入法生成p->slots,也就是说slab当中分配的item以链表的形式进行连接并保存到p->slots当中
static void do_slabs_free_chunked(item *it, const size_t size) {
item_chunk *chunk = (item_chunk *) ITEM_data(it);
slabclass_t *p;
it->it_flags = ITEM_SLABBED;
it->slabs_clsid = 0;
it->prev = 0;
// header object's original classid is stored in chunk.
p = &slabclass[chunk->orig_clsid];
if (chunk->next) {
chunk = chunk->next;
chunk->prev = 0;
} else {
// header with no attached chunk
chunk = NULL;
}
// return the header object.
// TODO: This is in three places, here and in do_slabs_free().
it->prev = 0;
it->next = p->slots;
if (it->next) it->next->prev = it;
p->slots = it;
p->sl_curr++;
// TODO: macro
p->requested -= it->nkey + 1 + it->nsuffix + sizeof(item) + sizeof(item_chunk);
if (settings.use_cas) {
p->requested -= sizeof(uint64_t);
}
item_chunk *next_chunk;
while (chunk) {
assert(chunk->it_flags == ITEM_CHUNK);
chunk->it_flags = ITEM_SLABBED;
p = &slabclass[chunk->slabs_clsid];
chunk->slabs_clsid = 0;
next_chunk = chunk->next;
chunk->prev = 0;
chunk->next = p->slots;
if (chunk->next) chunk->next->prev = chunk;
p->slots = chunk;
p->sl_curr++;
p->requested -= chunk->size + sizeof(item_chunk);
chunk = next_chunk;
}
return;
}
slabclass内存分配图
其实两幅图都是对的,但是侧重点稍微不一样。
- 第一幅主要想说明各个chunk也就是item之间是连续的一块内存分配的。
- 第二幅图主要想说明的各个chunk也就是item之间额外通过item的指针进行链接并且形成空间的item链表由slabclass指针维护。
slabs_clsid - 查询slabclass的ID
slabs_clsid方法,主要通过item的长度来查询应该适合存放到哪个slabsclass_t上面。找的逻辑就是简单的找到第一个可以容纳得下size大小的slabclass。
//通过item的size,选择当前的item适合放在哪个slab class中
unsigned int slabs_clsid(const size_t size) {
int res = POWER_SMALLEST; //从id = 1开始查找
//slabclass这个结构上的size会存储该class适合多大的item存储
//例如
//slabclass[0] 存储96byte
//slabclass[1] 存储120byte
//slabclass[2] 存储150byte
//则,如果存储的item等于109byte,则存储在slabclass[1]上
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;
}