memcached是一个内存(mem)缓存(cache)服务(d)。memcached的实现中不乏精巧的设计,其中内存分配部分可以说是其灵魂所在。作为一个基于内存的缓存,如何有效地利用内存无疑是最重要的。另外,懒惰的策略,使用libevent等都保障了高性能。(以下的内容都是基于1.2.5版本的实现)
item的构造
- 除了保存必要的key-value对之外,item结构还定义了其他一些属性(上图是1.2.5版本中的item结构)
- 三个指针字段的作用是构造链表,具体的使用情况在下面两节会提到
- time表示这个item最后一次被访问的时间, exptime为过期时间(相对时间,参考点是服务器启动时间)
- 几个长度字段
- slab_clsid会在下面做详细介绍
- 整个结构体(不含key,suffix,data部分)占40个字节内存空间
item的查找
- 采用普通的hashtable的实现,将key作hash,相同hash的item将以链表方式存放,如上图
- item构造中的h_next指针就是用来构成这个链表的
- hash函数的具体实现可以参考http://burtleburtle.net/bob/hash/doobs.html
,这同时也是一篇非常不错的分析hash函数的文章
- 注意:为了达到良好的一次命中率,hashtable是会自动扩展的。hashtable所占的内存约为item_num * 4byte * 4/3。如果一个cache实例中的item数量极多(100M或更多),这张hashtable本身的内存占用就不可忽视。在以小对象为主的缓存中,这一点是要引起重视的。
item的存放
- memcached启动时会构建一个slabclass的数组(如上图)。该数组的一个元素表示一个slab class。第一个slab class中chunk的大小为sizeof(item)+48(可通过参数设置)=88bytes,之后每个slab class的chunk大小为前一个乘以一个系数factor(可通过参数设置,默认为1.25),并保证能被8整除(这样在64位系统下也能对齐),直到大于512kb为止,这也就是size字段的值。每一类slab中chunk数(perslab字段)为1Mbytes/每个chunk大小,取整。系统最后会分配一个单chunk 1Mb的slab class。初始化时会针对每个slab class在内存中创建一个slab。
- memcached会根据需要存放的item的大小选择一个能容纳它的最小chunk size的slab class,放入该class的slab中。
- 每个slab class的end_page_ptr指针会指向这个slab class的最后一个分配的slab未使用部分。当一个item被存放进来,这个指针就会简单地后移一个chunk的大小(注意不是一个item的大小),这个动作就好像是在slab上刻一个槽(slot)出来,把item放进槽里。
- 当一个item删除的时候,放置它的槽并不会返还给内存,而是继续留给后面的item使用,slot数组就是用来记录这些空槽的。
- 所以当一个slab_clsid为14的item需要被保存的时候,memcached会先查找这个slab class中有没有空的槽,有的话,直接放进去。没有的话就从end_page_ptr处划一个chunk大小的槽,这时如果slab用完了,就再分配一个新的slab出来。这就是memcached item存放的基本步骤。
- 可以看到,这样内存的利用是很高效的,比起随到随分配,随删随释放的策略,不会产生内存碎片,不需要多余的维护,这也是memcached高性能和高稳定性的根源。
内存组织方式带来的问题
- 内存浪费
- 从上述实现方式来看,有两种可能存在的内存浪费:
- item基本总是小于chunk的,chunk size - item size这段空间被浪费。平均而言,浪费的内存和总内存的比将会达到 (factor - 1) / 2,factor为1.25时,这个比值为12.5%。减少这种内存浪费的一个办法是减小factor的值,让chunk大小增长更缓慢。当然,过小的factor值也会带来一个问题,因为最多只能有200个slab class,所以需要计算好。
- 有很多slab class中可能会没有item或只有很少的,这取决于你的item大小是分散分布的还是集中分布的。极端情况下,你的item是定长的,那它只会分布在一个slab class中,其他class初始化时分配的一个slab大小的空间都是浪费的。
- 从上述实现方式来看,有两种可能存在的内存浪费:
- slab回收和复用问题
- 上述实现中,slab只会被分配而不会被回收,如果这个slab class中不断地有item进来,那这是一个优点。但如果一个slab class一开始存放了很多item。然后新进来的item大小发生变化,又都存放到其他slab class中去了呢?这个slab class无疑多占了很多空间,而且当item开始过期,有很多slab应该被收回。
- memcached本身有slab回收机制,如果开启,将把slab大小统一设置成1Mb。(TODO)
LRU队列
- 当没有可用的空间给新来的item时,会按最近最少使用原则逐出cache中的item,memcached为每一个slab class维护了一个LRU队列,实现为一个双向链表(如上图)。
- 新来的item会从heads端加入,访问一个item,该item也会被取出,被重新放入heads端。所以越靠近tails端的item越老。
- 如果一个slab class没有空间了,会从它的tail开始,沿着链表往上,查找第一个不在被使用的item,并将之逐出,腾出一个槽来给新人用(实际实现只搜最后50个)。
懒惰策略(TODO)
- lazy expire
- lazy delete
参考资料
- memcached官方网站FAQ: http://www.socialtext.net/memcached/index.cgi?faq
- memcached官方邮件组归档: http://lists.danga.com/pipermail/memcached/