[转]memcache 的源代码分析

 原贴:http://www.maycode.com/index.php/hotspot/27-clanguage/839-cache.html

牛人真多啊,向先行者致敬!

1.关于本文档
       本文档所有的分析都是在1.2版本之上,偶尔会提到比较1.1版本.其他版本没有阅读.
        一个星期时间的工作,不可能对memcache有很深刻的分析.文档本身的目的在于为以后的研究准备一个总结资料.刚接触memcache时,对其设计分布式的思路感到十分欣喜,因为在中间层以极小的代价实现简单分布式无疑成为一些要求不是很高的分布式应用的一个很好的设计思路,这个特性决定memcache本身在分布式应用中,单个结点之间是Server相互独立,不会存在同级之间的通信.一个结点拒绝访问,如果没有相应的冗余策略,将导致该结点的数据丢失.同时,memcache的Server结点对数据的存储操作都是在内存中完成,而memcache对内存分配和回收采用了曾在SunOS实现的分页机制,预分配一个大内存(默认是 <= 200M),然后分页切块,对每个数据对象的存储便在所切的块中进行操作.这个特点决定memcache没有设计到任何磁盘IO操作,那么所有的关于memcache的性能瓶颈都在网络通信部分,而memcache正是将这一部分抛给了一个中间层完成.可以说真正的memcache是一个单进程,单线程,监听某个网络端口的daemon(或非daemon),是一个轻量级的应用服务进程.这些特性决定了memcache的应用范围,性能瓶颈和优化策略.
       本文档的目的也就诣在探讨查看memcache源码后的一些观点.
       文档分为六个部分:
       1.  文档介绍.主要介绍文档组织和文档目的.
       2.  memcache的代码分析.分析memcache的源代码,指出核心的数据结构和算法.
       3.  memcache的使用优化.分析memcache的特点,结合实际应用给出一些优化memcache的策略.
       4.  memcache的测试分析.初略测试了memcache,给出优化方案的例证.
       5.  memcache的中间层客户端编写.分析memcache的通信协议,模拟编写了一个简单的memcache中间层客户端.
       6.  libevent简介.memcache采用的是libevent进行网络IO处理,libevent作为一种新的网络IO方式以高效的方法(epoll/kqueue)组织IO.
       其中第六章可以不看.对于系统管理员,需要查看第一,三,四部分;进行二次开发的程序员可以查看第一,二,四,五,六部分.

2.memcache代码分析
       1. memcache main流程
 
图2.1 memcache main流程
       libevent的事件处理机制在main进程里体现在处理网络IO上.在TCP memcache的服务流程里,也是利用event处理事件的.
       2. memcache服务流程(TCP)
 
图2.2 memcache服务流程图(TCP)
       3. memcache状态转换和通信协议处理
  
