從源碼角度理清memcache緩存服務


 

 memcache作為緩存服務器,用來提高性能,大部分互聯網公司都在使用。

 

  前言

 

   文章的閱讀的對象是中高級開發人員、系統架構師。

 

   本篇文章,不是側重對memcache的基礎知識的總結,比如set,get之類的命令如何使用不會介紹。是考慮到,此類基礎知識網絡已經有一大把資料,所以更加傾向於深入性的知識點。文章側重的重點是對memcache的原理理清楚、在實戰中自己所遇到的坑、自己的思考心得與理解。

 

 好記性不如爛筆頭,整理文章的初衷是為了加深自己的理解,對知識進行梳理,人的大腦會逐步遺忘,記下來的文字,方便以后查閱。

 

   本文太長,一時看得暈,請挑你你需要的部分看。或者收藏起來,以后有更新部分,可以繼續訪問。筆者以后也會將一些疑惑的知識點,繼續完善到文章中去。

   文章為原創,轉摘歡迎你注明出處,謝謝!

 

一、memcache的原理

 

關系型數據庫的數據是放入在硬盤上的。磁盤的瓶頸是i/0(機械設備,靠磁盤片旋轉來定位數據)

 

memchace利用內存的速度快,把數據存入到內存中。內存這個設備有個特點,斷電后,內存里面的所有數據就會丟失。

 

基於這個特點,我們在架構的系統中使用memcache時,放入memcache中的數據,最終還是要在磁盤上有備份,不然丟失掉了。沒地方去找了。

 

memcache本質就是在管理着一大片的內存區域。我們的程序去跟memcache提供的接口存儲和獲取數據。注意,這個內存,memcache是跟操作系統去申請內存的。

 

二、memcache管理內存的機制

 

先了解memcache的數據類型,方便理解后續知識。

 

memcache只提供了一種數據類型:key->valuekey是字符串,value也必須是字符串。

 

2.1、額外知識點:操作系統與內存的關系

 

理解一個大前提非常重要:內存是操作系統在管理。

操作系統是責與所有硬件交換(硬盤,磁盤,內存、外設打印機等)。我們電腦插槽那個內存,也是操作系統在統一管理。

於是,在操作系統運行下的所有軟件(mysql,memcache,nginx等等),需要內存的時候,都是要去跟操作系統申請內存。

 

軟件向操作系統申請內存的辦法

 

    每次跟操作系統申請內存的最小單位是頁(page)。就像稱重規定最小單位是克,人自己這么約定的。

    關鍵詞:一次申請的最小單位是頁(page)。

    注:並不是說一次只能申請一頁(4kb)。是指只能按照4k*n為單位進行申請內存。

    操作系統是這樣管理內存的:

    把內存划分成等份大小的份(一塊一塊的)。這種在操作系統概念中叫做分頁法。

     有分頁又有分段,概念容易弄暈了。其實,分段技術早於分頁技術。過去是操作系統是使用分段技術來管理內存(程序運行在哪個內存區間,這就是段)

     但是分段法存在一些缺陷,所以后來操作系統使用分頁法了。

 

2.2、核心機制:slab機制介紹

 

 memcache借鑒了linux操作系統中的slab管理器的模式,使用slab方式來管理從操作系統申請到的內存。

 

2.2.1、借鑒了linux的slab管理器

 

   slab管理器的來源。在內核中,經常會發生對有限的幾種數據結構頻繁的分配內存和回收內存,比如進程描述符struct task_struct,索引節點對象struct inode等等。相比4KB或者8KB的物理頁而言,這些對象往往較小,例如進程描述符一般只有1.7KB左右,索引節點對象則更小。那么對於這種高頻發 生的小數據結構的內存分配和回收,是否有可能進行優化呢?
 
 
   計算機科學家Jeff Bonwick早已關注到這一點。同時他還發現, 內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。於是他設想這樣改進:不將內存釋放回一個全局的內存池,而是將內存保持為針對特定目而初始化的狀態后續的內存分配不需要重復初始化,因為從上次釋放和調用析構之后,它已經處於所需的狀態中了。基於這些思路,slab分配器(slab allocator)應運而生。
 

   slab分配器主要的功能就是對頻繁分配和釋放的小對象提供高效的內存管理。它的核心思想是實現一個緩存池,分配對象的時候從緩存池中取,釋放對象的時候再放入緩存池。

 

   memcache也使用這樣的辦法:模擬實現了一個slab分配器,把從操作系統申請到內存緩存起來,即便是刪除數據了,這部分內存也不會返回給操作系統,只是標識一個狀態"空閑"。目的是方便下回使用。slab管理器的核心思想其實就是:對頻繁使用的小對象進行緩存起來,不要釋放掉。最終避免頻繁的申請、釋放操作造成性能問題(耗資源)。

 

  

   實際上早期的memcache版本並沒有采用slab管理器的思路,后來的版本才改善,使用slab機制。

 

   memcache在使用slab機制出現以前,內存的分配是通過對所有記錄簡單地進行malloc和free來進行的(對操作系統申請內存和釋放內存)。 但是,這種方式會導致內存碎片,加重操作系統內存管理器的負擔,最壞的情況下, 會導致操作系統比memcached進程本身還慢。Slab Allocator就是為解決該問題而誕生的。

 

