Memcached的理论参数计算方式

84086320 2011-11-29

转http://lisily.blog.163.com/blog/static/244145200842982423141/

影响memcached工作的几个参数有:

常量REALTIME_MAXDELTA60*60*24*30

最大30天的过期时间

conn_init()中的freetotal(=200)

最大同时连接数

常量KEY_MAX_LENGTH250

最大键长

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_SMALLEST1

最小classid(1.2)

常量POWER_LARGEST200

最大classid(1.2)

常量POWER_BLOCK1048576

默认slab大小

常量CHUNK_ALIGN_BYTES(sizeof(void*))

保证chunk大小是这个数值的整数倍,防止越界(void*的长度在不同系统上不一样,在标准32位系统上是4)

常量ITEM_UPDATE_INTERVAL60

队列刷新间隔

常量LARGEST_ID255

最大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:[Copytoclipboard]

unsignedintslabs_clsid(size_tsize){

return1;

}

修改slabs_init函数,去掉循环创建所有classid属性的部分,直接添加slabclass[1]:

CODE:[Copytoclipboard]

slabclass[1].size=200;//每chunk200字节

slabclass[1].perslab=5000;//1000000/200

◎Memcached客户端

Memcached是一个服务程序,使用的时候可以根据它的协议,连接到memcached服务器上,发送命令给服务进程,就可以操作上面的数据。为了方便使用,memcached有很多个客户端程序可以使用,对应于各种语言,有各种语言的客户端。基于C语言的有libmemcache、APR_Memcache;基于Perl的有Cache::Memcached;另外还有Python、Ruby、Java、C#等语言的支持。PHP的客户端是最多的,不光有mcache和PECLmemcache两个扩展,还有大把的由PHP编写的封装类,下面介绍一下在PHP中使用memcached的方法:

mcache扩展是基于libmemcache再封装的。libmemcache一直没有发布stable版本,目前版本是1.4.0-rc2,可以在这里找到。libmemcache有一个很不好的特性,就是会向stderr写很多错误信息,一般的,作为lib使用的时候,stderr一般都会被定向到其它地方,比如Apache的错误日志,而且libmemcache会自杀,可能会导致异常,不过它的性能还是很好的。

mcache扩展最后更新到1.2.0-beta10,作者大概是离职了,不光停止更新,连网站也打不开了(~_~),只能到其它地方去获取这个不负责的扩展了。解压后安装方法如常:phpize&configure&make&makeinstall,一定要先安装libmemcache。使用这个扩展很简单:

CODE:[Copytoclipboard]

<?php

$mc=memcache();//创建一个memcache连接对象,注意这里不是用new!

$mc->add_server('localhost',11211);//添加一个服务进程

$mc->add_server('localhost',11212);//添加第二个服务进程

$mc->set('key1','Hello');//写入key1=>Hello

$mc->set('key2','World',10);//写入key2=>World,10秒过期

$mc->set('arr1',array('Hello','World'));//写入一个数组

$key1=$mc->get('key1');//获取'key1'的值,赋给$key1

$key2=$mc->get('key2');//获取'key2'的值,赋给$key2,如果超过10秒,就取不到了

$arr1=$mc->get('arr1');//获取'arr1'数组

$mc->delete('arr1');//删除'arr1'

$mc->flush_all();//删掉所有数据

$stats=$mc->stats();//获取服务器信息

var_dump($stats);//服务器信息是一个数组

?>

这个扩展的好处是可以很方便地实现分布式存储和负载均衡,因为它可以添加多个服务地址,数据在保存的时候是会根据hash结果定位到某台服务器上的,这也是libmemcache的特性。libmemcache支持集中hash方式,包括CRC32、ELF和Perlhash。

PECLmemcache是PECL发布的扩展,目前最新版本是2.1.0,可以在pecl网站得到。memcache扩展的使用方法可以在新一些的PHP手册中找到,它和mcache很像,真的很像:

CODE:[Copytoclipboard]

<?php

$memcache=newMemcache;

$memcache->connect('localhost',11211)ordie("Couldnotconnect");

$version=$memcache->getVersion();

echo"Server'sversion:".$version."n";

$tmp_object=newstdClass;

$tmp_object->str_attr='test';

$tmp_object->int_attr=123;

$memcache->set('key',$tmp_object,false,10)ordie("Failedtosavedataattheserver");

echo"Storedatainthecache(datawillexpirein10seconds)n";

$get_result=$memcache->get('key');

echo"Datafromthecache:n";

var_dump($get_result);

?>

这个扩展是使用php的stream直接连接memcached服务器并通过socket发送命令的。它不像libmemcache那样完善,也不支持add_server这种分布操作,但是因为它不依赖其它的外界程序,兼容性要好一些,也比较稳定。至于效率,差别不是很大。

另外,有很多的PHPclass可以使用,比如MemcacheClient.inc.php,phpclasses.org上可以找到很多,一般都是对perlclientAPI的再封装,使用方式很像。

◎BSM_Memcache

从Cclient来说,APR_Memcache是一个很成熟很稳定的client程序,支持线程锁和原子级操作,保证运行的稳定性。不过它是基于APR的(APR将在最后一节介绍),没有libmemcache的应用范围广,目前也没有很多基于它开发的程序,现有的多是一些ApacheModule,因为它不能脱离APR环境运行。但是APR倒是可以脱离Apache单独安装的,在APR网站上可以下载APR和APR-util,不需要有Apache,可以直接安装,而且它是跨平台的。

