深入memcached之实现篇

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的存放

词汇表

  • slab,page: 内存中的一个分块,一般以1Mbyte为单位或略小于这个值
  • chunk: 一个slab内部又会被分割为相等大小的若干块,以存放适合大小的item
  • slab class: 按chunk的大小,slab会分成不同的类别,前述的slab_clsid就是这个类别的id
  • slot,free chunk: item被移除后空的chunk,不会归还内存,留着给下一个适合大小的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高性能和高稳定性的根源。

内存组织方式带来的问题

  • 内存浪费
    • 从上述实现方式来看,有两种可能存在的内存浪费:
      1. item基本总是小于chunk的,chunk size - item size这段空间被浪费。平均而言,浪费的内存和总内存的比将会达到 (factor - 1) / 2,factor为1.25时,这个比值为12.5%。减少这种内存浪费的一个办法是减小factor的值,让chunk大小增长更缓慢。当然,过小的factor值也会带来一个问题,因为最多只能有200个slab class,所以需要计算好。
      2. 有很多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

参考资料

  1. memcached官方网站FAQ: http://www.socialtext.net/memcached/index.cgi?faq
  2. memcached官方邮件组归档: http://lists.danga.com/pipermail/memcached/

你可能感兴趣的:(深入memcached之实现篇)