memcached整体剖析

memcached是一个优秀的缓存系统,由于工作中经常使用它,经不住一探内部究竟的诱惑,于是乎阅读它的源码,与大家共享之。

本文试图最简单化应用场景,意图展现出memcached是如何设计的,其细节之处可能被隐去,读者自行理解就是。
SET  key val  :在内存储存一个 key => val 键值对;
GET  key       :从内存查找出 key 对应的值;
我将展示这个简单的操作在memcached内部是如何进行的。

一、网络模型
memcached是一个单进程多线程的工作模型,多线程设计为主线程和工作线程,主线程负责端口(11211)监听,工作线程负责accept fd的处理。它本身支持tcp和udp两种协议,这里以tcp为例。
事件机制实现采用libevent这个开源的东东,所以主线程和工作线程都有自己的event_init(),但是内存开辟区域(slab算法)等共享。

主线程跟工作线程之间通过pipe建立通讯连接,每当有请求过来时,主线程accept产生一个fd,然后向fd写,以触发工作线程的读事件,这样达到处理每个请求的通用逻辑,这里简化为结构体 conn,通过 conn_new(...)函数产生。

在网络模型阶段里,主要的逻辑就是针对的conn fd的处理,处理可能切换状态,针对每种状态作不同的操作。
void drive_machine(conn *c)
{
    while (...) {
        switch(c->state) {
            case conn_listening: // 监听端口的处理
                sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen));
                ;; 此sfd以后交给工作线程了,因为有多个工作线程,它采取轮循的方式选定工作线程。

            case conn_read: // 工作线程开始干的活
            ... 此后全部围绕 conn 结构体开始折腾。。
        }
    }
}

二、结构体和内存机制
conn停留在网络模型里,当客户端连接过来,发送 SET key val 时,发生了什么,这才是本文关注的重点。
一切从 process_command函数开始。
void process_command(conn *c, char *command)
{
    ntokens = tokenize_command(command, tokens, MAX_TOKENS);
    if (ntokens >= 3 &&
        ((strcmp(tokens[COMMAND_TOKEN].value, "get") == 0) ) {
        # 处理 get 命令请求
        process_get_command(c, tokens, ntokens, false);

    } else if ((ntokens == 6 || ntokens == 7) &&              
                (strcmp(tokens[COMMAND_TOKEN].value, "set") == 0 && (comm = NREAD_SET)) ) {
        # 处理set 命令请求,我们先从这个开始
        process_update_command(c, tokens, ntokens, comm, false);
    }
}

1、item结构体
当一个SET key val 过来时,memcached将它抽象到 item结构体 ,很明显的,key和val大小都是动态的,但也是有限制的,比如key最大为255。这样的一个结构体需要存储到内存堆里,memcached为了更合理利用内存堆,采用slab算法。所以我们必须先明白slab这个算法如何被应用。呆会回头再细看item这个结构体。

2、slab算法应用
可以说slab算法支撑了memcached这个优秀的缓存系统的实现。这东西并不复杂,也不神秘,举个生活的例子。
我们一个有仓库,仓库是有空间大小的,现在要放不确定大小和数量的物品,如何规划,能放最大的物品呢?直接给您答案哦:将仓库隔开N个房间,每个房间大小都是1M,对每个房间再隔开N个单元,同个每个单元大小一样,不同房间的单元不一样。这样的话,当一个物品要存放时,先根据它的大小,比如 256,就找到有这个大小合适的单元对应的房间,然后存储之。
明白slab原理后,看下下面的图,slabclass就是这个引子。
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* 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; // 一个房间可分割多少个单元,假设一个房间大小为1M
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size; // 每个单元大小
        size *= factor;    // 基因长子,为了分配效率,默认为 1.5
    }

    # 最大的房间
    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;   
}

假设我们要申请 256 的内存空间,看下memcached是如何做到这一点的。
先找出 classid,也就是slabclass的数组的索引,经计算比如为n,通过索引找到了slabclass_t结构体。
slabclass_t的成员slab_list + slabs + end_page_ptr + end_page_free 这几个共同协作,找到了右图标示的1M空间的某一个起点,这个就用来存储一个 item 结构体,至此,你就可以专心对应item了,记住,item有key和val。

3、hash存储
上面分析了item是如何存储到内存的,那么 key => val 这东东是如何被定位的呢?
memcached有个大的数组(用来做hash)primary_hashtable,这个数组的每个元素将存储item链表,因为有可能不同的key的有相同的hash值。item的h_next就是用来实现链表的。
it->h_next = primary_hashtable[hv & hashmask(hashpower)];
primary_hashtable[hv & hashmask(hashpower)] = it;

4、遍历item
需要遍历item时怎么办呢?memcached设计了heads和tails两个变量,这是很普通的队列的一种实现。

5、get命令发生了什么
经过上面的分析,您可以沿着图,去找到key对应的item了。


三、辨则明,思则进
毫无疑问,memcached是非常优秀的一个系统,不管它的效果和实现上,这里提几个可以作为讨论的话题。
1、hash的设计上
对key的存储、查找、删除,memcached采用hash方式,一般情况下是没问题的,但是如果换成红黑树和堆应该是更好的实现方式,效率至上。
2、内存分配
memcached只是粗略上的实现的slab算法,还是存在频繁的malloc这种操作,不管是稳定和性能上,如果配合内存池的方式,肯定会更好。
3、网络模型
memcached采用的单进程和多线程方式,很简单效率也很高。如果采用多进程,需要会用到共享内存,这在效率上比堆高明不了多少。如果可以换成单进程处理监听和事件处理,然后多线程分配事件处理,而不是将事件的epoll操作放在工作线程里,这方案不晓得比之如何。

转载于:https://my.oschina.net/fqing/blog/84358

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值