BSM_Memcache是我在BS.Magic项目中开发的一个基于APR_Memcache的PHP扩展,说起来有点拗口,至少它把APR扯进了PHP扩展中。这个程序很简单,也没做太多的功能,只是一种形式的尝试,它支持服务器分组。

和mcache扩展支持多服务器分布存储不同,BSM_Memcache支持多组服务器,每一组内的服务器还是按照hash方式来分布保存数据,但是两个组中保存的数据是一样的,也就是实现了热备,它不会因为一台服务器发生单点故障导致数据无法获取,除非所有的服务器组都损坏(例如机房停电)。当然实现这个功能的代价就是性能上的牺牲,在每次添加删除数据的时候都要扫描所有的组,在get数据的时候会随机选择一组服务器开始轮询,一直到找到数据为止,正常情况下一次就可以获取得到。

BSM_Memcache只支持这几个函数:

CODE:[Copytoclipboard]

zend_function_entrybsm_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}

};

mc_add_group函数返回一个整形(其实应该是一个object,我偷懒了~_~)作为组ID,mc_add_server的时候要提供两个参数,一个是组ID,一个是服务器地址(ADDRORT)。

CODE:[Copytoclipboard]

/**

*Addaservergroup

*/

PHP_FUNCTION(mc_add_group)

{

apr_int32_tgroup_id;

apr_status_trv;

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:[Copytoclipboard]

/**

*Addaserverintogroup

*/

PHP_FUNCTION(mc_add_server)

{

apr_status_trv;

apr_int32_tgroup_id;

doubleg;

char*srv_str;

intsrv_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_tport;

rv=apr_parse_addr_port(&host,&scope,&port,srv_str,p);

if(APR_SUCCESS==rv)

{

//Createthisserverobject

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;

}

//Addserver

rv=apr_memcache_add_server(mc_groups[group_id],st);

if(APR_SUCCESS==rv)

{

RETURN_TRUE;

}

}

}

RETURN_FALSE;

}

在set和del数据的时候,要循环所有的组:

CODE:[Copytoclipboard]

/**

*Storeitemintoallgroups

*/

PHP_FUNCTION(mc_set)

{

char*key,*value;

intkey_l,value_l;

doublettl=0;

doubleset_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;

}

//Writedataintoeveryobject

apr_int32_ti=0;

if(ttl<0)

{

ttl=0;

}

apr_status_trv;

for(i=0;i<MAX_GROUP;i++)

{

if(0==is_validate_group(i))

{

//Writeit!

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);

}

在mc_get中,首先要随机选择一个组,然后从这个组开始轮询:

CODE:[Copytoclipboard]

/**

*Fetchaitemfromarandomgroup

*/

PHP_FUNCTION(mc_get)

{

char*key,*value=NULL;

intkey_l;

apr_size_tvalue_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();

}

//Iwilltry...

//Randomread

apr_int32_tcurr_group_id=random_group();

apr_int32_ti=0;

apr_int32_ttry=0;

apr_uint32_tflag;

apr_memcache_t*oper;

apr_status_trv;

for(i=0;i<MAX_GROUP;i++)

{

try=i+curr_group_id;

try=try%MAX_GROUP;

if(0==is_validate_group(try))

{

//Getavalue

oper=mc_groups[try];

rv=apr_memcache_getp(mc_groups[try],p,(constchar*)key,&value,&value_l,0);

if(APR_SUCCESS==rv)

{

RETURN_STRING(value,1);

}

}

}

RETURN_FALSE;

}

CODE:[Copytoclipboard]

/**

*Randomgroupid

*Formc_get()

*/

apr_int32_trandom_group()

{

structtimevaltv;

structtimezonetz;

intusec;

gettimeofday(&tv,&tz);

usec=tv.tv_usec;

intcurr=usec%count_group();

return(apr_int32_t)curr;

}

BSM_Memcache的使用方式和其它的client类似:

CODE:[Copytoclipboard]

<?php

$g1=mc_add_group();//添加第一个组

$g2=mc_add_group();//添加第二个组

mc_add_server($g1,'localhost:11211');//在第一个组中添加第一台服务器

mc_add_server($g1,'localhost:11212');//在第一个组中添加第二台服务器

mc_add_server($g2,'10.0.0.16:11211');//在第二个组中添加第一台服务器

mc_add_server($g2,'10.0.0.17:11211');//在第二个组中添加第二台服务器

mc_set('key','Hello');//写入数据

$key=mc_get('key');//读出数据

mc_del('key');//删除数据

mc_shutdown();//关闭所有组

?>

APR_Memcache的相关资料可以在这里找到,BSM_Memcache可以在本站下载。

◎APR环境介绍

APR的全称:ApachePortableRuntime。它是Apache软件基金会创建并维持的一套跨平台的C语言库。它从Apachehttpd1.x中抽取出来并独立于httpd之外,Apachehttpd2.x就是建立在APR上。APR提供了很多方便的API接口可供使用,包括如内存池、字符串操作、网络、数组、hash表等实用的功能。开发Apache2Module要接触很多APR函数,当然APR可以独立安装独立使用,可以用来写自己的应用程序,不一定是Apachehttpd的相关开发。

相关推荐