2.2.2、slab class 和slab page、chunks的關系

 

    把相同大小的內存塊,歸類到一個組,這個組叫做slab class。這樣子是解決了linux的slab管理器的思想。

   memcache向操作系統申請內存,是以slab page為單位的,slab page的大小是1m。也就是每次申請1m(這個值可以修改,啟動的時候-I參數指定,最小1K,最大128M)。

   注:這個page不是操作系統概念中的page,memcahce中的page與操作系統的page不是一回事。一些文章中用page來稱呼,我以前被誤導了,以為是操作系統概念中的page,操作系統概念中的page,一個單位是4kb。而memcache這里是1m了。個人理解,memcache之所以要稱呼為page,是因為這是從操作系統申請的內存空間,恰好操作系統分配內存給應用軟件是以page為單位的。

    一個slab page里面會有很多的chunks(大小相同的內存塊),chunks是真正存儲key->value的區域(其實就是划分出來的內存小塊)

  memcache從操作系統申請內存,一次跟操作系統申請1m大小的內存(1m認為就是一個slab page)

     然后把這個1m的內存,分成相等大小的chunks。比如1m=1024kb=1024*1024b=103445504(字節)

     假設平分的大小是88個字節。那么就是103445504/88=90112chunks

 

 

    如上圖:slab class 1里面都是88字節的chunksslab class 2里面都是112字節的chunks

一個slab class里面可能有多個slab page。至少是一個slab page(當chunks不夠用的的時候,就會跟操作系統申請新的slab page加入到slab class中)。如下圖表示slab class里面有多個slab page了(一個slab calss下面的每個page,其擁有的chunks數量都是一樣的)。

 

思考:每個slab class中的chunks的大小是由什么決定的呢?

     由上一個slab class中的chunks大小決定。計算公式為:當前slab class中的chunks大小=上一個slab classchunks大小*增長因子。

     增長因子默認是1.25。比如上一個是288,那么288*1.25=360下一個slab class中的chunks大小是:360*1.25=456。

       注:啟動memcache的時候,第一個slab class里面的chunks都是48個字節。這個值是可以配置的。

 

 2.2.3、往memcache添加key的內部機制

    添加一個key->value的步驟如下:

 

1、先定位到合適的slab

 

      判斷邏輯是這樣:新加入一個key->value(也叫item),先計算這個key->value的整體大小。假設是118個字節。

     那么去slab列表里面,尋找哪個slab能夠存儲下。最終找到是144字節的chunks(slab3)能夠存儲。

      思考:為什么是slab class 3,而不是slab class 4slab class 5呢?

     memcache的計算辦法是,優先選擇最小的slab class。源碼在slabs.c中的slabs_clsid()函數中。這個函數傳入一個容量進去,返回能夠存儲其容量的slab class編號。

 

2、定位到合適的slab

 

          到這一步,現在找到slab3是可以存放。於是進入到slab3里面去。那slab3里面有沒有空閑空間來存儲呢?

         所以得先看看slab3里面是不是有空閑的chunksmemcache為每個slab維護了一個空閑鏈表。通俗理解就是:記錄這個slab那些chunks空間是可以用的。包括過期、刪除狀態(標識為軟刪除)

         在該slab class中,會優先選擇過期的chunks空間和刪除掉的chunk進行來存儲,其次將選擇未使用過的chunk(即完全空着,從來沒有用過的chunks)進行存儲。

         思考:這樣做的好處是什么?不要污染掉真正空閑的地方。空閑的trunk是沒有存儲任何數據的。而被刪除和過期的trunks則里面存儲了數據,所以優先使用。借鑒這種思想。

         通過上面步驟,在當前slab class里面假設找到了可以用的chunks,那么返回一個chunks以供使用。

         假設當前slab class中沒有可用的chunkns,怎么辦呢?

         此時,memcache就會跟操作系統去申請內存了。默認一次申請1m的內存空間。申請1m。然后繼續切分。

         注意:關於切分,很多這里沒有說透,以前我被誤導了。這時候其實不是新開slab class。是對當前的slab class 3進行的操作:

         當前的slab class 3里面的chunks都是144個字節。那么好,新申請的1m內存,就按照144個字節來切分。計算方法列出來看看:

            1m=1024k=1024*2014b=103445504字節。

            103445504/144字節=718317個塊(chunks)

             這718317chunks,就會加到slab class 3里面去。

 

