Memcached内存管理模型分析

  Memcached 是一个高性能的分布式内存对象缓存系统,它经过在内存中缓存数据和对象来减小读取数据库的次数,从而减轻RDBMS的负担,提升服务的速度、提高可扩展性。本文将基于memcached1.4.15版本源码,对其内存模型进行分析。算法

  首先从业务需求出发。咱们经过一条命令(如set)将一条键值对(key,value)插入memcached后,须要可以作到:一、对该键值数据的高效索引;二、系统可能会频繁的建立新数据和删除旧数据,须要高效的内存管理;三、系统应该可以自行删除长期不使用的缓存数据。数据库

  关于问题1,memcached经过哈希表来对键值数据进行管理,具体的实现中采用连接法来处理hash冲突问题。(本文不考虑多线程中的加锁问题和哈希表扩容问题)数组

  关于问题2,最简单的思路是来了新的数据就malloc内存,将新数据保存在这段新分配的内存中,当数据要被删除时就把这段内存free掉。可是频繁的malloc和free将会致使系统的内存碎片问题,加剧系统内存管理的负担。同时malloc和free做为系统调用,在时间方面也存在必定开销。Memcached的解决方式是建立内存池来管理内存分配,具体实现思路是采用Slab Allocator做为内存分配器。缓存

  关于问题3,memcached给每一个数据记录过时时间,并将同一个slab class的全部数据经过LRU算法进行组织,当插入新数据时经过检查LRU链表对超时的旧数据进行删除。数据结构

主要数据结构介绍:

  item:为键值数据的实际储存结构。item主要由两个部分组成,第一个部分是公共属性部分,包括链接其它 item 的指针 (next,prev,h_next),还有最近访问时间(time), 过时的时间(exptime)等,结构长度固定;第二部分是item的数据部分,由 CAS, key, suffix, value 组成,因为实际的键值数据长度不肯定,所以该部分的结构长度不固定。多线程

  在此处采用了struct的空数组技巧,在公共数据最后定义了空数组data,该data指针自己并不占用任何存储空间,指向数据部分的首地址。数据部分长度不肯定,根据具体数据长度分配的内存大小。实际item结构的长度 = item中的属性部分长度(固定) + 数据部分长度(不固定),所以不一样item之间须要的内存大小是不同的。分布式

 

  Chunk:由申请的连续内存块平均切分而成。好比申请的1M连续内存块,能够被切分红11个88bytes的chunk内存小块。Chunk是实际分配给item的内存空间。Memcached会维护多个不一样大小的chunk内存块。若某个item须要100bytes的内存空间,系统将会取出一个最接近且大于100bytes大小的chunk分配给该item做为内存空间。memcached

image  

  上图所示中,某item公共属性部分须要48bytes内存空间,数据部分须要52bytes内存空间,该item一共须要100bytes连续内存空间。该memcached分别维护了88bytes、112 bytes、144 bytes和184 bytes大小的chunk块群。最接近且大于该item所需内存大小的是size为112bytes的chunk块。所以咱们取出一个还没有被使用的112 bytes 的chunk块,并将该item中的数据保存到该chunk的内存空间中。此时该chunk中将有12bytes剩余内存将做为碎片被暂时浪费。函数

                       image


  

  Slab Class:管理特定大小的 chunk 的集合。Memcached每次默认分配的一个连续内存块为1M大小,它们被切分为不一样大小的chunk。可是不一样chunk的需求量不一样,有的状况下某些大小的chunk只需一个连续内存块切分的数量便可知足业务须要,但有的大小的chunk需求量比较大,须要分配更多的连续内存块来进行切分。这些切分为相同大小的chunk块群,都由对应的slab class进行管理。性能

       一般memcached会指定一个最小的chunk大小,同时设置一个增加因子。系统依次建立管理随增加因子增加且保持字节对齐的chunk大小的slab class。好比最小chunk大小为88bytes,增加因子为1.25,则系统将会分别建立管理88bytes大小、112bytes大小、144bytes大小的chunk的slab class。

image

  slabclass的属性说明。

