1. 程式人生 > >memcached記憶體分配及回收初探

memcached記憶體分配及回收初探

這篇文章很清晰的描述了memcached對記憶體分配和回收的機制。例如:slabs分割完畢後,當再次申請記憶體的時候,memcached是怎麼去申請記憶體的,當記憶體滿了以後,memcached的回收機制是什麼,回收的是哪一快記憶體?

在小虎的授意下,對memcached(後面簡稱mc) 的記憶體分配及回收機制進行了深度分析和測試,以下是一些學習的心得,和大家共同探討一下,期望能拋磚引玉

mc簡介:

mc是由LiveJournal技術團隊開發的一套分散式物件快取系統,基於c語言,目前應用十分廣泛,它可以應對任意多個連線,使用非阻塞的網路I/O。它的使用非常簡單和方便,最常用的功能不超過5個方法(set,get,delete...)。目前pconline的網站群基本上都是使用mc做快取服務

mc在很多時候都是作為資料庫前端緩衝使用的。因為它比資料庫少了SQL解析、磁碟操作等開銷,而且它是使用記憶體來管理資料的, 所以它可以提供比直接讀取資料庫更好的效能,在大型bbs系統中,訪問同樣的資料是很頻繁的,mc可以大大降低資料庫壓力,使系統執行效率提升。 另外,mc也經常作為伺服器之間資料共享的儲存媒介,例如在SSO系統中儲存系統單點登陸狀態的資料就可以儲存在mc中,被多個應用共享。

命題提出

      前段時間登入系統出現了一個比較怪異的問題,剛剛登入的使用者,還未到session過期時間就開始拋空異常,檢視mc記憶體利用率不到60%,檢查重啟mc後問題得到緩解,後來小虎分析可能是mc中快取的未過期資料被沖掉,於是有了下面的分析

mc記憶體分配機制簡介

memcached預設情況下采用了名為Slab Allocator的機制分配、管理記憶體,Slab Allocator的基本原理是按照預先規定的大小,將分配的記憶體分割成特定長度的塊,以完全解決記憶體碎片問題。

先來解釋一下與Slab Allocator儲存有關的幾個術語:

Page:分配給Slab的記憶體空間,預設是1MB。分配給Slab之後根據slab的大小切分成chunk。
Chunk:用於快取記錄的記憶體空間。
Slab Class:特定大小的chunk的組。

Growth Factor:增長因數,預設為1.25(較早的版本固定為2)

mc啟動後,會根據這個factor,計算出從1M逐步遞減的不同的slab,如factor=1.25時:

Java程式碼  收藏程式碼
  1. <span style="font-size: medium;">slab class   1: chunk size     88 perslab 11915  
  2. slab class   2: chunk size    112 perslab  9362  
  3. slab class   3: chunk size    144 perslab  7281  
  4. slab class   4: chunk size    184 perslab  5698  
  5. slab class   5: chunk size    232 perslab  4519  
  6. slab class   6: chunk size    296 perslab  3542  
  7. slab class   7: chunk size    376 perslab  2788  
  8. slab class   8: chunk size    472 perslab  2221  
  9. slab class   9: chunk size    592 perslab  1771  
  10. slab class  10: chunk size    744 perslab  1409  
  11. ...</span>  

第一列資料(slab class),為slab的編號;

第二列資料是chunk的大小,跟slab class是一一對應的關係,可以通俗的理解為slab就是存放一組相同大小chunk的集合,只不過這個集合是固定的(1M),

第三列資料,表示每種不同slab中的page可以存放的chunk個數,實際上等於1MB/ (chunk size),例如slab1中的chunk size是88B,那麼這種slab中每個page中可以存放的chunk個數為 1MB / 88B ,約等於11915

很顯然,slab的chunk size越大,其中的每個page包含的chunk數量就越少

如圖所示:

新入物件時,會根據自身大小來匹配slab列表,比如100KB的物件,根據最小空間損失原則,會被放入到slab2(size:112B)對應的page下,如下圖


這時,如果slab2下的page中有尚可以使用的chunk(即空閒的chunk或者過期的chunk),slab2會優先使用這些chunk,在沒有chunk可用的情況下,mc會去記憶體中再申請一個page,然後切分成chunk,然後使用;需要注意的是,根據 Slab Allocator演算法, 該例項中的100KB物件,是永遠沒有機會存放到其他slab(如slab3,slab4等等),即便是其他slab中有大量的可用chunk,細心的朋友會發現,這種機制很有可能會導致記憶體浪費嚴重,mc命中率降低等問題,對,這種問題真的存在,這也正是這種機制的缺點,下面會進行詳細的分析和探討

mc資料刪除機制簡介:

首先我們知道,快取在mc中的資料,不會主動從記憶體中消失,也就是說mc不會監視記錄是否過期,而是在client端執行get方法時才去檢查時間戳是否過期(這樣做的目的就是避免在過期監視上耗費cpu資源,以提高mc的響應能力);每次有新物件加入時,mc會優先將物件置於已超時的同一規格chunk中,然而,即使如此,記憶體仍然會發生追加新記錄時空間不足的情況,那麼,當mc記憶體耗完後,又是怎樣處理新入的資料呢?mc有兩種處理策略,一種是預設的LRU(Least Recently Used),指刪除近段時間最少使用同規格chunk,再將物件塞入),另一種策略是存滿即不可再存,除非有過期的物件,否則會報錯

再回頭看問題:

想必各位已經發現我們的登入狀態資料是怎麼被沖走的了,對,沒錯,LRU!在記憶體還有將近一半的情況下,就發生了LRU,為什麼呢?這一半空閒的記憶體,表面是空閒的,實際上已經被mc將其打包成page分配到了其他stat裡,而這些stat即便空閒、資料過期,也不會被mc回收已供其他繁忙的slab呼叫的。產生這種情況的直接原因就是,mc啟動後,較大chunk size的slab同時間大量湧入mc,假設slab為20,這時,mc不得為slab 20分配大量的page,而在一小段時間後,slab 20中的chunk紛紛過期,但是它們曾經佔用的page就永遠不會被mc主動回收了,除非再有與slab20同規格的物件進入時,這些page才會重新得到使用的機會,與此形成強烈對比的slab2(chunk size = 112B)卻處於無可用chunk,無記憶體已供分配新page的境地,這個時候,LRU出場了,會按時間相關度清理掉一些尚未過期的slab2 chunk,如此造成快取來去匆匆,實際上效能嚴重下降

LRU:

首先,在memcache分配的時候,初始化會去分配一系列的slab,例如初始的slab為88k,然後factor為1.25,那麼你會發現開始的時候 就會有:88,112,44,....一直到1M大小的slab各一個,假如物件集中在其中某一個區間,那麼很快那個slab就會分配滿,此時如果記憶體還有,那麼就會新建一個同樣大小的slab作為鏈掛在第一個同等大小的slab上,如果說記憶體也滿了,slab也滿了,那麼就開始LRU演算法了。 
但是Memcached的LRU演算法是針對slab的,而非全域性的,如果資料集中在一個slab上,那麼初始化的時候其他幾個slab肯定就浪費了,同時,如果slab的大小和物件的大小有比較大的差異,那麼浪費的將會更加巨大。所以在評估使用 memcache初始大小和factor的時候需要注意這些,選擇適合的初始化size和factor,減少slab分配的浪費。

解決思路

思路1.通用解決方法: 調整growth factor

逐步調整growth factor,並觀察chunks的分佈,儘量將資料物件的大小控制到一定區間內,啟動時加入-f引數即可,在factor=1.25時有39組slab

Java程式碼  收藏程式碼
  1. $memcached  -m 5 -vv  -l localhost  -p 11211 -f 1.25s  
  2. slab class   1: chunk size     88 perslab 11915  
  3. slab class   2: chunk size    112 perslab  9362  
  4. slab class   3: chunk size    144 perslab  7281  
  5. slab class   4: chunk size    184 perslab  5698  
  6. slab class   5: chunk size    232 perslab  4519  
  7. slab class   6: chunk size    296 perslab  3542  
  8. slab class   7: chunk size    376 perslab  2788  
  9. slab class   8: chunk size    472 perslab  2221  
  10. slab class   9: chunk size    592 perslab  1771  
  11. slab class  10: chunk size    744 perslab  1409  
  12. slab class  11: chunk size    936 perslab  1120  
  13. slab class  12: chunk size   1176 perslab   891  
  14. slab class  13: chunk size   1472 perslab   712  
  15. slab class  14: chunk size   1840 perslab   569  
  16. ...  
  17. slab class  36: chunk size 250376 perslab     4  
  18. slab class  37: chunk size 312976 perslab     3  
  19. slab class  38: chunk size 391224 perslab     2  
  20. slab class  39: chunk size 489032 perslab     2  

 當growth factor調大成2以後,slab class明顯變少,只有13組了

Java程式碼  收藏程式碼
  1. $memcached  -m 5 -vv  -l localhost  -p 11211 -f 2  
  2. slab class   1: chunk size    128 perslab  8192  
  3. slab class   2: chunk size    256 perslab  4096  
  4. slab class   3: chunk size    512 perslab  2048  
  5. slab class   4: chunk size   1024 perslab  1024  
  6. slab class   5: chunk size   2048 perslab   512  
  7. slab class   6: chunk size   4096 perslab   256  
  8. slab class   7: chunk size   8192 perslab   128  
  9. slab class   8: chunk size  16384 perslab    64  
  10. slab class   9: chunk size  32768 perslab    32  
  11. slab class  10: chunk size  65536 perslab    16  
  12. slab class  11: chunk size 131072 perslab     8  
  13. slab class  12: chunk size 262144 perslab     4  
  14. slab class  13: chunk size 524288 perslab     2  

很顯然,factor越小,chunk匹配得就越精準,但是slab組就會分得越多,而產生LRU的機會也會增加,factor越大,分組就越少,產生LRU的機會就越小,但是chunk匹配精準度會有所下降,如在資料大小為130B時,如果f=1.25,mc會將其放入class3(chunk size = 144B),浪費的空間為14B;如果f=2.0,mc會將其放入class2(class size = 256)中,浪費的空間為126B,相當驚人,所以factor的大小設定在一個比較平衡的值,一般以預設的1.25較為理想。

思路2.動態調整slab中page的數量

大體思路是使用java客戶端監控程式,定時檢查每個slab的使用情況,動態調整每個slab中的page,將某個比較空閒的slab中的page移動到另外的slab中去,不過mc的開發人員認為在mc中遍歷slab和page移動會造成較大系統開銷,所有沒有提供直接的api已供呼叫,一直遮蔽呼叫,在1.28版本以前,還可以通過在memcached.h中新增巨集#define ALL_SLABS_REASIGN並重新編譯使slabs reassign命令生效,但在使用過程中,出現了大量的效能問題,mc穩定性下降,而且在資料移動過程中,會導致mc不可寫的問題,針對這些弊端,mc的開發團隊在其後續版本中已經徹底刪除相關處理邏輯,我也嘗試對1.28的原始碼進行修改和編譯,基本可用,但是何時呼叫 reassign,以及reassign後對系統造成的影響仍然需要進一步的資料分析,繼續跟進中。。。