三、memcahce的監控

 

 使用一個php語言開發的界面管理工具。名稱叫做memadmin。

 下載地址:http://www.oschina.net/p/memadmin

 

 根據實戰經驗,要注意的一個監控項,就是LRU數。通過這可以看出,是不是發生了內存不夠的情況了。如下圖:

 

 

  另外一個項,bytes,當前存儲占用了多少字節。這個項的值,我們會被誤導。

  比如顯示占據的存儲空間是3.7g。遠遠沒有達到最大分配的內存數4g。但是這個階段卻發生了lru剔除數據的現象。

  其實有些slab class占據着內存空間。這些內存空間並沒有機會被新加入的key->value來使用,於是導致了某些slab class無法繼續申請新的內存的現象。

  而lru只是針對當前訪問的slab class進行的。並不是針對全局(所有slab class)進行。

 

  通俗點理解如下:

 

  slab class 1

  slab class 2

  slab class1 中有大量空閑的內存空間,而新加入的key->value都是進入到slab class 2去。slab class2中沒有空閑的內存空間了。 memcache就跟操作系統申請內存,操作系統沒有足夠的內存給予memcache(發生LRU算法的大前提后續有文字解釋),這個時候 memcache迫不得已了,就會執行lru算法。

 

   研究這里的統計值是怎么算出來的,可以避免被誤導

 

   這個統計值的計算標准是怎么樣的?  什么時候會更新呢?

 

   1、添加一個item成功后,會增加統計值。

 

  每次添加一個key,就會讓這個統計項的值增加。可以這么理解:增加一個key->value成功后,就更新掉那個總數值。比如新增加的 key->value大小是220個字節,而恰好定位到slab class 3。假設里面的chunks大小是260個字節。

 實際上統計總數的時候,就會增加260個字節。雖然只占220個字節。但是chunks的機制,260-220=40個字節,也是空着的,不能被拿來使用(相當於內存碎片了),那么會按照260個字節來說。

2、delete命令和get命令的時候,會減小統計值。

     經我測驗:只有delete和get命令時才會將占據的內存統計值減小值。get命令,會判斷當前讀取的key是否過期,若過期了,則會把對應的內存空間標識為可用狀態,同時就會將統計總數值減小。

   測驗思路如下:故意添加一個失效期只有50秒的key,這個key的大小是298個字節。然后去memcache命令行使用stats命令查看內存占用的空間(或者memadmin界面工具也可以)。因為添加了一個值,於是發現統計總數增加了298個字節。

 50秒過后,再去運行stats命令,發現統計值並沒有變化。

 當我使用get命令查詢這個key時,memcache會檢查這個key已經過期,則會自動更新掉統計值。delete命令也是類似。

 

3、flush_all的用途是將所有的key都設置為過期,運行這個命令所占內存會不會清0呢?

 

     結論:運行flush_all命令后,統計值是不會有變化的。

    解釋:使用flush_all是不會訪問到所有key的。只是設置一個類似於這樣的標記:flush_all_time=記錄上一次失效的時間。

 

    思考:從上面做實驗來看,這個統計所占據的內存值,只能做一個大概的。並不不是很准確的。比如一個item占着內存空間260個字節,實際上這個item已 經過期了。由於一直沒有機會使用get命令讀取這個item,那么memcache的總數統計項中,並沒有減掉這個260個字節。所以看起來統計值是接近 4g了。我們會覺得,內存是不是不夠用了,其實不必擔憂,重點是看有沒有發生LRU計數項。這個是很重要的數據。

 

四、memcache的長連接實驗

 

memcache服務端提供了tcp協議接口來操作。所以paython,java,php都可以基於這個協議來與memcache通信。

php鏈接memcache服務端,使用的是memcached擴展(客戶端)。

客戶端鏈接memcache服務端,有長連接和短連接兩種方式。

 

 4.1、短連接與長連接的比較

 

    到底哪種方式效率高? 性能更好呢?

   筆者認為是長連接。既然設置了長連接,那么肯定有它用武之地。當遇到大量的並發請求的時候,長連接可以發揮出性能優勢。主要是基於:tcp連接數會更少。如果大量的客戶端連接memcache服務,在linux可以看到很多的tcp連接。

 

   我的測驗辦法是:使用ab命令去發起大量請求到一個php文件。而這個php文件就是去與memcache服務進行交互。馬上在linux使用命令netstat -n -p | grep 11211查看11211這個端口的tcp連接情況。

   使用短連接,看到效果如下:

   

   

 

客戶端頻繁地與memcache服務,實際上就是一個這樣的過程:建立連接>釋放連接。

遇到大量的請求,頻繁的建立>釋放tcp連接,是需要耗費linux文件句柄的。會使得linux服務器文件句柄達到極限。處理不過來。

另外導致的問題是,cpu的負載高。建立和釋放釋放資源(連接),實際上是比較耗費cpu資源的。大量重復建立和釋放連接,會讓cpu的負載變高。

如下命令可以統計指定端口的tcp連接總數:

 

當我使用ab命令並發請求php的時候,從上圖看到11211端口的tcp連接數一直在增加。

使用長連接的效果,同樣使用ab命令並發請求php,

長連接的測驗辦法:可以在linux通過命令,看到即便是請求結束后,還是能夠看到這些連接。

 

