Archive

Archive for the ‘Memcache’ Category

Memcached源码阅读笔记(一)

December 31st, 2010 Tank No comments

最近开始硬着头皮阅读Memcached的源码,记录下发现的细节和学习到小技巧吧,由于我自己很久没有写过c了,还随便复习了下c的基础知识。

1、第一个slab的chunk size值

       当我试图修改memcached的起始slab大小以便用于存储非常小的缓存对象(往往值是没有意义的,只是用一个1之类的标志来表示key是否存在,这个通常用于判断缓存一种状态而不是数值,例如是否获得过当天的登陆奖励)时发现了这个问题。
        通过启动memcached时添加参数: -n48 (48是要被设置的值,必须是正数) 可以指定第一个slab的chunk大小,但是经过尝试和跟踪代码发现,第一个slab的chunk size不止受此一个参数限制,具体计算第一个chunk size的过程如下:

  1. -n 指定的值被存放到 settings.chunk_size中
  2. 当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的源代码发现两个问题:

  1. 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初始化时计算的可能值小于实际需要的最小值。
  2. 到底chunk中存放了些什么
    为什么在选择slab时memcached需要计算那么多的空间进去,导致一个1byte的对象却需要59byte空间存放,从代码中得到的答案如下图:
    在chunk中通过item存放了缓存对象的一些详细信息,包括key的长度,value长度,过期时间,flag等信息,具体可以查看item结构定义,后面紧跟的是key的实际内容、suffix实际打印的内容、value的实际值,所以chunk中存放远比value信息多,有一些值的作用目前还没有搞清楚,但是大部分都是在缓冲对象使用过程中有明显作用的。
Categories: Memcache Tags:

导出Memcached所有缓存数据

December 16th, 2010 Tank No comments

关于导出memcached的全部数据,只是使用了memcached提供的几个功能接口而已,具体代码如下,但是有几个要注意的地方。

  1. 第一个是cachedump命令每次返回的数据大小只有2M,这个是memcached的代码中写死的一个数值,除非在编译前修改(见参考一)
  2. 第二个问题是在一个memcached做cachedump时,似乎不能在循环里面对另外一个memcached做set操作。这个只是在尝试的时候发现的问题,一开始我是在第一个foreach循环中获取数据并放到另外一个memcached里的,这个时候会出现放置的数据为2byte的一个假数据。经过测试,取的数据时对的,但是放的时候出现了错误,所以下面的代码先循环cachedump的key,再单独循环key列表去导出数据。
  3. 最后这个问题是有第一个引起的,根据我导入的情况看,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、另外的一篇博客

Categories: Memcache Tags:

Memcached性能检测

December 16th, 2010 Tank No comments

上篇解释了Memcached的内存分配机制,这篇总结下如何检测memcached是否发挥了优秀的性能。