图2.3 memcache状态转换和通信协议处理
       需要说明的是,这里需要排除所有出错处理.很显然,不管是哪种操作下,一旦出错,信息需要通过conn_write状态往client写入出错信息的,那么在string_out时,必定转入conn_write状态.
       而且,很多细节也没有在流程图中给出,如统计信息的处理,超时后get操作时删除等等.对于在memcache协议中定义的其他操作,如stats,version,quit,flush_all,own,disown等等由于使用很少,在流程中没有详细给出,可以查看源代码.
       4.  memcache核心数据结构
               1. item结构
       item是存储在memcache的key-value对的抽象.由于组织item存放是按照LRU算法组织的.那么在其中有几个成员在修改源代码时必须注意,time是最近访问时间.exptime是item消亡时间.item是一个双向列表.同时还挂在一个Hash table上.
               2. conn结构
       conn结构是联系上下文的关键对象.对于每个连接的到来,都有一个conn结构与其对应,并且对应到某个连接状态,进入状态转换而完成操作.
       conn在程序开始也进行了一次预分配,分配200个连接空间.当200个使用完之后便是按需分配,到达一个分配一个.
       conn和item,iovec(内核级别缓冲结构)关联.
               3. slabclass_t结构
       slabclass_t保存了分级大小的空间槽,以分别适用于不同大小的item存放.取决于两个命令行参数,-f和-n.在应用slabclass_t时,定义的是一个数组,该数组长度取决于增长的指数级别和初始值大小(32+chunk_size),每个空间槽是不允许大于1M的,也就是1048576.
               4. settings结构
        系统获取的命令行参数保存的地方.
               5. stats结构:
       统计信息保存地方,可以考虑对其进行操作以适应不同的统计信息处理,如获取某个时间段的get命中率等等操作.
       5.  memcache核心算法
       事件触发处理连接,LRU算法挂载item,加锁并事后处理删除操作,预分配空间和按需请求不够空间策略获取存储空间,freelist方式管理空闲空间.名为new_hash的hash算法计算key的hash值(http://burtleburtle.net/bo...

3.memcache使用优化
       在优化memcache的工作之前,需要了解memcache体系的工作流程.一个分布式的memcache的运作是需要三个部分的,多台提供memcache服务的servers(简称S),一个进行分布式映射的中间层lib(其实这个也可以当作客户端的一部分,简称L),和进行memcache请求的客户端(简称C).
       在memcache工作时,有以下四个步骤:
       1.  C通过带有特性化的Key值的命令串,向L请求memcache服务,L将命令串进行分解,并通过对key的某种Hash算法决定S的地址
       2.  L将分解的(Comm Key-Value)或者(Comm Key)向相关的S请求memcache服务.
       3.  相关的S根据memcache协议向L返回服务结果.
       4.  L将结果进行聚集包装后返回给C一个人性化的响应.
       这个流程可以用图3.1进行描述.
 
图3.1 memcache工作步骤
       在每个S端,请求处理的Key-Value对当作一个对象(不过这个对象的结构是单一的),再进行另一步hash之后,存储在内存里.存储算法在第二部分有详细的描述.这种通过双层hash来分开处理分布式和缓存两个功能的方法值得学习.
       从上面的分析可以看出,分布式的memcache服务是需要很大的网络开销的.对于一般的应用而言,是否都要进行memcache的优化,甚至是否需要用到memcache,都需要进行权衡分析.如果只是本地小规模的应用,在数据库可承受的范围内,是宁愿采用数据库+文件缓存的方式.1.1版本的memcache走TCP模式在本地应用的处理速度甚至比不上Mysql数据的Unix域套接口的处理速度的一半,当然会更加比不上在程序内部直接操作内存了.虽然1.2版本的memcache已经提供了-s参数指定Unix域套口和-u指定udp模式.而且如果不需要用到分布式的话,不推荐使用memcache,除非你的内存足够大到浪费的程度.
       因此,在进行优化之前,需要看看是否真的需要优化memcache,是否真正需要用到memcache.
       优化可以从以下几个方面进行:
       1.  命中率.
       对于缓存服务而言,命中率是至关重要的.命中率的提升可以通过多种方案实现.其一,提高服务获取的内存总量.这无疑是增加命中的最直接的办法,将缓存数据完全放入数据池中.只要连接不失效,就一定命中.其二,提高空间利用率,这实际上也是另一种方式的增加内存总量.具体实现在下一个方面给出.其三,对于一些很特别的memcache应用,可以采用多个memcache服务进行侦听,分开处理,针对服务提供的频繁度划分服务内存,相当于在应用一级别上再来一次LRU.其四,对于整体命中率,可以采取有效的冗余策略,减少分布式服务时某个server发生服务抖动的情况.如,14台机器实现分布式memcache,划分两组服务,其中一组13台做一个分布式的memcache,一组1台做整个的memcache备份.对于update操作,需要进行两边,get操作只需要一遍,一旦访问失效,则访问备份服务器.这样,对于备份服务器需要内存比较大,而且只适应于读操作大于写操作的应用中.这可以认为是RAID3,当然,也可以采用RAID1完全镜像.
       2.  空间利用率.
       对于使用memcache做定长数据缓存服务而言,是可以在空间利用率上进行优化.甚至最简单的办法可以不用更改memcache的源码遍可以完成由-f和-n参数的配合可以做到定长优化,不过极可能需要浪费掉预分配的199M内存空间.当然前提是memcache的版本是1.2,同时如果使用的是1.2.0和1.2.1的话,需要更改掉一个BUG,那就是getopt时将opt串中最后一个”s”改成”n”,希望memcache能在以后的版本发现这个BUG.例如,如果key是一个定长id(如一个8位的流水号00000001),value是一个定长的串(如16位的任意字符串),对应于一个chunk_size可以这么计算:chunk_size = sizeof(item) + nkey + *nsuffix + nbytes = 32 + 9 + (flag的数位长度 )2+ (16)的数位长度) 2+(两换行的长度)4 + 17 = 40 + 10 + 16 = 66,那么可以通过 -f 1.000001 -n  `expr 66 - 32`,即 -f 1.000001 -n 34 来启动memcache.这种情况下,会浪费掉memcache预先分配的200M空间中的199M.从第2个预分配等级到第200个预分配等级将不会用到.然而,存在解决办法,那就是在编译memcache是加入编译参数-DDONT_PREALLOC_SLABS,或者在源代码中加入#define DONT_PREALLOC_SLABS即可,只是会去除memcache的预分配内存机制.
       如果需要memcache的预分配内存机制,那么需要对其源代码进行修改.修改如下:  