上圖的狀態為established。就是一直在保持連接狀態的tcp連接。

當使用長連接的時候,在linux服務端看到的連接總數,也比較少。

可以預先開多少個連接。

 4.2、長連接的優勢

 

  思考:什么情況下使用長連接,什么情況下使用短連接呢?

  高並發請求下,才能看到長連接帶來的明顯效益:tcp連接復用、資源消耗少(主要是cpu負載)。

  很多人表面看覺得,使用長連接(持久連接),會占着資源一直不釋放掉。消耗太多資源。從直覺上,更加喜歡建立>斷開連接的操作方式,明顯感覺會釋放資 源,所以會減少資源消耗。我們可能以為,來2000個請求,使用持久連接,就會建立2000個連接,即便請求完畢后,2000個連接也一直維護着。於是比 較耗費資源。

   持久連接,是一種復用技術。預先創建500個連接,放入連接池里面。當有請求來的時候,先去連接池里面看,是 否有空閑狀態的連接,有就拿過來使用。若沒有可用連接,則創建一個連接,用完這個連接后,是釋放掉,還是接着放入連接池呢?可以進行配置的。可以配置連接 池中保持多少個連接在等待請求。

並不是說,2萬個客戶端請求,那么就要一直維護着2萬個連接。實際上連接池里面維護的可能是1000個連接(可以自己配置),連接池中這1000個連接是與服務端(比如mysql、memcache)維持着通信狀態的。

  在計算機中有一個經驗:頻繁地創建資源和釋放資源(比如建立連接),帶來的開銷比維護這些資源都要高.維護只需要在內存中發送心跳包,需要的時候從內存中調 用出,由於已經在內存中了,不用去創建資源了,直接從內存中調出來的數據很快的。操作系統linux中著名的slab機制管理內存空間,就是基於這個原來 做的。slab管理機制,會將內存區域自己維護起來,減少頻繁地申請和釋放內存資源,內存其實沒有釋放,只是標識了一個狀態:可用、不可用。

需要的時候,直接拿過來使用(避免申請耗費cpu資源)。

  現在發現,學到一個思想:頻繁使用的資源,要緩存起來。目的是避免頻繁地去申請、釋放。設計一個連接池方案,是一種成熟的技術,不會那么弱智。

 

  平時大部分應用都用不上連接池

 

  平時我們使用短連接,是建立tcp連接,用完后釋放掉tcp連接。我們習以為常,實際上是因為大部分應用不會遇到連接數瓶頸(建立連接用完只要快速釋放,看起來速度很快),所以沒有使用連接池帶來的好處。

  這很像http請求場景:大量的http請求80端口。建立連接>傳輸完數據>斷開連接,也是一次短連接的過程。我們看不到問題。

  我們使用數據庫連接池技術(長連接就是維護着一個連接池,叫法不同),平時開發中並不需要使用,而是在高並發情況下,才會看到使用數據庫連接池帶來的明顯效果。

  連接池技術是針對高並發情況下進行的優化,在沒達到連接瓶頸的時候,用了跟沒用,看不出明顯速度區別的。

 

 

 五、思考與解惑

 

 思考1:配置memcahce的最大內存空間為2g。那么,memcache是不在是啟動的時候,就跟操作系統申請那么多的內存空間呢?

  不是。如果會一開始就跟操作系統申請這么多的內存。這樣的壞處明顯。比如memcache存儲的數據量一直都維持在1g的容量,而memache就跟操作申請2g內存,完全是浪費內存資源。memcache默認會創建n個slab cass。但實際上每個slab class也只跟操作系統申請了1m的內存。

 memcache占有多少內存,key->value的數據越來越多,跟操作系統申請的內存越來越多。

  思考2:當數據過期時,或者是說將數據刪除后,占據內存有沒有被釋放掉呢?

答案:並沒有。memcache只是對自己維護的內存區域標識了一個狀態"空閑"、"已被使用"。其實memcache已經從操作系統申請到的內存,比如占了1g了。並沒有返回給操作系統的。只有等到memcache進程終止掉了,操作系統會自動回收。

memcache其實是故意不返回的,借鑒了slab管理器的思想。內存沒有釋放給操作系統,而是自己維護着一個狀態。下一次其他數據需要存儲的時候,直接拿到這些空閑的內存存儲數據即可了。