二、Memcached性能检测

        Memcached作为一个内存key-value存储容器有非常优秀的性能,但是在上次的使用中确发现大量的数据丢失情况发生,导致cache的功能基本消失。具体的检测方式如下:

  1. 检测命中率

    检测命中率是一个最基本的、最宏观的方式,使用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丢失的发生。

  2. 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就可以观察到这些剔除的情况。
    具体分析如下:

    1. evicted属性
      如果一个slab的evicted属性不是0,则说明当前slab出现了提前剔除数据的情况,这个slab可能是你需要注意的。
    2. evicted_time属性
      如果evicted不为0,则evicited_time就代表最后被剔除的数据时间缓存的时间。并不是发生了LRU就代码memcached负载过载了,因为有些时候在使用cache时会设置过期时间为0,这样缓存将被存放30天,如果内存慢了还持续放入数据,而这些为过期的数据很久没有被使用,则可能被剔除。需要注意的是,最后剔除的这个数据已经被缓存的时间,把evicted_time换算成标准时间看下是否已经达到了你可以接受的时间,例如:你认为数据被缓存了2天是你可以接受的,而最后被剔除的数据已经存放了3天以上,则可以认为这个slab的压力其实可以接受的;但是如果最后被剔除的数据只被缓存了20秒,不用考虑,这个slab已经负载过重了。
    3. age属性
      age属性反应了当前还在缓存的数据中最久的时间,它的大小和evicted_time没有必然的大小关系,因为可能时间最久的数据确实频繁被读取的,这时候不会被LRU清理掉,但是如果它小于evicted_time的话,则说明数据在被下去读取前就被清理了,或者存放了很多长时间但是不被使用的缓存对象。
  3. 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的内存使用率降低的属性有:

    1. chunk_size, chunk_per_page
      这两个属性是固定的,但是它反映当前slab存储的数据大小,可以供你分析缓存数据的散列区间,通过调整增长因子可以改变slab的区间分布,从而改变数据散列到的区域。如果大量的230byte到260byte的数据,而刚好一个slab大小是250byte,则250byte到260byte的数据将被落到下一个slab,从而导致大量的空间浪费。
    2. 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操作。
    3. total_chunks
      这个的作用和total_pages基本相同,不过这个属性可以更准确的反应实际可以存放的缓存对象总数。
    4. 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。
    5. active_slabs, total_malloced
      在stats slabs输出的最后两项是两个统计数据,一个是活动的slab总数,因为slab虽然带编号,但是这个编号不一定是连续的,因为有可能有些中间区间的slab没有值就没有初始化,这样以后该slab有值的时候就不用改变slab的编号了。所以活动的slab总数不一定等于slab的最大编号。
      total_malloced这个是实际已经分配的总内存数,单位为byte,这个数值决定了memcached实际还能申请多少内存,如果这个值已经达到设定的上限,则不会有新的page被分配,以前分配的page也已经固定slab了。

 

    综合上面的数据,可以发现造成memcached的内存使用率降低的属性有:

  1. page中从来没有被使用过的chunks;
  2. chunk中存放数据和chunk实际大小的差值;
  3. 由于短时间的数据集中在某个slab区域,导致大量page被分配,而之后被闲置的内存,这些即使有整个page的空闲也不会被分配给实际压力很大的slab区域(这个功能是不是以后memcached会考虑实现呢?)。

 

参考:http://hi.baidu.com/zhuguoneng/blog/item/aa5fbb3949e766f83b87cee4.html

Categories: Memcache Tags: ,

Memcache内存分配策略

December 14th, 2010 Tank 2 comments

上周由于接手个一个新的项目,该项目对于memcache的依赖非常大,从而导致我不得不真的开始深入了解memcache的内存使用情况,这里总结下我个人的收获,也算是一次小的memcache优化吧。

一、Memcache内存分配机制

        关于这个机制网上有很多解释的,我个人的总结如下。

  1. Page为内存分配的最小单位。

    Memcached的内存分配以page为单位,默认情况下一个page是1M,可以通过-I参数在启动时指定。如果需要申请内存时,memcached会划分出一个新的page并分配给需要的slab区域。page一旦被分配在重启前不会被回收或者重新分配(page ressign已经从1.2.8版移除了)
    Memcached pages

  2. Slabs划分数据空间。

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

  3. Chunk才是存放缓存数据的单位。

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

  4. 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数组。

  5. Memcached内存分配策略。

    综合上面的介绍,memcached的内存分配策略就是:按slab需求分配page,各slab按需使用chunk存储。
    这里有几个特点要注意,

    1. Memcached分配出去的page不会被回收或者重新分配
    2. Memcached申请的内存不会被释放
    3. slab空闲的chunk不会借给任何其他slab使用

 

      知道了这些以后,就可以理解为什么总内存没有被全部占用的情况下,memcached却出现了丢失缓存数据的问题了。

关于memcached命令行参数可以参考:http://techgurulive.com/2010/01/26/how-to-configure-memcached-memcached-configuration-parameters/

Categories: Memcache Tags: