最近开始硬着头皮阅读Memcached的源码,记录下发现的细节和学习到小技巧吧,由于我自己很久没有写过c了,还随便复习了下c的基础知识。
1、第一个slab的chunk size值
当我试图修改memcached的起始slab大小以便用于存储非常小的缓存对象(往往值是没有意义的,只是用一个1之类的标志来表示key是否存在,这个通常用于判断缓存一种状态而不是数值,例如是否获得过当天的登陆奖励)时发现了这个问题。
通过启动memcached时添加参数: -n48 (48是要被设置的值,必须是正数) 可以指定第一个slab的chunk大小,但是经过尝试和跟踪代码发现,第一个slab的chunk size不止受此一个参数限制,具体计算第一个chunk size的过程如下:
- -n 指定的值被存放到 settings.chunk_size中
- 当memcached启动时,会初始化slab(Slabs.c中的slabs_init),第一个slab的chunk size大小首先会被指定为
unsigned int size = sizeof(item) + settings.chunk_size;
然后在实际确定前需要转换为8的整数倍
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES – (size % CHUNK_ALIGN_BYTES);
通过上面的计算过程可以发现,其实第一个slab的chunk size还受到struct item的大小决定。
在尝试使用最小值时(即指定-n1时),在我的机器32位机器上生成的第一个slab大小是48,原因是 sizeof(item) 为32,加上1以后33,而33不是8的倍数,所以放大到48;在64位机器上 sizeof(item) 为48,所以最小应该为 56。
2、chunk中不仅仅存放缓存对象的value
这个不知道是我自己理解的错误还是误听哪的说明,我一直以为chunk中只存放了缓存对象的value,所以在分配slab时只需要根据缓存的value大小来。但是这种情况下无法解释为什么在确定slab的chunk大小时要加上一个item的大小,如果只放1byte的数据却使用的是56byte(64位的服务器)的chunk未免太浪费了,而且不可能没有人发现这种情况,所以有理由怀疑我以前的认识了。
chunk中不仅保持了缓存对象的value,而且保存了缓存对象的key,expire time, flag等详细信息
经过查看memcached的源代码发现两个问题:
- memcached如何计算需要存放的缓存对象大小,以及如何选择slab
查找缓存对象应该放到哪个slab时,不仅适用了value的长度,而且使用很多其他信息,具体计算公式如下:
*nsuffix = (uint8_t) snprintf(suffix, 40, " %d %d\r\n", flags, nbytes – 2);
return sizeof(item) + nkey + *nsuffix + nbytes;
上式中item是存储缓存对象的struct,nkey为key的长度加1(用于存放字符串最后的''),nbytes为value的长度加2(存放'\r\n'),suffix为实际打印的字符数,memcached使用返回的大小来选择能够存放得下的最接近的slab。所以在选择slab时,memcached并不是只关心缓存值的大小。
例如:如果是最短的key(1位),最短的值(1位),则在64机器上需要使用的空间其实是:
48 + (1 + 1) + 6 + (1 + 2) = 59
需要找一个大于或者等于59的slab存放。
当指定-n1时第一个slab的大小为56,这个时候就出现了一个奇怪的现象,memcached的slab 1永远放不进值,因为最小的key和value需要的空间59都大于slab 1的56,必须放到slab 2(size为80),见下图:

slab 2中只存放了一个对象,而且dump出来显示名称为“t”,值只有1byte,但是却使用了size为80的chunk,而slab 1将永远用不到。这个也可以说明其实在做slab初始化时计算的可能值小于实际需要的最小值。
- 到底chunk中存放了些什么
为什么在选择slab时memcached需要计算那么多的空间进去,导致一个1byte的对象却需要59byte空间存放,从代码中得到的答案如下图:
在chunk中通过item存放了缓存对象的一些详细信息,包括key的长度,value长度,过期时间,flag等信息,具体可以查看item结构定义,后面紧跟的是key的实际内容、suffix实际打印的内容、value的实际值,所以chunk中存放远比value信息多,有一些值的作用目前还没有搞清楚,但是大部分都是在缓冲对象使用过程中有明显作用的。
关于导出memcached的全部数据,只是使用了memcached提供的几个功能接口而已,具体代码如下,但是有几个要注意的地方。
- 第一个是cachedump命令每次返回的数据大小只有2M,这个是memcached的代码中写死的一个数值,除非在编译前修改(见参考一)
- 第二个问题是在一个memcached做cachedump时,似乎不能在循环里面对另外一个memcached做set操作。这个只是在尝试的时候发现的问题,一开始我是在第一个foreach循环中获取数据并放到另外一个memcached里的,这个时候会出现放置的数据为2byte的一个假数据。经过测试,取的数据时对的,但是放的时候出现了错误,所以下面的代码先循环cachedump的key,再单独循环key列表去导出数据。
- 最后这个问题是有第一个引起的,根据我导入的情况看,2M一般只能拿到4万左右的key,有些slab的item达到了4千万,4万只是千分之一,所以只能边导出边删除,知道没有数据可以导出为止。
<?php
echo "Start time ".date("Y-m-d H:i:s")."\n";
$slabs = array_slice($argv, 1);
foreach ($slabs as $slabId) {
export($slabId);
}
echo "End time ".date("Y-m-d H:i:s")."\n";
function export($slabId)
{
$num = 0;
$total = 0;
$times = 0;
$deleteCount = 0;
$addCount = 0;
$fromCache = new Memcache();
$fromCache->connect("192.168.0.100", 11211);
for ($i = 0; $i < 200; $i++) {
$keys = array();
$cdump = $fromCache->getExtendedStats( 'cachedump' , intval( $slabId ) , 10000000 );
foreach( $cdump AS $entries )
{
if( $entries )
{
foreach( $entries AS $eName => $eData )
{
$keys[] = $eName;
}
}
}
$toCache = new Memcache();
$toCache->connect("192.168.0.101", 11211);
if (count($keys) == 0 ) {
break;
}
foreach ($keys as $key) {
$value = $fromCache->get($key);
if ($toCache->add($key, $value, MEMCACHE_COMPRESSED, 864000)) {
$back = $toCache->get($key);
if (strlen(serialize($value)) != strlen(serialize($back))) {
$toCache->delete($key);
} else {
$num++;
}
}
$fromCache->delete($key);
}
$deleteCount += count($keys);
$addCount += $num;
$num = 0;
}
echo "Total import $addCount items into slab $slabId.\n";
echo "Total remove $deleteCount items from $slabId.\n";
}
?>
参考链接:
1、Memcachd的cachedump最大返回值限制
2、关于导出memcached数据的讨论
3、另外的一篇博客
上篇解释了Memcached的内存分配机制,这篇总结下如何检测memcached是否发挥了优秀的性能。
二、Memcached性能检测
Memcached作为一个内存key-value存储容器有非常优秀的性能,但是在上次的使用中确发现大量的数据丢失情况发生,导致cache的功能基本消失。具体的检测方式如下:
- 检测命中率
检测命中率是一个最基本的、最宏观的方式,使用telnet连接到memcached服务器,然后执行stats命令就可以看到宏观的一些信息,如下图。

这个命令中比较关键的属性是get_hits和get_misses,get_hits表示读取cache命中的次数,get_misses是读取失败的次数,即尝试读取不存在的缓存数据。
命中率=get_hits / (get_hits + get_misses)
命中率越高说明cache起到的缓存作用越大。但是在实际使用中,这个命中率不是有效数据的命中率,有些时候get操作可能只是检查一个key存在不存在,这个时候miss也是正确的,这就像用memcached作为一种定时器,将一些临时数据在memcache中存放特定时间长度,业务逻辑会根据cache是否存在而作不同的逻辑,这种数据其实已经不是单纯的缓存了,也不应该统计到命中率中。再者,这个命中率是从memcached启动开始所有的请求的综合值,不能反映一个时间段内的情况,所以要排查memcached的性能问题,还需要更详细的数值。但是高的命中率还是能够反映出memcached良好的使用情况,突然下跌的命中率能够反映大量cache丢失的发生。
- Stats items
Stats items命令可以查看每个slab中存储的item的一些详细信息,具体可以见下图。

关键属性有:
Stats items属性
| 属性名称 |
属性说明 |
| number |
存放的数据总数 |
| age |
存放的数据中存放时间最久的数据已经存在的时间,以秒为单位 |
| evicted |
被剔除的数据总数 |
| evicted_time |
最后被剔除的数据在cache中存放的时间,以秒为单位 |
stats items可以详细的观察各slab的数据对象的情况,因为memcached的内存分配策略导致一旦memcached的总内存达到了设置的最大内存,代表所有的slab能够使用的page都已经固定,这个时候如果还有数据放入,将开始导致memcached使用LRU策略剔除数据。而LRU策略不是针对所有的slabs,而是只针对新数据应该被放入的slab,例如有一个新的数据要被放入slab 3,则LRU只对slab 3进行。通过stats items就可以观察到这些剔除的情况。
具体分析如下:
- evicted属性
如果一个slab的evicted属性不是0,则说明当前slab出现了提前剔除数据的情况,这个slab可能是你需要注意的。
- evicted_time属性
如果evicted不为0,则evicited_time就代表最后被剔除的数据时间缓存的时间。并不是发生了LRU就代码memcached负载过载了,因为有些时候在使用cache时会设置过期时间为0,这样缓存将被存放30天,如果内存慢了还持续放入数据,而这些为过期的数据很久没有被使用,则可能被剔除。需要注意的是,最后剔除的这个数据已经被缓存的时间,把evicted_time换算成标准时间看下是否已经达到了你可以接受的时间,例如:你认为数据被缓存了2天是你可以接受的,而最后被剔除的数据已经存放了3天以上,则可以认为这个slab的压力其实可以接受的;但是如果最后被剔除的数据只被缓存了20秒,不用考虑,这个slab已经负载过重了。
- age属性
age属性反应了当前还在缓存的数据中最久的时间,它的大小和evicted_time没有必然的大小关系,因为可能时间最久的数据确实频繁被读取的,这时候不会被LRU清理掉,但是如果它小于evicted_time的话,则说明数据在被下去读取前就被清理了,或者存放了很多长时间但是不被使用的缓存对象。
- Stats slabs
从Stats items中如果发现有异常的slab,则可以通过stats slabs查看下该slab是不是内存分配的确有问题。
Stats slabs结果如下图

Stats slabs的属性说明如下:
Stats slabs属性
| 属性名称 |
属性说明 |
| chunk_size |
当前slab每个chunk的大小 |
| chunk_per_page |
每个page能够存放的chunk数 |
| total_pages |
分配给当前slab的page总数 |
| total_chunks |
当前slab最多能够存放的chunk数,应该等于chunck_per_page * total_page |
| used_chunks |
已经被占用的chunks总数 |
| free_chunks |
过期数据空出的chunk里还没有被使用的chunk数 |
| free_chunks_end |
新分配的但是还没有被使用的chunk数 |
这个命令的信息量很大,所有属性都很有价值。下面一一解释各属性:
综合上面的数据,可以发现造成memcached的内存使用率降低的属性有:
- chunk_size, chunk_per_page
这两个属性是固定的,但是它反映当前slab存储的数据大小,可以供你分析缓存数据的散列区间,通过调整增长因子可以改变slab的区间分布,从而改变数据散列到的区域。如果大量的230byte到260byte的数据,而刚好一个slab大小是250byte,则250byte到260byte的数据将被落到下一个slab,从而导致大量的空间浪费。
- total_pages
这个是当前slab总共分配大的page总数,如果没有修改page的默认大小的情况下,这个数值就是当前slab能够缓存的数据的总大小(单位为M)。如果这个slab的剔除非常严重,一定要注意这个slab的page数是不是太少了。
我上次处理的那个项目因为和另外的一个项目共用的memcache,而且memcache已经运行了很长时间,导致page都已经全部被分配完,而刚好两个项目的缓存数据大小差别很多,导致新项目数据最多的slab 4竟然只有一个page,所以数据缓存不到22s就被替换了,完全失去了缓存的意义。
针对我遇到的那个情况,解决方案是重新分配page,或者重启memcache服务。但是page reassign方法从1.2.8版已经完全移除了,所以现在没有办法在线情况下重新分配page了。另外一种有些时候是不可以接受的,因为一次缓存服务器的重启将导致所有缓存的数据将重新从DB取出,这个可能造成db的压力瞬间增大。而且有的缓存数据时不入库的,这个时候我们就需要做memcache的导入和导出了。在下篇文章中我会总结下memcache的dump操作。
- total_chunks
这个的作用和total_pages基本相同,不过这个属性可以更准确的反应实际可以存放的缓存对象总数。
- used_chunks, free_chunks, free_chunks_end
这三个属性相关度比较高,从数值上来看它们满足:
total_chunks = used_chunks + free_chunks + free_chunks_end
used_chunks就是字面的意思,已经使用的chunk数;free_chunks却不是所有的未被使用的chunk数,而是曾经被使用过但是因为过期而被回收的chunk数;free_chunks_end是page中从来没有被使用过的chunk数。

从上图可以看出,slab 1只放了一个对象,但是已经申请了一整个page,这个时候used_chunks为1,但是free_chunks却为0,因为还没有任何回收的空间,而free_chunks_end却等于10081,说明这么多的chunk从来没有被使用过。下图就是这个数据过期后的stats slabs数据,可以发现free_chunks有值了,就是过期的那个chunk,所以是1,used_chunks为0,free_chunks_end不变。