思考3:memcache對數據的過期是如何檢測的?

 

  網上有資料,提到是懶惰刪除法。就是在訪問這個key(使用get命令讀取)的時候,才去判斷此key是否過期。若過期了,則將其占據了內存區域(chunk塊)標識為空閑狀態。

  之所以叫做懶惰法,筆者這樣理解:平時並不去主動掃描所有key的過期時間,需要用到這個key的時候,才去判斷過期。這是懶人做法。如果是勤奮的做法,一般開一個垃圾回收線程,定期去掃描key,發現過期了,就將此塊內存區域標為"空閑"狀態。但是這樣專門開一個線程去掃描,需要耗費cpu資源。

 

  懶惰刪除法的缺點是:如果這個key一直沒有去訪問,那么就永遠不知道有沒有過期(memcache不會標識其內存區域為空閑狀態)。那么這塊內存,沒法讓其他key加入進來使用。 造成了內存的浪費。

 redis吸取了這個教訓,做了一點改進:每次讀取key的時候,選定一個范圍內的數據掃描一次。

 

 思考4:LRU列表、空閑chunks列表、空閑chunks總數統計

  筆者在看那個LRU算法的時候,網上一些資料,看暈了。需要區分一下概念,源碼才能看得明白邏輯:

  添加數據的時候,定位到合適的slab class后(比如slab class3),會去LRU列表中,拿最末尾的一個item,若其時間已經過期,則直接返回內存空間使用。

  若找不到,則去slab class的空閑空間拿內存空間。

  得理清楚上面一些概念,不然搞不明白。從網上弄了一張比較好的圖,根據自己的理解,在圖中自己加了一點說明:

 

 

 

  

 

    LRU列表:   記錄着一個slab class中最近訪問的item,按照最近訪問時間進行排序。

        訪問這個列表,有兩種方式:一種是使用tails,這是從尾部開始訪問,得到的訪問時間離現在最遠的item;  另外一種是使用heads,從頭部開始訪問。得到的訪問時離現在最近的item。

        tails[id],id是一個整數,是slab class的編號。每個slab class都有一個tails隊列。使用tails是從尾部開始訪問,如果需要從頭部開始訪問這個列表,那么就使用heads[i]。

        注:尾部的數據,就是相對沒那么頻繁訪問的,於是memcache優先從尾部拿一個item來判斷是否過期。

   空閑item列表slots:源碼中完整引用是p->slots,p就表示某個指定的slab class。slots中記錄的是可以拿來使用的item(記錄的是哪些,是一個列表)。比如一個item被刪除,或者代碼檢測到過期時間已經過期,那么都會把item加到這個空閑列表中去。

   sl_curr:源碼中完整引用是p->sl_curr存儲是一個整數。統計當前有slab class有多少個空着的item。比如有5個,那么這個項的值就是5。memcache源碼中的解釋為:total free items in list

 

   上述項的值更新是怎么進行的? do_slabs_free()函數中會有如下代碼:

     p->slots = it;//slots是存儲要回收的空閑items,加到里面去 

     p->sl_curr++;//給當前slab的空閑item個數+1 。

 

  思考5:LRU算法是在什么時候才會執行?

 

      只有當申請不到內存的時候。才去做LRU算法。其實這是一種迫不得已的辦法:沒有內存可用了。為了保證數據能存儲進去,只能踢下一些不太使用的數據了

   注:有個配置-M可以配置memcache內存不夠的時候,禁止數據存儲進去,而不是執行LRU算法。不過一般不怎么用,因為數據存儲不進去,使用體驗不好。

       怎么樣才算申請不到內存空間呢? 兩種情況可能會出現:

 

      1、memcache去跟操作系統申請內存,一方面是操作系統沒有足夠的內存分配給memcache(總共4g內存,操作系統沒有足夠內存分配了)

       2、由於memcache啟動的時候配置了一個最大限制內存(啟動時的-m參數),目前memcache占據的內存,已經超過這個數了。

      示范:

       /usr/local/memcached/bin/memcached -d -m 1024  -u root -l 127.0.0.1 -p 11211

      -m指定了2014,單位是m。也就是最大1g內存。

 

更加細化到什么命令執行:在執行添加item操作的時候,才有可能去執行LRU算法。get操作不會去執行LRU算法。get命令記得回去判斷key的過期,然后標識為過期狀態,標識為過期狀態,就加到隊列中了。這樣它占據的內存就可以騰出了使用了。

 

 添加數據涉及到的LRU思路,看源碼,LRU算法是在items.c文件中的do_item_alloc()中執行。筆者通過閱讀源碼理清楚了如下步驟:

 

步驟1、do_item_alloc()是在新增加key的時候調用的。這個函數的作用是:傳入一個key->value,然后函數會計算key->value所需要的大小,比如所需空間是280個字節。那么就會去slab calss列表里面尋找一個能夠存儲下的slab class。找到一個slab class后,就會進入到slab class里面去搜索了:從LRU列表(LRU列表前面有解釋)的尾部,彈出末尾的一個item。判斷它的過期時間,如果這個item已經過期,正好可以拿其內存空間來使用。

 

步驟2、如果第一步拿到的item沒有過期,然后才考慮,從當前slab class里面獲取空閑的chunks塊。

 

步驟3、如果當前slab class里面沒有空閑的chunks,則會申請一個slab page(1m大小)放入到當前slab class里面去 (封裝在do_slabs_alloc()中實現)。

 

步驟4、如果還不成功,那么就執行LRU算法了:將LRU列表中,從尾部踢下一個item。LRU列表是按照最近訪問時間來排序的,從尾部踢的一個item,相對來說是最不活躍的item了。

 