引用
   1. 在slabs.c中,将函数slabs_clsid改成:
     unsigned int slabs_clsid(size_t size)
     {   unsigned int res = POWER_SMALLEST; 
       if(size==0)
                 return 0;
       res = (size)%power_largest;
       return res;
     }
   2. 在item.c中,将函数 item_make_header改为:
     int item_make_header(char *key, uint8_t nkey, int flags, int nbytes,
                    char *suffix, int *nsuffix) 
     {
           *nsuffix = sprintf(suffix, " %u %u/r/n", flags, nbytes - 2);
           return sizeof(item)+ nkey + *nsuffix + nbytes + hash(key,nkey,0);
     }
   3. 在item.c中,将函数 item_free改为:
     void item_free(item *it) 
     {  unsigned int ntotal = it->slabs_clsid;
           assert((it->it_flags & ITEM_LINKED) == 0);
           assert(it != heads[it->slabs_clsid]);
           assert(it != tails[it->slabs_clsid]);
           assert(it->refcount == 0);

           it->slabs_clsid = 0;
           it->it_flags |= ITEM_SLABBED;
           slabs_free(it, ntotal);
     }

       做一个轮流存储的机制使用预分配的内存,这样的好处是其他地方不需要做任何修改就可以了,当然你可以在源代码中加入上面的代码,并将它们放在一个自定义的宏后面.
       3.  加速比.
       加速比,也即事件的处理效率.是否可以修改libevent的事件处理效率,需要研究.如果内存空间很大,可以将freeconn的数值调大,增加预分配的conn内存大小.
       是否可以将memcache做成多线程处理,但在处理多线程数据同步是个问题.
        如果有时间,愿意来试试这个策略.
       4.  安全性能.
       memcache还存在一个比较显著的问题,那就是其安全性能.只要了解memcache监听的端口,对于能够使用分布式memcache进行数据通信的网络环境的机器,都可以通过memcache协议于memcache服务器进行通信,获取或种植数据.不能保证种植进内存里的数据不会被别有心意的人再利用.也不能保证服务器的内存不被漫天遍地的垃圾数据所堆积,造成命中极低.
       memcache的设计理念在一个轻字,如果对每次Client的通讯需要校验身份,那么恐怕memcache也就达不到其想要的效果了.存在解决办法缓解这个问题,一般而言,需要使用memcache服务的机器,可以在Server维持一张红色列表.这张表上的机器便可以获取服务.很显然,memcache并非任意Client都能访问,只有信任的机器访问,那么为什么不将这些信任的机器放在一个/etc/mem_passwd下呢.
       还有,memcached走udp时,很大几率接受到upd时,都会使服务死掉,特别是set,add,replace时,这个问题需要去考究一下.不过没有时间了.

4.memcache测试分析
       服务器端memcache在命令行运行的参数:
引用
# memcached –d –m 512 –l *.*.*.* -u ** -f 1.00001 –n 16 –c 10000 -vv

       1.  读写memcache指令测试
       在利用了memcache官方推荐的c客户端libmemcache和自己编写的一个简单客户端测试之后,在set/get/add/del指令的运行速度如表4.1和图4.1所示:
 
表4.1 memcache的指令运行速度
图4.1 memcache的指令运行速度
       2.  并发连接
       由于在memcache服务器端,一个结点运行的是单进程单线程的daemon(或非daemon)服务,同时对于采用了libevent处理网络IO而言,其并发连接的数目是和libevent采用的机制相关连的.很显然,accept函数在接收到connection后将Client的socket放进event库中,等待处理.而libevent库在LINUX中使用的是epoll,监听EPOLLIT水平触发.因此从理论上讲,memcache的并发连接可以达到infinite,前提是event池和内存空间足够大.而没有和linux的线程处理有关系.事实上,在后面的测试中便可发现,在单结点连接压力测试时,瞬时并发连接可以达到5000多个.只是等待触发时间上的长短和有效无效的区别.
       在表4.2中可以清晰的看到并发连接的一些数据.
       3.  服务端系统负载
       通过自己编写的服务器端,对单结点的memcache进行了连接压力测试.其中测试用例的编写是这样的:启用七个客户端,每个客户端串行运行1000个进程,每个进程开3000线程,每个线程执行10次memcache的读操作或者写操作(操作相同).客户端并发连接.
       1.  客户端(7)的环境:Intel(R) Xeon(R) CPU 5120 @ 1.86GHz,4G memory.
       2.  服务器端(1)的环境:Intel(R) Xeon(R) CPU 5120 @ 1.86GHz,4G memory.
       3.  网络环境:100M网卡,Cisco交换机.
       4.  数据记录:见表4.2和图4.2.
 
表4.2 memcache连接和系统负载
图4.2 memcache连接和系统负载
       很显然,memcache的运行在系统cpu的消耗上占十分少的比重,即便是很恐怖的并发连接也不会给系统带来多大的负载,因为其磁盘IO free(所有操作都在内存中)和相应的内存分配机制决定其占用cpu的极少,而相反,在网络IO上却花费很大的时间.
       4.  空间分配,命中率
       由于本地测试式的get数据非常固定,因此命中率基本为100%.在10.68.1.31上运行了一个有前端应用的memcachce服务器,运行时间已经有364个多小时了.
       因此通过10.68.1.31上的数据说明(版本为1.1.13).通过memcache的统计协议可以清楚的看到其命中率高达95.9%,如表4.3所示:
 
表4.3 memcache空间分配和命中

5.memcache客户端编写
       1.   memcache协议
       在memcache协议中规定了Client和Server的通信规则.
       在源码分析中,主要分析了update/get/del/incr/decr几类的处理过程.其具体的规则可以在官方文档中有说明(),这里做简单的解释.
引用
1.  Update(set/add/replace):
Client请求规则:
/r/n
/r/n
Server响应规则:
STORED/r/n 或者 NOT_STORED/r/n

其中,是set,add,replace三种中的一种;
是client请求存储的键值;
是任意16bit长的unsigned int值,在get操作时,也将伴随data一起返回,可以用来存储某些认证信息或者描述信息;
是key-value对象的消亡时间,如果为0,则代表永不消亡;
是数据的长度,千万小心,这个很重要,在memcache源代码里,直接读取这个数值来当作数据的长度,而不是用strlen计算的.这个显而易见,因为数据中有可能存在/r/n符号,也就是协议中规定的分隔符.如果出现,则严格按长度取数据;
也就是value值,可以包含/r/n值.

STORED代表update操作成功,NOT_STORED代表update操作失败.

2.  Get(get/bget)
Client请求规则:
*/r/n
Server响应规则:
VALUE /r/n
/r/n
END/r/n

Get/bget操作可以一次操作多个key值,server的响应格式中的关键字可以参看上面的解释,END代表数据显示结束.如果没有数据,则只有一个END/r/n.

3.  Delete(delete)
Client请求规则:
delete /r/n
Server响应规则:
DELETED/r/n 或者 NOT_DELETED/r/n

