Memcached是danga.com(运营LiveJournal的技术团队)开发的一套分布式内存对象缓存系统,用于在动态系统中减少数据库负载, 提升性能。关于这个东西,相信很多人都用过,本文意在通过对memcached的实现及代码分析,获得对这个出色的开源软件更深入的了解,并可以根据我们 的需要对其进行更进一步的优化。末了将通过对BSM_Memcache扩展的分析,加深对memcached的使用方式理解。
本文的部分内容可能需要比较好的数学基础作为辅助。◎Memcached是什么 在阐述这个问题之前,我们首先要清楚它“不是什么”。很多人把它当作和SharedMemory那种形式的存储载体来使用,虽然memcached使用了 同样的“Key=>Value”方式组织数据,但是它和共享内存、APC等本地缓存有非常大的区别。Memcached是分布式的,也就是说它不是 本地的。它基于网络连接(当然它也可以使用localhost)方式完成服务,本身它是一个独立于应用的程序或守护进程(Daemon方式)。 Memcached使用libevent库实现网络连接服务,理论上可以处理无限多的连接,但是它和Apache不同,它更多的时候是面向稳定的持续连接 的,所以它实际的并发能力是有限制的。在保守情况下memcached的最大同时连接数为200,这和Linux线程能力有关系,这个数值是可以调整的。 关于libevent可以参考相关文档。 Memcached内存使用方式也和APC不同。APC是基于共享内存和MMAP的,memcachd有自己的内存分配算法和管理方式,它和共享内存没有 关系,也没有共享内存的限制,通常情况下,每个memcached进程可以管理2GB的内存空间,如果需要更多的空间,可以增加进程数。 ◎Memcached适合什么场合 在很多时候,memcached都被滥用了,这当然少不了对它的抱怨。我经常在论坛上看见有人发贴,类似于“如何提高效率”,回复是“用memcached”,至于怎么用,用在哪里,用来干什么一句没有。memcached不是万能的,它也不是适用在所有场合。 Memcached是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小到只有一台服务器的应用, memcached不会带来任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX本地连接也一样。 在我之前的测试数据中显示,memcached本地读写速度要比直接PHP内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。可见,如果只是 本地级缓存,使用memcached是非常不划算的。 Memcached在很多时候都是作为数据库前端cache使用的。因为它比数据库少了很多SQL解析、磁盘操作等开销,而且它是使用内存来管理数据的, 所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降低数据库压力,使系统执行效率提升。 另外,memcached也经常作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就可以保存在memcached中,被 多个应用共享。 需要注意的是,memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以 memcached不能用来持久保存数据。很多人的错误理解,memcached的性能非常好,好到了内存和硬盘的对比程度,其实memcached使用 内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘的数据库系统相比,好处在于它本身非常“轻”,因为没有过多的开销和直接 的读写方式,它可以轻松应付非常大的数据交换量,所以经常会出现两条千兆网络带宽都满负荷了,memcached进程本身并不占用多少CPU资源的情况。◎Memcached的工作方式 以下的部分中,读者最好能准备一份memcached的源代码。 Memcached是传统的网络服务程序,如果启动的时候使用了-d参数,它会以守护进程的方式执行。创建守护进程由daemon.c完成,这个程序只有一个daemon函数,这个函数很简单(如无特殊说明,代码以1.2.1为准):
CODE: #include <fcntl.h> #include <stdlib.h> #include <unistd.h> int daemon(nochdir, noclose) int nochdir, noclose; { int fd; switch (fork()) { case -1: return (-1); case 0: break; default: _exit(0); } if (setsid() == -1) return (-1); if (!nochdir) (void)chdir("/"); if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) { (void)dup2(fd, STDIN_FILENO); (void)dup2(fd, STDOUT_FILENO); (void)dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void)close(fd); } return (0); } |
CODE: void slabs_init(size_t limit) { int i; int size=1; mem_limit = limit; for(i=0; i<=POWER_LARGEST; i++, size*=2) { slabclass[i].size = size; slabclass[i].perslab = POWER_BLOCK / size; slabclass[i].slots = 0; slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0; slabclass[i].end_page_ptr = 0; slabclass[i].end_page_free = 0; slabclass[i].slab_list = 0; slabclass[i].list_size = 0; slabclass[i].killing = 0; } /* 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 = atol(getenv("T_MEMD_INITIAL_MALLOC")); } } /* pre-allocate slabs by default, unless the environment variable for testing is set to something non-zero */ { char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC"); if (!pre_alloc || atoi(pre_alloc)) { slabs_preallocate(limit / POWER_BLOCK); } } } |
CODE: void slabs_init(size_t limit, double factor) { int i = POWER_SMALLEST - 1; unsigned int size = sizeof(item) + settings.chunk_size; /* Factor of 2.0 means use the default memcached behavior */ if (factor == 2.0 && size < 128) size = 128; mem_limit = limit; memset(slabclass, 0, sizeof(slabclass)); while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) { /* 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 = POWER_BLOCK / slabclass[i].size; size *= factor; if (settings.verbose > 1) { fprintf(stderr, "slab class %3d: chunk size %6d perslab %5d\n", i, slabclass[i].size, slabclass[i].perslab); } } power_largest = i; slabclass[power_largest].size = POWER_BLOCK; slabclass[power_largest].perslab = 1; /* 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 = atol(getenv("T_MEMD_INITIAL_MALLOC")); } } #ifndef DONT_PREALLOC_SLABS { char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC"); if (!pre_alloc || atoi(pre_alloc)) { slabs_preallocate(limit / POWER_BLOCK); } } #endif } |
CODE: static int grow_slab_list (unsigned int id) { slabclass_t *p = &slabclass[id]; if (p->slabs == p->list_size) { size_t new_size = p->list_size ? 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; } |
CODE: typedef unsigned long int ub4; /* unsigned 4-byte quantities */ typedef unsigned char ub1; /* unsigned 1-byte quantities */ #define hashsize(n) ((ub4)1<<(n)) #define hashmask(n) (hashsize(n)-1) |
CODE: /* expires items that are more recent than the oldest_live setting. */ void item_flush_expired() { int i; item *iter, *next; if (! settings.oldest_live) return; for (i = 0; i < LARGEST_ID; i++) { /* The LRU is sorted in decreasing time order, and an item's timestamp * is never newer than its last access time, so we only need to walk * back until we hit an item older than the oldest_live time. * The oldest_live checking will auto-expire the remaining items. */ for (iter = heads[i]; iter != NULL; iter = next) { if (iter->time >= settings.oldest_live) { next = iter->next; if ((iter->it_flags & ITEM_SLABBED) == 0) { item_unlink(iter); } } else { /* We've hit the first old item. Continue to the next queue. */ break; } } } } |
CODE: /* wrapper around assoc_find which does the lazy expiration/deletion logic */ item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) { item *it = assoc_find(key, nkey); if (delete_locked) *delete_locked = 0; if (it && (it->it_flags & ITEM_DELETED)) { /* it's flagged as delete-locked. let's see if that condition is past due, and the 5-second delete_timer just hasn't gotten to it yet... */ if (! item_delete_lock_over(it)) { if (delete_locked) *delete_locked = 1; it = 0; } } if (it && settings.oldest_live && settings.oldest_live <= current_time && it->time <= settings.oldest_live) { item_unlink(it); it = 0; } if (it && it->exptime && it->exptime <= current_time) { item_unlink(it); it = 0; } return it; } |
◎Memcached的理论参数计算方式影响 memcached 工作的几个参数有:
常量REALTIME_MAXDELTA 60*60*24*30 最大30天的过期时间 conn_init()中的freetotal(=200) 最大同时连接数 常量KEY_MAX_LENGTH 250 最大键长 settings.factor(=1.25) factor将影响chunk的步进大小 settings.maxconns(=1024) 最大软连接 settings.chunk_size(=48) 一个保守估计的key+value长度,用来生成id1中的chunk长度(1.2)。id1的chunk长度等于这个数值加上item结构体的长度(32),即默认的80字节。 常量POWER_SMALLEST 1 最小classid(1.2) 常量POWER_LARGEST 200 最大classid(1.2) 常量POWER_BLOCK 1048576 默认slab大小 常量CHUNK_ALIGN_BYTES (sizeof(void *)) 保证chunk大小是这个数值的整数倍,防止越界(void *的长度在不同系统上不一样,在标准32位系统上是4) 常量ITEM_UPDATE_INTERVAL 60 队列刷新间隔 常量LARGEST_ID 255 最大item链表数(这个值不能比最大的classid小) 变量hashpower(在1.1中是常量HASHPOWER) 决定hashtable的大小 根据上面介绍的内容及参数设定,可以计算出的一些结果: 1、在memcached中可以保存的item个数是没有软件上限的,之前我的100万的说法是错误的。 2、假设NewHash算法碰撞均匀,查找item的循环次数是item总数除以hashtable大小(由hashpower决定),是线性的。 3、Memcached限制了可以接受的最大item是1MB,大于1MB的数据不予理会。 4、Memcached的空间利用率和数据特性有很大的关系,又与DONT_PREALLOC_SLABS常量有关。 在最差情况下,有198个slab会被浪费(所有item都集中在一个slab中,199个id全部分配满)。 ◎Memcached的定长优化 根据上面几节的描述,多少对memcached有了一个比较深入的认识。在深入认识的基础上才好对它进行优化。 Memcached本身是为变长数据设计的,根据数据特性,可以说它是“面向大众”的设计,但是很多时候,我们的数据并不是这样的“普遍”,典型的情况 中,一种是非均匀分布,即数据长度集中在几个区域内(如保存用户 Session);另一种更极端的状态是等长数据(如定长键值,定长数据,多见于访问、在线统计或执行锁)。 这里主要研究一下定长数据的优化方案(1.2),集中分布的变长数据仅供参考,实现起来也很容易。 解决定长数据,首先需要解决的是slab的分配问题,第一个需要确认的是我们不需要那么多不同chunk长度的slab,为了最大限度地利用资源,最好chunk和item等长,所以首先要计算item长度。 在之前已经有了计算item长度的算法,需要注意的是,除了字符串长度外,还要加上item结构的长度32字节。 假设我们已经计算出需要保存200字节的等长数据。 接下来是要修改slab的classid和chunk长度的关系。在原始版本中,chunk长度和classid是有对应关系的,现在如果把所有的 chunk都定为200个字节,那么这个关系就不存在了,我们需要重新确定这二者的关系。一种方法是,整个存储结构只使用一个固定的id,即只使用199 个槽中的1个,在这种条件下,就一定要定义DONT_PREALLOC_SLABS来避免另外的预分配浪费。另一种方法是建立一个hash关系,来从 item确定classid,不能使用长度来做键,可以使用key的NewHash结果等不定数据,或者直接根据key来做hash(定长数据的key也 一定等长)。这里简单起见,选择第一种方法,这种方法的不足之处在于只使用一个id,在数据量非常大的情况下,slab链会很长(因为所有数据都挤在一条 链上了),遍历起来的代价比较高。 前面介绍了三种空间冗余,设置chunk长度等于item长度,解决了第一种空间浪费问题,不预申请空间解决了第二种空间浪费问题,那么对于第一种问题 (slab内剩余)如何解决呢,这就需要修改POWER_BLOCK常量,使得每一个slab大小正好等于chunk长度的整数倍,这样一个slab就可 以正好划分成n个chunk。这个数值应该比较接近1MB,过大的话同样会造成冗余,过小的话会造成次数过多的alloc,根据chunk长度为200, 选择1000000作为POWER_BLOCK的值,这样一个slab就是100万字节,不是1048576。三个冗余问题都解决了,空间利用率会大大提 升。 修改 slabs_clsid 函数,让它直接返回一个定值(比如 1 ):
CODE: unsigned int slabs_clsid(size_t size) { return 1; } |
CODE: slabclass[1].size = 200; //每chunk200字节 slabclass[1].perslab = 5000; //1000000/200 |
CODE: zend_function_entry bsm_memcache_functions[] = { PHP_FE(mc_get, NULL) PHP_FE(mc_set, NULL) PHP_FE(mc_del, NULL) PHP_FE(mc_add_group, NULL) PHP_FE(mc_add_server, NULL) PHP_FE(mc_shutdown, NULL) {NULL, NULL, NULL} }; |
CODE: /** * Add a server group */ PHP_FUNCTION(mc_add_group) { apr_int32_t group_id; apr_status_t rv; if (0 != ZEND_NUM_ARGS()) { WRONG_PARAM_COUNT; RETURN_NULL(); } group_id = free_group_id(); if (-1 == group_id) { RETURN_FALSE; } apr_memcache_t *mc; rv = apr_memcache_create(p, MAX_G_SERVER, 0, &mc); add_group(group_id, mc); RETURN_DOUBLE(group_id); } |
CODE: /** * Add a server into group */ PHP_FUNCTION(mc_add_server) { apr_status_t rv; apr_int32_t group_id; double g; char *srv_str; int srv_str_l; if (2 != ZEND_NUM_ARGS()) { WRONG_PARAM_COUNT; } if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ds", &g, &srv_str, &srv_str_l) == FAILURE) { RETURN_FALSE; } group_id = (apr_int32_t) g; if (-1 == is_validate_group(group_id)) { RETURN_FALSE; } char *host, *scope; apr_port_t port; rv = apr_parse_addr_port(&host, &scope, &port, srv_str, p); if (APR_SUCCESS == rv) { // Create this server object apr_memcache_server_t *st; rv = apr_memcache_server_create(p, host, port, 0, 64, 1024, 600, &st); if (APR_SUCCESS == rv) { if (NULL == mc_groups[group_id]) { RETURN_FALSE; } // Add server rv = apr_memcache_add_server(mc_groups[group_id], st); if (APR_SUCCESS == rv) { RETURN_TRUE; } } } RETURN_FALSE; } |
CODE: /** * Store item into all groups */ PHP_FUNCTION(mc_set) { char *key, *value; int key_l, value_l; double ttl = 0; double set_ct = 0; if (2 != ZEND_NUM_ARGS()) { WRONG_PARAM_COUNT; } if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|d", &key, &key_l, &value, &value_l, ttl) == FAILURE) { RETURN_FALSE; } // Write data into every object apr_int32_t i = 0; if (ttl < 0) { ttl = 0; } apr_status_t rv; for (i = 0; i < MAX_GROUP; i++) { if (0 == is_validate_group(i)) { // Write it! rv = apr_memcache_add(mc_groups[i], key, value, value_l, (apr_uint32_t) ttl, 0); if (APR_SUCCESS == rv) { set_ct++; } } } RETURN_DOUBLE(set_ct); } |
CODE: /** * Fetch a item from a random group */ PHP_FUNCTION(mc_get) { char *key, *value = NULL; int key_l; apr_size_t value_l; if (1 != ZEND_NUM_ARGS()) { WRONG_PARAM_COUNT; } if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &key, &key_l) == FAILURE) { RETURN_MULL(); } // I will try ... // Random read apr_int32_t curr_group_id = random_group(); apr_int32_t i = 0; apr_int32_t try = 0; apr_uint32_t flag; apr_memcache_t *oper; apr_status_t rv; for (i = 0; i < MAX_GROUP; i++) { try = i + curr_group_id; try = try % MAX_GROUP; if (0 == is_validate_group(try)) { // Get a value oper = mc_groups[try]; rv = apr_memcache_getp(mc_groups[try], p, (const char *) key, &value, &value_l, 0); if (APR_SUCCESS == rv) { RETURN_STRING(value, 1); } } } RETURN_FALSE; } |
CODE: /** * Random group id * For mc_get() */ apr_int32_t random_group() { struct timeval tv; struct timezone tz; int usec; gettimeofday(&tv, &tz); usec = tv.tv_usec; int curr = usec % count_group(); return (apr_int32_t) curr; } |