注:每次剔除一個,都會讓計數器(監控中的evitions項)加1的。於是就有我們在監控上去看evitions項的值。

 上述步驟,每進行一次若沒有拿不到item空間,那么會會重復進行5次(封裝在一個for循環里面,循環5次)。

 

   函數調用關系依次為:

 

  do_store_item()>>do_item_alloc()>>>slabs_alloc()>>do_slabs_alloc()>>do_slabs_newslab()

  do_store_item()memcache.c文件

  do_item_alloc()items.c文件

  slabs_alloc()slab.c文件

  do_slabs_alloc()在slab.c文件

  do_slabs_newslab()在slab.c文件

 

  這部分的源碼閱讀

 

  看memcache源碼,在文件items.c文件中,里面的注釋是我根據自己理解加的。

 

/*
+-----------------------------------------------------------------------
整個函數的目的是:尋找合適的slab存儲一個item,最終返回item空間的的引用
+----------------------------------------------------------------------- 
使用場景:保存一個key->value操作的時候。 
+--------------------------------------------------------------------
給定一個key,計算key需要的空間。 把大小傳入slabs_clsid()函數,然后返回slab的編號。如果找不到適合大小的slab,則整個返回0 
會先計算key->value所需空間,然后尋找合適的slab class
+-----------------------------------------------------------------------
*/
//源碼資料參考:http://blog.csdn.net/caiyunl/article/details/7878107 
item *do_item_alloc(char *key, const size_t nkey, const int flags,
                    const rel_time_t exptime, const int nbytes,
                    const uint32_t cur_hv) {
    uint8_t nsuffix;
    item *it = NULL;
    char suffix[40];
    
    
    //給suffix賦值,並返回item總的長度(除去cas的)。總長度用於決定該item屬於哪個slabclass
    //疑問:value的長度呢? nbytes參數 
    size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
    
    
    /*
    
    從宏ITEM_ntotal可以看出一個item 的實際長度為 
    sizeof(item) + nkey + 1 + nsuffix + nbytes  ( + sizoef(uint64_t), 如果使用了cas)
    */

    if (settings.use_cas) {
        ntotal += sizeof(uint64_t);
    }

    //傳入大小,返回一個適合此大小存儲的slab編號 
    unsigned int id = slabs_clsid(ntotal);
    
    
    if (id == 0)
        return 0;//返回0,則表示從目前的所有slab列表里面找不到適合存儲的slab class(因為item太大了)
        

     //外面調用這個函數判斷,只是設置。如果沒有找到合適的slab,為什么沒有去創建一個slab呢? 
    //do_slabs_newslab 
    /*
     每個 slabclass 都擁有一些 slab, 當所有 slab 都用完時, memcached 會給它分配一個新的 slab, 
     do_slabs_newslab 就是做這個工作的.
    */
    
    mutex_lock(&cache_lock);
    
    //優先去slab的隊列尾部尋找過期的item空間,意思是想優先使用這些空間來替換
     
    /* do a quick check if we have any expired items in the tail.. */
    int tries = 5;//嘗試多少次 
    int tried_alloc = 0;/*記錄是否申請內存失敗,根據這個值判斷*/
    item *search;//這里只是初始化,值在后面賦值 
    void *hold_lock = NULL;
    rel_time_t oldest_live = settings.oldest_live;//使用flush_all命令相關的設置
     
    /*從尾部開始搜索,因為尾部的time總是最早的,所以就是一種LRU實現 */ 
    
    search = tails[id];//每個slab有個自己的tails數組,id就是slab的編號
    /*
    tail這個數組就是維護的是一個slab按照訪問時間排序的item,應該只保留部分數據。到時候執行lru算法也踢掉這里面的item。也就是time時間最小的算是距離現在時間最遠的,就會被踢掉。 
    */
    /* We walk up *only* for locked items. Never searching for expired.
     * Waste of CPU for almost all deployments */
     
     
     /*
     循環5次:從最近訪問的item隊列(tails[id]),尾部開始遍歷5個item看看。 
     這里就是一種lru算法。最近最少使用,其實就是根據訪問時間來排序成一個隊列。 
     */ 
     
    for (; tries > 0 && search != NULL; tries--, search=search->prev) {
        
        uint32_t hv = hash(ITEM_key(search), search->nkey, 0);
        /* Attempt to hash item lock the "search" item. If locked, no
         * other callers can incr the refcount
         */
        /* FIXME: I think we need to mask the hv here for comparison? */
        if (hv != cur_hv && (hold_lock = item_trylock(hv)) == NULL)
            continue;
            
        /* Now see if the item is refcount locked */
        if (refcount_incr(&search->refcount) != 2) {
            refcount_decr(&search->refcount);
            /* Old rare bug could cause a refcount leak. We haven't seen
             * it in years, but we leave this code in to prevent failures
             * just in case */
            if (search->time + TAIL_REPAIR_TIME < current_time) {
                
                itemstats[id].tailrepairs++;
                search->refcount = 1;
                do_item_unlink_nolock(search, hv);
            }
            if (hold_lock)
                item_trylock_unlock(hold_lock);
            continue;
        }
        /* Expired or flushed */
         /* 先檢查 LRU 隊列中最后一個 item 是否過期, 過期的話就把這個 item空間拿來使用 */
        if ((search->exptime != 0 && search->exptime < current_time)
        
            || (search->time <= oldest_live && oldest_live <= current_time)) {
                
               /*優先選擇最近最少訪問列表中已經過期的一個item來使用*/ 
            itemstats[id].reclaimed++;//替換次數加1,顯示在stats命令中的 reclaimed項 
            if ((search->it_flags & ITEM_FETCHED) == 0) {
                itemstats[id].expired_unfetched++;
            }
            it = search;//把這個空間返回使用,實際上就是替換掉已經過期的item
            slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);//雖然屬於同一個slabclass,但是長度仍可能不一樣,需要修改一下  
            do_item_unlink_nolock(it, hv); //將過期的item從雙向鏈表和hash表中除去
            /* Initialize the item block: */
            it->slabs_clsid = 0;
            
        } else if ((it = slabs_alloc(ntotal, id)) == NULL) { 
        
        /*
        1、找不到已經過期的chunks空間(),則slabs_alloc()函數是從當前選擇的slabclass獲取空閑的item空間  
        2、slabs_alloc()如果找不空閑的chunk空間,會去跟操作系統申請一個page加到當前slab class里面 
        3、如何這樣申請內存空間失敗的話:就把 LRU 隊列最后一個 item 剔除, 然后分配出來使用(返回) 
      */
            
            tried_alloc = 1;/*記錄是否申請內存失敗,1標識為失敗*/
             
            if (settings.evict_to_free == 0) { //==0表示關掉了LRU踢下線算法,直接不允許存入數據進memcache 
                itemstats[id].outofmemory++;
            } else {
                /*
                 這部分代碼就是lru的踢下操作了:執行到這里的時候,已經是迫不得已了:
                 在當前slab class過期的空間沒找到、空閑的空間也沒有、跟操作系統申請內存也失敗
                 最下策的辦法:甭管了,從LRU列表中踢一下一個來使用吧。 
                */ 
                itemstats[id].evicted++;//當前的slab被踢下去的總數加1 
                itemstats[id].evicted_time = current_time - search->time;//最近發生踢下去操作的時間 
                
                if (search->exptime != 0)
                    itemstats[id].evicted_nonzero++;
                if ((search->it_flags & ITEM_FETCHED) == 0) {
                    itemstats[id].evicted_unfetched++;//被剔除的數據中,統計這種數據:從來沒有被獲取過一次的 
                }
                it = search;
                slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);
                /* 把這個 item 從 LRU 隊列和哈希表中移除 */ 
                do_item_unlink_nolock(it, hv);
                /* Initialize the item block: */
                it->slabs_clsid = 0;

                /* If we've just evicted an item, and the automover is set to
                 * angry bird mode, attempt to rip memory into this slab class.
                 * TODO: Move valid object detection into a function, and on a
                 * "successful" memory pull, look behind and see if the next alloc
                 * would be an eviction. Then kick off the slab mover before the
                 * eviction happens.
                 */
                 /*如果開啟了自動reassign機制,則每發生內存不夠擠下去情況,就執行一次優化,是不是異步的,還是同步進行,如果是同步是不要卡在這里等操作完畢呢? 添加數據會受到影響*/
                if (settings.slab_automove == 2) 
                    slabs_reassign(-1, id);
            }
            
        }// end if

        refcount_decr(&search->refcount);
        /* If hash values were equal, we don't grab a second lock */
        if (hold_lock)
            item_trylock_unlock(hold_lock);
        break;
        
    } //end for 循環


    /*
    tried_alloc為1,表示申請分配內存失敗。tries表示嘗試的次數,總共是5次嘗試 
    這里再嘗試一次(看空閑chunks、跟操作系統申請內存),增加成功幾率 
    */
    if (!tried_alloc && (tries == 0 || search == NULL)){
         it = slabs_alloc(ntotal, id);
    }

    if (it == NULL) {
        itemstats[id].outofmemory++;//內存不夠? 
        mutex_unlock(&cache_lock);
        return NULL;
    }

    assert(it->slabs_clsid == 0);//為假,則終止代碼執行 
    assert(it != heads[id]);

    /* Item initialization can happen outside of the lock; the item's already
     * been removed from the slab LRU.
     */
  
     //初始化一些item屬性,可以看出這里只是申請了data所需要的空間,而未給data真正的賦值,並且將其連入到LRU和hash表的操作也不在這
    it->refcount = 1;     /* the caller will have a reference */
    mutex_unlock(&cache_lock);
    it->next = it->prev = it->h_next = 0;
    it->slabs_clsid = id;//設置所屬的slab編號,傳入的item,尋找到了合適的slab編號 

    DEBUG_REFCNT(it, '*');
    it->it_flags = settings.use_cas ? ITEM_CAS : 0;
    it->nkey = nkey;
    it->nbytes = nbytes;
    memcpy(ITEM_key(it), key, nkey);//由第二個參數指定的內存區域復制第三個參數長度的字節到第一個參數指定的內存位置去 
    it->exptime = exptime;//設置item的過期時間 
    memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
    it->nsuffix = nsuffix;
    return it;

    
}
View Code

 