其中,是真正删除从server端删除的时间(sec),在时间未到前,server只将其放入一个delete队列里,其他诸如add/replace/get操作不能成功.

4.  Incr/decr(incr/decr)
Client请求规则:
/r/n
Server响应规则:
   NOT_FOUND/r/n 或者 /r/n

   其中,client请求的是将要在对应的value上增减的值;
   Server的是增减后的新值.

5.  其他(如stats/quit等)
可以去看协议原文,使用不是太多.

       2.  针对协议的一个简单实现
       在这个例子中简单实现了一个能进行update/get/delete操作测试用例,只是简单socket的应用而已.如果可以,模仿这个写一个简单的客户端应该难度不大.
引用
/****************************************/
*            mem_benchmark_conn2.c
*
*  Mon Mar 7 10:52:30 2007
*  Copyright  2007  Spark Zheng
*  Mail
*  v0.1 Mar 5 2007 file:mem_benchmark_conn.c
/****************************************/

#include < stdio.h>
#include < stdlib.h>
#include < string.h>
#include < ctype.h>

#include < unistd.h>
#include < pthread.h>
#include < time.h>
#include < sys/types.h>
#include < sys/time.h>
#include < sys/resource.h>
//#include < sys/socket.h>
//#include < netdb.h>
//#include < arpa/inet.h>

#ifndef MEM_SERVER
#define MEM_SERVER "10.210.71.25"
#endif

#ifndef MEM_PORT
#define MEM_PORT 11211
#endif

void p_usage(void);
void *conn_mem(void);
int NonbSocket(const char *server, int port);
int mem_set(int sock,const char *key,const char *value);
int mem_add(int sock,const char *key,const char *value);
int mem_get(int sock,const char *key,char *value);
int mem_del(int sock,const char *key);

int main(int argc,char **argv)
{
       int conn=0;
       int i=0;
       pthread_t ptid[10000];
       struct rlimit rlim;
       struct timeval tv1,tv2;

       if(argc < 2)
       {
               p_usage();
               exit(255);
       }

       conn = atoi(argv[1]);

       if(getrlimit(RLIMIT_NOFILE,&rlim) != 0)
       {
               fprintf(stderr,"getrlimit error in line %d/n",__LINE__);
               exit(254);
       }

       if((conn > rlim.rlim_cur) && (2*conn > 1024))
       {
               rlim.rlim_cur = 2*conn;
       }
       if(rlim.rlim_cur > rlim.rlim_max)
       {
               rlim.rlim_max = rlim.rlim_cur;
       }

       if(setrlimit(RLIMIT_NOFILE,&rlim) != 0)
       {
               fprintf(stderr,"setrlimit error in line %d/n",__LINE__);
               exit(254);
       }

       gettimeofday(&tv1,NULL);

       while(i++ < conn)
       {
               if(pthread_create(&ptid[i],NULL,(void *)conn_mem,NULL) != 0)
               {
                       perror("pthread_create error/n");
                       exit(253);
               }
       }

       i=0;

       while(i++ < conn)
       {
               if(pthread_join(ptid[i],NULL) != 0)
               {
                       perror("pthread_join error/n");
                       exit(253);
               }
       }

       gettimeofday(&tv2,NULL);

       printf("time is %f,conn is %f persecond/n",((tv2.tv_sec-tv1.tv_sec)+(tv2.tv_usec-tv1.tv_usec)/1000000.0),conn/((tv2.tv_sec-tv1.tv_sec)+(tv2.tv_usec-tv1.tv_usec)/1000000.0));

       return 0;
}

void p_usage(void)
{
       printf("Usage:./mem_benchmark_conn < conn_num >/n");
       printf("Notice: the conn_num must <= 10000/n");
       return;
}

