1. 程式人生 > >memcached內存管理機制分析

memcached內存管理機制分析

BE 實現思路 oid 之前 color 禁止 內存管理機制 abs 才會

memached是高性能分布式內存對象系統,通過在內存中存儲數據對象來減少對磁盤的數據讀取次數,提高服務速度。

從業務需求出發。我們通過一條命令(如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 組成,由於實際的鍵值數據長度不確定,因此該部分的結構長度不固定。

技術分享圖片

Chunk:由申請的連續內存塊平均切分而成,比如申請的1M連續內存塊,可以被切分成11個88bytes的chunk內存小塊。Chunk是實際分配給item的內存空間,Memcached會維護多個不同大小的chunk內存塊,若某個item需要100bytes的內存空間,系統將會取出一個最接近且大於100bytes大小的chunk分配給該item作為內存空間。

技術分享圖片

比如: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剩余內存將作為碎片被暫時浪費。

技術分享圖片

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。

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塊並進行管理,直到申請的內存用完為止。

技術分享圖片

當有實際數據待儲存到該slab下的chunk時,Memcached首先會判斷該slab是否有過期的item待回收使用,如果沒有再判斷是否有空閑的chunk,如果還沒有,才會給該slab分配1M連續內存。這時slab將會對這塊連續內存進行切分chunk管理,並從中取出一個空閑chunk用於儲存數據。

按需分配的方式,初始化階段Memcached只會為每個slab指定對應chunk大小,並不會給slab分配實際內存。

slab內存擴容

預分配初始化中,我們給size為88bytes的slab class分配了一塊約為1M的連續內存塊,並將其切分為了11915個chunk。但是實際使用中,11915個chunk很可能是不夠使用的。如果原有chunk全部被使用後,又有新的數據需要88bytes的chunk內存空間。此時Memcached將會對該slab進行擴容操作。

在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。

hash表實現

  Memcached的hash表采用鏈接法實現。hashtable被分成多個桶bucket,每個item通過hash函數確定具體的bucket,然後鏈接到該bucket上,如果該桶中已存在鏈接的item(即出現了哈希沖突),則將這個item通過h_next指針形成該bucket下鏈接的單向鏈表。圖中,item A和item B都被哈希映射到了bucket[1]中,它們通過h_next組織為單向鏈表,且bucket[1]作為鏈表表頭。

技術分享圖片

LRU鏈表

每個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耗盡且分配不到新內存時將會返回錯誤。

數據存儲流程

哈希查找是否存在相同鍵值-》根據item大小選擇slab class-》從過期item或空閑鏈表中分配item-》加入LRU鏈表及哈希表

數據刪除流程

通過哈希表獲取該鍵值數據item-》從哈希表中移除該item節點-》從LRU鏈表中移該item節點-》清空item數據並將該chunk重新添加到空閑chunk鏈表

memcached內存管理機制分析