思考6:定位slab時,使用最小slab class的原因

 

memcache源碼,它在尋找slab的時候,有個原則:優先尋找最小的slab進行使用。比如尋找到4slab是可以存儲的,那么會使用最小的那個slab

源碼如下(在slabs.c中):

/*
根據傳入item的大小,根據大小查找合適的slab,最終返回slab的編號
*/
unsigned int slabs_clsid(const size_t size) {

    int res = POWER_SMALLEST;//最小slab個數,默認是1,定義在memcached.h頭文件中 

    if (size == 0)

        return 0;

    while (size > slabclass[res].size){  //slabclass[res].size每次循環往后面移, 遍歷所有的slab,直到找到比它空間大的slab 

        if (res++ == power_largest)     /* won't fit in the biggest slab */ //永遠不要使用最后一個slab 

            return 0;

    } 

     /*返回0,則表示找不到適合目前item存儲的slab。    res++ == power_largest表示已經是最最后的那個slab class了,那么表明已經找不到適合存儲目前item的slab class。*/   
    return res;

}
View Code

 

之所以永遠使用是最小的slab,從第一個slab遍歷到最后一個slab,始終找到最小的slab來存儲,是基於這個特點:每新創建一個slab,   chunks的大小都會按照步長來增加

比如第一個slab里面的每個chunks大小是96字節,第二個會是120,第三個152字節,第四個192字節。

 