typedef struct {
   unsigned int size;     //该 slabclass 的 chunk 大小 
    unsigned int perslab;   //表示每一个 slab 能够切分红多少个 chunk,若是slab为1M,则perslab = 1M/size
    void *slots;          //回收到的item链表
    unsigned int sl_curr;   //当前链表中有多少个回收而来的空闲chunk  
    unsigned int slabs;     //该class一共分配了多少chunk
    void **slab_list;       //list数组用于维护chunk.
    unsigned int list_size; /* size of prev array */        
    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;

 

 

内存初始化:

  Memcached的内存初始化方式分为两种,分别为预分配方式和按需分配方式。Memcached默认采用按需分配方式。

 

  在预分配方式中,memcached会在启动时经过malloc申请64M的连续内存(可配置),而后memcached根据初始chunk大小和增加因子建立管理不一样chunk大小的slab class,每一个slab class依次从以前申请的64M内存中获取1个1M的连续内存块,并将该内存块切分为对应大小的chunk块并进行管理,直到申请的内存用完为止。

  下图表示预分配方式下初始化时建立的前3个slab_class,每一个slab class分配了一个1M的连续内存块。其中slab_class1切分为了11915个每一个大小为88bytes的chunk,slab_class2切分为了9362个每一个大小为112bytes的chunk,slab_class3切分为了7281个每一个大小为144bytes的chunk,更多slab_class以此类推(图中每一个slab class中只画了4个chunk)。

image

 

  咱们以具体的size为88bytes的slab class为例进行说明。该slab class目前被分配了约1M的连续内存,这段内存被挂载在slab_list[0]上。这段内存被切分红了11915段(图中只画了4段),每段做为一个88bytes的chunk。每个chunk又被item初始化,并经过slots指针做为表头节点,与每一个item的prev、next指针共同组成了空闲item双向链表。初始化完后该slab class中存在11915个空闲的item。每当系统须要一个88bytes的chunk时,就经过表头slots从链表中取出一个chunk便可。

image

  经过预分配方式,每一个slab class在初始化阶段公平的分配到了约为1M的连续内存,让系统在一开始每一个slab都有chunk可供分配。可是实际业务中,不一样大小的chunk使用频率并不相同,有的slab中的chunk很快就被使用完毕,而有的slab中的chunk又长期未被使用,形成内存的浪费。所以Memcached默认采用的是按需分配的方式。

 

  在按需分配的方式中,初始化阶段Memcached只会为每一个slab指定对应chunk大小,并不会给slab分配实际内存。

  当有实际数据待储存到该slab下的chunk时,Memcached首先会判断该slab是否有过时的item待回收使用,若是没有再判断是否有空闲的chunk,若是尚未,才会给该slab分配1M连续内存。这时slab将会对这块连续内存进行切分chunk管理,并从中取出一个空闲chunk用于储存数据。

Slab内存扩容:

  

  在以前的预分配初始化中,咱们给size为88bytes的slab class分配了一块约为1M的连续内存块,并将其切分为了11915个chunk。可是实际使用中,11915个chunk极可能是不够使用的。若是原有chunk所有被使用后,又有新的数据须要88bytes的chunk内存空间。此时Memcached将会对该slab进行扩容操做。

image

(上图中slab_list[0]中的chunk均未使用,并不会实际申请slab_list[1]的新连续内存块)

  在slab中存在一个初始大小为16的slab_list数组,用于管理连续内存块。其中预分配的第一个连续内存块被挂载在slab_list[0]上。当第一个连续内存块中chunk不够用时,Memcached将会再次给该slab分配一个大小约为1M的连续内存块,并挂载在slab_list[1]上,并一样将该段连续内存切分为11915个chunk,并将这些新chunk添加到该slab的空闲双向链表中。此时该slab一共管理11915*2个chunk,其中新分配的11915个chunk为空闲chunk。

  由于slab_list数组初始大小为16,理论上该slab能够挂载16个这样的连续内存,每一个连续内容可切分为11915个chunk,也就是slab可以管理16*11915=190640个chunk。若是190640个chunk都不能知足这个slab的chunk需求,那么Memcached将会对slab_list经过realloc进行扩容,每次扩容的大小为原slab_list的大小的2倍。一次slab_list扩容后,该数组大小为32,将可分配32*11915个chunk。只要系统内存足够,经过slab_list的扩容和分配新的连续内存块,每一个slab class能够管理无数个大小相同的chunk。

 

哈希表:

  Memcached的哈希表采用连接法实现。hashtable被分红多个桶bucket,每一个item经过hash函数肯定具体的bucket,而后连接到该bucket上,若是该桶中已存在连接的item(即出现了哈希冲突),则将这个item经过h_next指针造成该bucket下连接的单向链表。图中,item A和item B都被哈希映射到了bucket[1]中,它们经过h_next组织为单向链表,且bucket[1]做为链表表头。(能够参考STL中的unordered_map)

image

 

LRU链表:

  Memcached中每一个slab中都维护了一个LRU链表,来组织该slab中已经被分配的item块,用于记录“最近最少使用”的item信息。其中heads指向链表的头节点,tails指向链表的尾节点。每当有新chunk被使用时,将会将该chunk的item添加到LRU链表头。或者有原使用的item被修改,也会将其从链表中移动到LRU链表头处。经过该机制,保证了链表头部分的的item为新建立或新修改的数据,链表尾item为该slab中储存最久的数据。

  Memcached采用了惰性删除的机制,系统不会主动监视item中数据是否过时,而是在get的时候查看该item的时间戳,若是已过时就删除并将该chunk释放到空闲链表中。

  同时在新数据插入中,Memcached也会优先判断该slab的LRU链表尾部的item节点是否超时,若是超时的话,Memcached也会优先删除并使用已经超时的item的chunk做为新数据的储存空间。

  当该slab的LRU链表尾部item节点并未超时,可是slab中无可用chunk,且没法从系统中扩容到新的内存空间时,Memcached将会直接摘取LRU链表中的最后的item,强行删除并将其空间分配给新的数据记录。

  Memcached能够配置为禁止使用LRU机制,这样的话当该slab中chunk耗尽且分配不到新内存时将会返回错误。

插入数据流程:

  在Memcached中插入数据主要分为如下几个流程:

  1.哈希查找是否存在相同键值

  2.根据item大小选择slab class

  3.从过时item或空闲链表中分配item

  4.加入LRU链表及哈希表

  咱们以插入一个名为Data1的键值数据进行说明。首先系统将根据Data1的key去查询哈希表,是否存在相同的键值数据,若是数据相同,只执行更新操做;若是key相同但value不一样,对原item执行删除操做,并继续执行。

  data1 + item 部分所需内存 < 88bytes,所以咱们选择管理88bytes的slab class。首先判断该slab的LRU链表尾部tails所指是否存在超时过时节点,此时LRU链表为空,tails指向null,并没有过时节点。

  而后再判断该slab class中slots指针维护的空闲链表,此时空闲链表中存在空闲chunk。Memcached将空闲链表的头节点chunk取出,并将Data1的数据保存到该chunk中。此时空闲chunk双向链表如图浅黑色指针部分。

  接着经过hash映射肯定该item对应于哈希表中bucket[1]中,由于以前该哈希表中bucket[1]已经挂载了2个数据item,所以咱们将Data1的item添加到bucket[1]中的单链表的表头。此时哈希表如图红色指针部分。

  咱们把该item加入该slab class的LRU链表,此时该item将做为该slab class第一个被使用的item。LRU链表如图蓝色指针部分。

  最后咱们修改该slab class中的属性,由于一个chunk已经被使用,所以咱们将该slab class中的sl_curr当前可用chunk修改成11914。

  

image

 

再添加一个具体数据

  完成了上述插入操做后,咱们再尝试添加一个新的Data2数据,data2 + item 部分所需内存依旧小于88bytes。

  首先依旧是进行哈希查找是否存在相同键值,略过不谈。

  一样是选择管理88bytes的slab class,判断该slab的LRU链表尾部tails所指是否存在超时过时节点。此时LRU链表只有以前储存Data1的item节点,tails即指向它,该item节点并未过时。

  此时该slab class的空闲链表中依旧存在空闲chunk,咱们再次从该slots维护的空闲双向链表中取出表头的88bytes的chunk,并将Data2的数据保存到该chunk中。

  经过hash映射肯定该item对应于哈希表中bucket[3]中,由于以前中bucket[3]中并未挂载任何item,所以咱们将Data2的item添加到bucket[3]中的单链表的表头。

  咱们把该item加入该slab class的LRU链表的表头。此时Data2所对应的item位于LRU链表的第一个节点,Data1对应的item位于LRU链表的第二个节点。

  最后将该slab class中的sl_curr当前可用chunk修改成11913。

image

 

  

删除数据流程:

  关于删除数据item部分的操做大体为添加操做的逆操做,主要流程为:

  1.经过哈希表获取该键值数据item

  2.从哈希表中移除该item节点

  3.从LRU链表中移该item节点

  4.清空item数据并将该chunk从新添加到空闲chunk链表

  在此再不作具体分析。

相关文章
相关标签/搜索