为什么要分两种free chunk呢?
我的理解是这样的:如果free_chunks_end不为零,说明当前slab没有出现过容量不够的时候;而如果free_chunks始终为0,说明很多数据过期时间过长或者在过期前就被剔除了,这个要结合剔除数据和数据保留的时间(age属性)来看待。所以分开统计这两个值可以准确的判断实际空闲的chunk的状态,一旦所以的chunk被使用过一次以后,除非重新申请page,否则free_chunks_end始终为0。所以对于运行时间比较久的memcached,可能大部分这个值都是0。
- active_slabs, total_malloced
在stats slabs输出的最后两项是两个统计数据,一个是活动的slab总数,因为slab虽然带编号,但是这个编号不一定是连续的,因为有可能有些中间区间的slab没有值就没有初始化,这样以后该slab有值的时候就不用改变slab的编号了。所以活动的slab总数不一定等于slab的最大编号。
total_malloced这个是实际已经分配的总内存数,单位为byte,这个数值决定了memcached实际还能申请多少内存,如果这个值已经达到设定的上限,则不会有新的page被分配,以前分配的page也已经固定slab了。
综合上面的数据,可以发现造成memcached的内存使用率降低的属性有:
- page中从来没有被使用过的chunks;
- chunk中存放数据和chunk实际大小的差值;
- 由于短时间的数据集中在某个slab区域,导致大量page被分配,而之后被闲置的内存,这些即使有整个page的空闲也不会被分配给实际压力很大的slab区域(这个功能是不是以后memcached会考虑实现呢?)。
参考:http://hi.baidu.com/zhuguoneng/blog/item/aa5fbb3949e766f83b87cee4.html
上周由于接手个一个新的项目,该项目对于memcache的依赖非常大,从而导致我不得不真的开始深入了解memcache的内存使用情况,这里总结下我个人的收获,也算是一次小的memcache优化吧。
一、Memcache内存分配机制
关于这个机制网上有很多解释的,我个人的总结如下。
- Page为内存分配的最小单位。
Memcached的内存分配以page为单位,默认情况下一个page是1M,可以通过-I参数在启动时指定。如果需要申请内存时,memcached会划分出一个新的page并分配给需要的slab区域。page一旦被分配在重启前不会被回收或者重新分配(page ressign已经从1.2.8版移除了)

- Slabs划分数据空间。
Memcached并不是将所有大小的数据都放在一起的,而是预先将数据空间划分为一系列slabs,每个slab只负责一定范围内的数据存储。如下图,每个slab只存储大于其上一个slab的size并小于或者等于自己最大size的数据。例如:slab 3只存储大小介于137 到 224 bytes的数据。如果一个数据大小为230byte将被分配到slab 4中。从下图可以看出,每个slab负责的空间其实是不等的,memcached默认情况下下一个slab的最大值为前一个的1.25倍,这个可以通过修改-f参数来修改增长比例。

- Chunk才是存放缓存数据的单位。
Chunk是一系列固定的内存空间,这个大小就是管理它的slab的最大存放大小。例如:slab 1的所有chunk都是104byte,而slab 4的所有chunk都是280byte。chunk是memcached实际存放缓存数据的地方,因为chunk的大小固定为slab能够存放的最大值,所以所有分配给当前slab的数据都可以被chunk存下。如果时间的数据大小小于chunk的大小,空余的空间将会被闲置,这个是为了防止内存碎片而设计的。例如下图,chunk size是224byte,而存储的数据只有200byte,剩下的24byte将被闲置。

- Slab的内存分配。
Memcached在启动时通过-m指定最大使用内存,但是这个不会一启动就占用,是随着需要逐步分配给各slab的。
如果一个新的缓存数据要被存放,memcached首先选择一个合适的slab,然后查看该slab是否还有空闲的chunk,如果有则直接存放进去;如果没有则要进行申请。slab申请内存时以page为单位,所以在放入第一个数据,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk的数组,在从这个chunk数组中选择一个用于存储数据。如下图,slab 1和slab 2都分配了一个page,并按各自的大小切分成chunk数组。

- Memcached内存分配策略。
综合上面的介绍,memcached的内存分配策略就是:按slab需求分配page,各slab按需使用chunk存储。
这里有几个特点要注意,
- Memcached分配出去的page不会被回收或者重新分配
- Memcached申请的内存不会被释放
- slab空闲的chunk不会借给任何其他slab使用

知道了这些以后,就可以理解为什么总内存没有被全部占用的情况下,memcached却出现了丢失缓存数据的问题了。
关于memcached命令行参数可以参考:http://techgurulive.com/2010/01/26/how-to-configure-memcached-memcached-configuration-parameters/