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
上圖所示中,某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。
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)。
咱們以具體的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便可。
經過預分配方式,每一個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用於儲存數據。
在以前的預分配初始化中,咱們給size爲88bytes的slab class分配了一塊約爲1M的連續內存塊,並將其切分爲了11915個chunk。可是實際使用中,11915個chunk極可能是不夠使用的。若是原有chunk所有被使用後,又有新的數據須要88bytes的chunk內存空間。此時Memcached將會對該slab進行擴容操做。
(上圖中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)
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。
完成了上述插入操做後,咱們再嘗試添加一個新的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。
關於刪除數據item部分的操做大體爲添加操做的逆操做,主要流程爲:
1.經過哈希表獲取該鍵值數據item
2.從哈希表中移除該item節點
3.從LRU鏈表中移該item節點
4.清空item數據並將該chunk從新添加到空閒chunk鏈表
在此再不作具體分析。