void *conn_mem(void)
{
       int sock;
       char *key = "test_a";
       char *value = "this is a";

       if((sock=NonbSocket(MEM_SERVER,MEM_PORT)) < 0)
       {
               fprintf(stderr,"socket error in line %d/n",__LINE__);
               return NULL;
       }

int i=0;
while(i++ < 10)
{
///*
       mem_set(sock,key,value);
//*/
/*
       char *key2="test_b";
       char *value2="this is b";
       mem_add(sock,key2,value2);
*/
/*
       char buf[101];
       mem_get(sock,key,buf);
       printf("get value for %s is %s/n",key,buf);
*/
/*
       char *key3="test_c";
       mem_del(sock,key);
*/
}
       close(sock);

       return NULL;
}

int mem_set(int sock,const char *key,const char *value)
{
       char set[101];
       char recv_buf[101];

       sprintf(set,"set %s 0 0 %d/r/n%s/r/n",key,strlen(value),value);

       if(write(sock,set,strlen(set)) < 0)
       {
               fprintf(stderr,"write error in line %d/n",__LINE__);
               return -1;
       }

       if(read(sock,recv_buf,100) < 0)
       {
               fprintf(stderr,"read error in line %d/n",__LINE__);
               return -2;
       }

       printf("in set %s/n",recv_buf);

       return 0;
}

int mem_add(int sock,const char *key,const char *value)
{
       char add[101];
       char recv_buf[101];

       sprintf(add,"add %s 0 0 %d/r/n%s/r/n",key,strlen(value),value);

       if(write(sock,add,strlen(add)) < 0)
       {
               fprintf(stderr,"write error in line %d/n",__LINE__);
               return -1;
       }

       if(read(sock,recv_buf,100) < 0)
       {
               fprintf(stderr,"read error in line %d/n",__LINE__);
               return -2;
       }

       printf("in add %s/n",recv_buf);

       return 0;
}

int mem_get(int sock,const char *key,char *value)
{
       char get[101];
       char recv_buf[101];

       sprintf(get,"get %s/r/n",key);

       if(write(sock,get,strlen(get)) < 0)
       {
               fprintf(stderr,"write error in line %d/n",__LINE__);
               return -1;
       }

       if(read(sock,recv_buf,100) < 0)
       {
               fprintf(stderr,"read error in line %d/n",__LINE__);
               return -2;
       }

       strncpy(value,recv_buf,strlen(recv_buf));

       printf("in get %s/n",recv_buf);

       return 0;
}

int mem_del(int sock,const char *key)
{
       char del[101];
       char recv_buf[101];

       sprintf(del,"delete %s 0/r/n",key);

       if(write(sock,del,strlen(del)) < 0)
       {
               fprintf(stderr,"write error in line %d/n",__LINE__);
               return -1;
       }

       if(read(sock,recv_buf,100) < 0)
       {
               fprintf(stderr,"read error in line %d/n",__LINE__);
               return -2;
       }

       printf("in del %s/n",recv_buf);

       return 0;
}

       3.  分布式的实现
       分布式的实现可以这么完成,构建一个struct用于存放server信息.对于每个请求的key值,用很简单的hash算法(如libmemcache用的是crc32)映射到server数组中的某个数组,然后对其进行通信.获取处理结果之后,将结果美化返回client.

6. libevent 简介
       1.  libevent
       libevent是一个事件触发的网络库,适用于windows,linux,bsd等多种平台,内部使用iopc/epoll/kqueue等系统调用管理事件机制,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能.
       从代码中看,libevent支持用户使用三种类型的事件,分别是网络IO,定时器,信号三种,在定时器的实现上使用了红黑树(RB tree)的数据结构,以达到高效查找,排序,删除定时器的目的,网络IO上,libevent的epoll居然用的EPOLLLT水平触发的方式,不容易出错,但是在效率上可能比EPOLLET要低一些.跟网络无关的,libevent也有一些缓冲区管理的函数,libevent没有提供缓存的函数.而且libevent的接口形式非常值得参考.
       2.  epoll
在linux中,libevent用的是epoll.如果有兴趣的话,可以查看man epoll页面.或者看前面blog上引用的libevent的资源.
       
       文章学习了 黑夜路人 相关文章, 奶瓶博士的memcache深度分析文章 和一些 memcache作者的文章 ,在此向他们致敬.
Tags:  一个 需要 可以 进行 服务

你可能感兴趣的:([转]memcache 的源代码分析)