思考7:啟動memcache進程的時候,可以指定默認創建多少個slab class呢?

 

  slabs.c中slabs_init()函數是初始化slab。看代碼是初始化200個。但是實際做試驗,發現並沒有初始化這么多。

  遞增趨勢。最大一個的slabclass里面的chunk不會大於一個Page的大小(默認1M)。

  思考:實際上slab_init()就已經初始化200個slab class了。

  只是還沒有跟操作系統去申請內存給每個slab class。啟動的時候,可以打開預先分配機制。這樣啟動memcache進程,就會為200個slab class跟操作系統申請內存。

  既然已經200個slab class。那么也完全夠用了。可以設置剛開始的chunks大小增長的因子默認是1.25。可以修改的。

  第一個slab class中的chunk大小是48個字節,然后按照增長因子遞增(1.25)。48*1.25*1.25*1.25

  199次方。

 

思考8:如何設置第一個chunks的大小以及增長因子

 

  啟動時候-n參數指定chunks的字節數,-f參數指定增長因子

 /usr/local/memcached/bin/memcached -d -m 512 -n 240 -l 127.0.0.1 -p 11211 -u root -f 1.48 -vv

 --I 指定跟操作系統申請的page大小。

比較重要的幾個啟動參數:

-f:增長因子,chunk的值會按照增長因子的比例增長(chunk size growth factor).

-n:每個chunk的初始大小(minimum space allocated for key+value+flags),chunk大小還包括本身結構體大小.

-I:每個slab page大小(Override the size of each slab page. Adjusts max item size)

-m:現在memcache的數據最大占據內存(max memory to use for items in megabytes)

 

思考:理論上是使用 -n 240指定了第一個slab class的中的chunks大小是240個字節。但是啟動后,實際上卻是288個字節呢? 截圖如下:

 

 思考9:定位不到合適的slab class咋辦

 

    假設現在存儲的item大小是480個字節。由於slab class列表里面最大的chunks也只有260個字節。那么就沒有合適的slab class供存儲。此時算是定位不到合適的slab class了,咋辦?此時memcache,就要新創建slab class嗎?

 

   據目前理解是,按照最大的slab class里面的chunks來決定,里面的chunks假設大小是260個字節。如果按照這樣,那么就會是260*1.25=325 。        

   1.25這是增長因子。如果這樣子得到是325個字節。如果新創建的slab還是按照325個字節來分chunks。那么還是不夠存儲當前的480個字節的。所以,我猜測,應該是有其他思路的。

 

原因是?待解答!

 

 思考10:重新對一個key->value值保存,所需的內存空間增大,會如何辦?

 

假設場景:原來一個key->value計算出大小是需要容量280個字節。恰好存入到slab class3里面。slab class3 里面的所有chunk大小是300字節。但是現在要針對原來的key值,重新保存數據。value的值增大了,於是重新計算key->value需要330個字節。那存儲到slab class 3里面是存不下了。那會怎么操作呢?難道將slab class3里面的key移動到新的slab class里面去嗎?

 

  待解答。等待看源碼理清楚。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM