memcache做爲緩存服務器,用來提升性能,大部分互聯網公司都在使用。
php
前言java
文章的閱讀的對象是中高級開發人員、系統架構師。
node
本篇文章,不是側重對memcache的基礎知識的總結,好比set,get之類的命令如何使用不會介紹。是考慮到,此類基礎知識網絡已經有一大把資料,因此更加傾向於深刻性的知識點。文章側重的重點是對memcache的原理理清楚、在實戰中本身所遇到的坑、本身的思考心得與理解。mysql
好記性不如爛筆頭,整理文章的初衷是爲了加深本身的理解,對知識進行梳理,人的大腦會逐步遺忘,記下來的文字,方便之後查閱。linux
本文太長,一時看得暈,請挑你你須要的部分看。或者收藏起來,之後有更新部分,能夠繼續訪問。筆者之後也會將一些疑惑的知識點,繼續完善到文章中去。nginx
文章爲原創,轉摘歡迎你註明出處,謝謝!redis
1、memcache的原理算法
關係型數據庫的數據是放入在硬盤上的。磁盤的瓶頸是i/0(機械設備,靠磁盤片旋轉來定位數據)。sql
而memchace利用內存的速度快,把數據存入到內存中。內存這個設備有個特色,斷電後,內存裏面的全部數據就會丟失。數據庫
基於這個特色,咱們在架構的系統中使用memcache時,放入memcache中的數據,最終仍是要在磁盤上有備份,否則丟失掉了。沒地方去找了。
memcache本質就是在管理着一大片的內存區域。咱們的程序去跟memcache提供的接口存儲和獲取數據。注意,這個內存,memcache是跟操做系統去申請內存的。
2、memcache管理內存的機制
先了解memcache的數據類型,方便理解後續知識。
memcache只提供了一種數據類型:key->value。key是字符串,value也必須是字符串。
2.一、額外知識點:操做系統與內存的關係
理解一個大前提很是重要:內存是操做系統在管理。
操做系統是責與全部硬件交換(硬盤,磁盤,內存、外設打印機等)。咱們電腦插槽那個內存,也是操做系統在統一管理。
因而,在操做系統運行下的全部軟件(mysql,memcache,nginx等等),須要內存的時候,都是要去跟操做系統申請內存。
軟件向操做系統申請內存的辦法
每次跟操做系統申請內存的最小單位是頁(page)。就像稱重規定最小單位是克,人本身這麼約定的。
關鍵詞:一次申請的最小單位是頁(page)。
注:並非說一次只能申請一頁(4kb)。是指只能按照4k*n爲單位進行申請內存。
操做系統是這樣管理內存的:
把內存劃分紅等份大小的份(一塊一塊的)。這種在操做系統概念中叫作分頁法。
有分頁又有分段,概念容易弄暈了。其實,分段技術早於分頁技術。過去是操做系統是使用分段技術來管理內存(程序運行在哪一個內存區間,這就是段)
可是分段法存在一些缺陷,因此後來操做系統使用分頁法了。
2.二、核心機制:slab機制介紹
memcache借鑑了linux操做系統中的slab管理器的模式,使用slab方式來管理從操做系統申請到的內存。
2.2.一、借鑑了linux的slab管理器
slab分配器主要的功能就是對頻繁分配和釋放的小對象提供高效的內存管理。它的核心思想是實現一個緩存池,分配對象的時候從緩存池中取,釋放對象的時候再放入緩存池。
memcache也使用這樣的辦法:模擬實現了一個slab分配器,把從操做系統申請到內存緩存起來,即使是刪除數據了,這部份內存也不會返回給操做系統,只是標識一個狀態"空閒"。目的是方便下回使用。slab管理器的核心思想其實就是:對頻繁使用的小對象進行緩存起來,不要釋放掉。最終避免頻繁的申請、釋放操做形成性能問題(耗資源)。
實際上早期的memcache版本並無採用slab管理器的思路,後來的版本才改善,使用slab機制。
memcache在使用slab機制出現之前,內存的分配是經過對全部記錄簡單地進行malloc和free來進行的(對操做系統申請內存和釋放內存)。 可是,這種方式會致使內存碎片,加劇操做系統內存管理器的負擔,最壞的狀況下, 會致使操做系統比memcached進程自己還慢。Slab Allocator就是爲解決該問題而誕生的。
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=90112個chunks。
如上圖:slab class 1裏面都是88字節的chunks。slab 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 class的chunks大小*增加因子。
增加因子默認是1.25。好比上一個是288,那麼288*1.25=360。下一個slab class中的chunks大小是:360*1.25=456。
注:啓動memcache的時候,第一個slab class裏面的chunks都是48個字節。這個值是能夠配置的。
2.2.三、往memcache添加key的內部機制
添加一個key->value的步驟以下:
1、先定位到合適的slab
判斷邏輯是這樣:新加入一個key->value(也叫item),先計算這個key->value的總體大小。假設是118個字節。
那麼去slab列表裏面,尋找哪一個slab可以存儲下。最終找到是144字節的chunks組(slab3)可以存儲。
思考:爲何是slab class 3,而不是slab class 4、slab class 5呢?
memcache的計算辦法是,優先選擇最小的slab class。源碼在slabs.c中的slabs_clsid()函數中。這個函數傳入一個容量進去,返回可以存儲其容量的slab class編號。
2、定位到合適的slab後
到這一步,如今找到slab3是能夠存放。因而進入到slab3裏面去。那slab3裏面有沒有空閒空間來存儲呢?
因此得先看看slab3裏面是否是有空閒的chunks。memcache爲每一個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)。
這718317個chunks,就會加到slab class 3裏面去。
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算法。
研究這裏的統計值是怎麼算出來的,能夠避免被誤導
這個統計值的計算標準是怎麼樣的? 何時會更新呢?
一、添加一個item成功後,會增長統計值。
每次添加一個key,就會讓這個統計項的值增長。能夠這麼理解:增長一個key->value成功後,就更新掉那個總數值。好比新增長的 key->value大小是220個字節,而剛好定位到slab class 3。假設裏面的chunks大小是260個字節。
實際上統計總數的時候,就會增長260個字節。雖然只佔220個字節。可是chunks的機制,260-220=40個字節,也是空着的,不能被拿來使用(至關於內存碎片了),那麼會按照260個字節來講。
二、delete命令和get命令的時候,會減少統計值。
經我測驗:只有delete和get命令時纔會將佔據的內存統計值減少值。get命令,會判斷當前讀取的key是否過時,若過時了,則會把對應的內存空間標識爲可用狀態,同時就會將統計總數值減少。
測驗思路以下:故意添加一個失效期只有50秒的key,這個key的大小是298個字節。而後去memcache命令行使用stats命令查看內存佔用的空間(或者memadmin界面工具也能夠)。由於添加了一個值,因而發現統計總數增長了298個字節。
50秒事後,再去運行stats命令,發現統計值並無變化。
當我使用get命令查詢這個key時,memcache會檢查這個key已通過期,則會自動更新掉統計值。delete命令也是相似。
三、flush_all的用途是將全部的key都設置爲過時,運行這個命令所佔內存會不會清0呢?
結論:運行flush_all命令後,統計值是不會有變化的。
解釋:使用flush_all是不會訪問到全部key的。只是設置一個相似於這樣的標記:flush_all_time=記錄上一次失效的時間。
思考:從上面作實驗來看,這個統計所佔據的內存值,只能作一個大概的。並不不是很準確的。好比一個item佔着內存空間260個字節,實際上這個item已 通過期了。因爲一直沒有機會使用get命令讀取這個item,那麼memcache的總數統計項中,並無減掉這個260個字節。因此看起來統計值是接近 4g了。咱們會以爲,內存是否是不夠用了,其實沒必要擔心,重點是看有沒有發生LRU計數項。這個是很重要的數據。
4、memcache的長鏈接實驗
memcache服務端提供了tcp協議接口來操做。因此paython,java,php均可以基於這個協議來與memcache通訊。
php連接memcache服務端,使用的是memcached擴展(客戶端)。
客戶端連接memcache服務端,有長鏈接和短鏈接兩種方式。
4.一、短鏈接與長鏈接的比較
到底哪一種方式效率高? 性能更好呢?
筆者認爲是長鏈接。既然設置了長鏈接,那麼確定有它用武之地。當遇到大量的併發請求的時候,長鏈接能夠發揮出性能優點。主要是基於: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.二、長鏈接的優點
思考:什麼狀況下使用長鏈接,什麼狀況下使用短鏈接呢?
高併發請求下,才能看到長鏈接帶來的明顯效益:tcp鏈接複用、資源消耗少(主要是cpu負載)。
不少人表面看以爲,使用長鏈接(持久鏈接),會佔着資源一直不釋放掉。消耗太多資源。從直覺上,更加喜歡創建>斷開鏈接的操做方式,明顯感受會釋放資 源,因此會減小資源消耗。咱們可能覺得,來2000個請求,使用持久鏈接,就會創建2000個鏈接,即使請求完畢後,2000個鏈接也一直維護着。因而比 較耗費資源。
持久鏈接,是一種複用技術。預先建立500個鏈接,放入鏈接池裏面。當有請求來的時候,先去鏈接池裏面看,是 否有空閒狀態的鏈接,有就拿過來使用。若沒有可用鏈接,則建立一個鏈接,用完這個鏈接後,是釋放掉,仍是接着放入鏈接池呢?能夠進行配置的。能夠配置鏈接 池中保持多少個鏈接在等待請求。
並非說,2萬個客戶端請求,那麼就要一直維護着2萬個鏈接。實際上鍊接池裏面維護的多是1000個鏈接(能夠本身配置),鏈接池中這1000個鏈接是與服務端(好比mysql、memcache)維持着通訊狀態的。
在計算機中有一個經驗:頻繁地建立資源和釋放資源(好比創建鏈接),帶來的開銷比維護這些資源都要高.維護只須要在內存中發送心跳包,須要的時候從內存中調 用出,因爲已經在內存中了,不用去建立資源了,直接從內存中調出來的數據很快的。操做系統linux中著名的slab機制管理內存空間,就是基於這個原來 作的。slab管理機制,會將內存區域本身維護起來,減小頻繁地申請和釋放內存資源,內存其實沒有釋放,只是標識了一個狀態:可用、不可用。
須要的時候,直接拿過來使用(避免申請耗費cpu資源)。
如今發現,學到一個思想:頻繁使用的資源,要緩存起來。目的是避免頻繁地去申請、釋放。設計一個鏈接池方案,是一種成熟的技術,不會那麼弱智。
平時大部分應用都用不上鍊接池
平時咱們使用短鏈接,是創建tcp鏈接,用完後釋放掉tcp鏈接。咱們習覺得常,其實是由於大部分應用不會遇到鏈接數瓶頸(創建鏈接用完只要快速釋放,看起來速度很快),因此沒有使用鏈接池帶來的好處。
這很像http請求場景:大量的http請求80端口。創建鏈接>傳輸完數據>斷開鏈接,也是一次短鏈接的過程。咱們看不到問題。
咱們使用數據庫鏈接池技術(長鏈接就是維護着一個鏈接池,叫法不一樣),平時開發中並不須要使用,而是在高併發狀況下,纔會看到使用數據庫鏈接池帶來的明顯效果。
鏈接池技術是針對高併發狀況下進行的優化,在沒達到鏈接瓶頸的時候,用了跟沒用,看不出明顯速度區別的。
5、思考與解惑
思考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算法。不過通常不怎麼用,由於數據存儲不進去,使用體驗很差。
怎麼樣纔算申請不到內存空間呢? 兩種狀況可能會出現:
一、memcache去跟操做系統申請內存,一方面是操做系統沒有足夠的內存分配給memcache(總共4g內存,操做系統沒有足夠內存分配了)。
二、因爲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()中執行。筆者經過閱讀源碼理清楚了以下步驟:
步驟一、do_item_alloc()是在新增長key的時候調用的。這個函數的做用是:傳入一個key->value,而後函數會計算key->value所須要的大小,好比所需空間是280個字節。那麼就會去slab calss列表裏面尋找一個可以存儲下的slab class。找到一個slab class後,就會進入到slab class裏面去搜索了:從LRU列表(LRU列表前面有解釋)的尾部,彈出末尾的一個item。判斷它的過時時間,若是這個item已通過期,正好能夠拿其內存空間來使用。
步驟二、若是第一步拿到的item沒有過時,而後才考慮,從當前slab class裏面獲取空閒的chunks塊。
步驟三、若是當前slab class裏面沒有空閒的chunks,則會申請一個slab page(1m大小)放入到當前slab class裏面去 (封裝在do_slabs_alloc()中實現)。
步驟四、若是還不成功,那麼就執行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) { /* 一、找不到已通過期的chunks空間(),則slabs_alloc()函數是從當前選擇的slabclass獲取空閒的item空間 二、slabs_alloc()若是找不空閒的chunk空間,會去跟操做系統申請一個page加到當前slab class裏面 三、如何這樣申請內存空間失敗的話:就把 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; }
思考6:定位slab時,使用最小slab class的緣由
看memcache源碼,它在尋找slab的時候,有個原則:優先尋找最小的slab進行使用。好比尋找到4個slab是能夠存儲的,那麼會使用最小的那個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; }
之因此永遠使用是最小的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裏面去嗎?
待解答。等待看源碼理清楚。