搭建memcached服務器
Memcached簡介
在介紹Memcached之前,讓我們首先通過一(yī)個示例了解什麽是服務端緩存。
相信大家都玩過一(yī)些網絡聯機遊戲吧(ba)。在我那個年(nián)代(03年(nián)左右),這些遊戲常常添加了對戰功能,并提供了天梯來顯示具有最優秀戰績的(de)玩家以及當前玩家在天梯系統中的(de)排名。這是遊戲開發商所常常采用的(de)一(yī)種聚攏玩家人氣的(de)手段。而希望在遊戲中證明自(zì)己的(de)玩家則會由此激發鬥志,進而花費更多時間來在天梯中取得更好的(de)成績。
就天梯系統來說,其最主要的(de)功能就是為(wèi)玩家提供天梯排名的(de)信息,而并不允許玩家對該系統中所記錄的(de)數據作任何修改。這樣設定的(de)結果就是,整個天梯系統的(de)讀操作居多,而寫操作很少。反過來,由于一(yī)個遊戲中的(de)玩家可(kě)能有上千萬甚至上億人,而且在線人數常常達到上萬人,因此對天梯的(de)訪問也會是非常頻繁的(de)。這樣的(de)話,即使每秒鍾隻有10個人訪問天梯中的(de)排名,對這上億個玩家的(de)天梯排名進行讀取及排序也是一(yī)件非常消耗性能的(de)事情。
一(yī)個自(zì)然而然的(de)想法就是:在對天梯排名進行一(yī)次計算後,我們在服務端将該天梯排名緩存起來,并在其它玩家訪問的(de)時候直接返回該緩存中所記錄的(de)結果。而在一(yī)定時間段之後,如(rú)一(yī)個小時,我們再對緩存中的(de)數據進行更新。這樣我們就不再需要每個小時執行成千上萬次的(de)天梯排名計算了。
而這就是服務端緩存所提供的(de)最重要功能。其既可(kě)以提高(gāo)單個請求的(de)響應速度,又可(kě)以降低(dī)服務層及數據庫層的(de)壓力。除此之外,多個服務實例都可(kě)以讀取該服務端緩存所緩存的(de)信息,因此我們也不再需要擔心這些數據在各個服務實例中都保存了一(yī)份進而需要彼此同步的(de)問題,也即是提高(gāo)了擴展性。
而Memcached就是一(yī)個使用了BSD許可(kě)的(de)服務端緩存實現。但是與其它服務端緩存實現不同的(de)是,其主要由兩部分組成:獨立運行的(de)Memcached服務實例,以及用于訪問這些服務實例的(de)客戶端。因此相較于普通服務端緩存實現中各個緩存都運行在服務實例之上的(de)情況,Memcached服務實例則是在服務實例之外獨立運行的(de):
從上圖中可(kě)以看出,由于Memcached緩存實例是獨立于各個應用服務器實例運行的(de),因此應用服務實例可(kě)以訪問任意的(de)緩存實例。而傳統的(de)緩存則與特定的(de)應用實例綁定,因此每個應用實例将隻能訪問特定的(de)緩存。這種綁定一(yī)方面會導緻整個應用所能夠訪問的(de)緩存容量變得很小,另一(yī)方面也可(kě)能導緻不同的(de)緩存實例中存在着冗餘的(de)數據,從而降低(dī)了緩存系統的(de)整體效率。
在運行時,Memcached服務實例隻需要消耗非常少的(de)CPU資源,卻需要使用大量的(de)內(nèi)存。因此在決定如(rú)何組織您的(de)服務端緩存結構之前,您首先需要搞清當前服務中各個服務實例的(de)負載情況。如(rú)果一(yī)個服務器的(de)CPU使用率非常高(gāo),卻存在着非常多的(de)空餘內(nèi)存,那麽我們就完全可(kě)以在其上運行一(yī)個Memcached實例。而如(rú)果當前服務中的(de)所有服務實例都沒有過多的(de)空餘內(nèi)存,那麽我們就需要使用一(yī)系列獨立的(de)服務實例來搭建服務端緩存。一(yī)個大型服務常常擁有上百個Memcached實例。而在這上百個Memcached實例中所存儲的(de)數據則不盡相同。由于這種數據的(de)異構性,我們需要在訪問由Memcached所記錄的(de)信息之前決定在該服務端緩存系統中到底由哪個Memcached實例記錄了我們所想要訪問的(de)數據:
如(rú)上圖所示,用戶需要通過一(yī)個Memcached客戶端來完成對緩存服務所記錄信息的(de)訪問。該客戶端知道(dào)服務端緩存系統中所包含的(de)所有Memcached服務實例。在需要訪問具有特定鍵值的(de)數據時,該客戶端內(nèi)部會根據所需要讀取的(de)數據的(de)鍵值,如(rú)“foo”,以及當前Memcached緩存服務的(de)配置來計算相應的(de)哈希值,以決定到底是哪個Memcached實例記錄了用戶所需要訪問的(de)信息。在決定記錄了所需要信息的(de)Memcached實例之後,Memcached客戶端将從配置中讀取該Memcached服務實例所在地(dì)址,并向該Memcached實例發送數據訪問請求,以從該Memcached實例中讀取具有鍵值“foo”的(de)信息。在各個論壇的(de)讨論中,這被稱為(wèi)是Memcached的(de)兩階段哈希(Two-stage hash)。
而對數據的(de)記錄也使用了類似的(de)流程:假設用戶希望通過服務端緩存記錄數據“bar”,并為(wèi)其指定鍵值“foo”。那麽Memcached客戶端将首先對用戶所賦予的(de)鍵值“foo”及當前服務端緩存所記錄的(de)可(kě)用服務實例個數執行哈希計算,并根據哈希計算結果來決定存儲該數據的(de)Memcached服務實例。接下來,客戶端就會向該實例發送請求,以在其中記錄具有鍵值“foo”的(de)數據“bar”。
這樣做(zuò)的(de)好處則在于,每個Memcached服務實例都是獨立的(de),而彼此之間并沒有任何交互。在這種情況下,我們可(kě)以省略很多複雜的(de)功能邏輯,如(rú)各個節點之間的(de)數據同步以及結點之間消息的(de)廣播等等。這種輕量級的(de)架構可(kě)以簡化很多操作。如(rú)在一(yī)個節點失效的(de)時候,我們僅僅需要使用一(yī)個新的(de)Memcached節點替代老節點即可(kě)。而在對緩存進行擴容的(de)時候,我們也隻需要添加額外的(de)服務并修改客戶端配置。
這些記錄在服務端緩存中的(de)數據是全局可(kě)見的(de)。也就是說,一(yī)旦在Memcached服務端緩存中成功添加了一(yī)條新的(de)記錄,那麽其它使用該緩存服務的(de)應用實例将同樣可(kě)以訪問該記錄:
在Memcached中,每條記錄都由四部分組成:記錄的(de)鍵,有效期,一(yī)系列可(kě)選的(de)标記以及表示記錄內(nèi)容的(de)數據。由于記錄內(nèi)容的(de)數據中并不包含任何數據結構,因此我們在Memcached中所記錄的(de)數據需要是經過序列化之後的(de)表示。
內(nèi)存管理(lǐ)
在使用緩存時,我們不得不考慮的(de)一(yī)個問題就是如(rú)何對這些緩存數據的(de)生存期進行管理(lǐ)。這其中包括如(rú)何使一(yī)個記錄在緩存中的(de)數據過期,如(rú)何在緩存空間不夠時執行數據的(de)替換等。因此在本節中,我們将對Memcached的(de)內(nèi)存管理(lǐ)機制進行介紹。
首先我們來看一(yī)看Memcached的(de)內(nèi)存管理(lǐ)模型。通常情況下,一(yī)個內(nèi)存管理(lǐ)算法所最需要考慮的(de)問題就是內(nèi)存的(de)碎片化(Fragmentation):在長(cháng)時間地(dì)分配及回收之後,被系統所使用的(de)內(nèi)存将趨向于散落在不連續的(de)空間中。這使得系統很難找到連續內(nèi)存空間,一(yī)方面增大了內(nèi)存分配失敗的(de)概率,另一(yī)方面也使得內(nèi)存分配工作變得更為(wèi)複雜,降低(dī)了運行效率。
為(wèi)了解決這個問題,Memcached使用了一(yī)種叫Slab的(de)結構。在該分配算法中,內(nèi)存将按照1MB的(de)大小劃分為(wèi)頁,而該頁內(nèi)存則會繼續被分割為(wèi)一(yī)系列具有相同大小的(de)內(nèi)存塊:
因此Memcached并不是直接根據需要記錄的(de)數據的(de)大小來直接分配相應大小的(de)內(nèi)存。在一(yī)條新的(de)記錄到來時,Memcached會首先檢查該記錄的(de)大小,并根據記錄的(de)大小選擇記錄所需要存儲到的(de)Slab類型。接下來,Memcached就會檢查其內(nèi)部所包含的(de)該類型Slab。如(rú)果這些Slab中有空餘的(de)塊,那麽Memcached就會使用該塊記錄該條信息。如(rú)果已經沒有Slab擁有空閑的(de)具有合适大小的(de)塊,那麽Memcached就會創建一(yī)個新的(de)頁,并将該頁按照目标Slab的(de)類型進行劃分。
一(yī)個需要考慮的(de)特殊情況就是對記錄的(de)更新。在對一(yī)個記錄進行更新的(de)時候,記錄的(de)大小可(kě)能會發生變化。在這種情況下,其所對應的(de)Slab類型也可(kě)能會發生變化。因此在更新時,記錄在內(nèi)存中的(de)位置可(kě)能會發生變化。隻不過從用戶的(de)角度來說,這并不可(kě)見。
Memcached使用這種方式來分配內(nèi)存的(de)好處則在于,其可(kě)以降低(dī)由于記錄的(de)多次讀寫而導緻的(de)碎片化。反過來,由于Memcached是根據記錄的(de)大小選擇需要插入到的(de)塊類型,因此為(wèi)每個記錄所分配的(de)塊的(de)大小常常大于該記錄所實際需要的(de)內(nèi)存大小,進而造成了內(nèi)存的(de)浪費。當然,您可(kě)以通過Memcached的(de)配置文件來指定各個塊的(de)大小,從而盡可(kě)能地(dì)減少內(nèi)存的(de)浪費。
但是需要注意的(de)是,由于默認情況下Memcached中每頁的(de)大小為(wèi)1MB,因此其單個塊最大為(wèi)1MB。除此之外,Memcached還限制每個數據所對應的(de)鍵的(de)長(cháng)度不能超過250個字節。
一(yī)般來說,Slab中各個塊的(de)大小以及塊大小的(de)遞增倍數可(kě)能會對記錄所載位置的(de)選擇及內(nèi)存利用率有很大的(de)影響。例如(rú)在當前的(de)實現下,各個Slab中塊的(de)大小默認情況下是按照1.25倍的(de)方式來遞增的(de)。也就是說,在一(yī)個Memcached實例中,某種類型Slab所提供的(de)塊的(de)大小是80K,而提供稍大一(yī)點空間的(de)Slab類型所提供的(de)塊的(de)大小就将是100K。如(rú)果現在我們需要插入一(yī)條81K的(de)記錄,那麽Memcached就會選擇具有100K塊大小的(de)Slab,并嘗試找到一(yī)個具有空閑塊的(de)Slab以存入該記錄。
同時您也需要注意到,我們使用的(de)是100K塊大小的(de)Slab來記錄具有81K大小的(de)數據,因此記錄該數據所導緻的(de)內(nèi)存浪費是19K,即19%的(de)浪費。而在需要存儲的(de)各條記錄的(de)大小平均分布的(de)情況下,這種內(nèi)存浪費的(de)幅度也在9%左右。該幅度實際上取決于我們剛剛提到的(de)各個Slab中塊大小的(de)遞增倍數。在Memcached的(de)初始實現中,各個Slab塊的(de)遞增倍數在默認情況下是2,而不是現在的(de)1.25,從而導緻了平均25%左右的(de)內(nèi)存浪費。而在今後的(de)各個版本中,該遞增倍數可(kě)能還會發生變化,以優化Memcached的(de)實際性能。
如(rú)果您一(yī)旦知道(dào)了您所需要緩存的(de)數據的(de)特征,如(rú)通常情況下數據的(de)大小以及各個數據的(de)差異幅度,那麽您就可(kě)以根據這些數據的(de)特征來設置上面所提到的(de)各個參數。如(rú)果數據在通常情況下都比較小,那麽我們就需要将最小塊的(de)大小調整得小一(yī)些。如(rú)果數據的(de)大小變動不是很大,那麽我們可(kě)以将塊大小的(de)遞增倍數設置得小一(yī)些,從而使得各個塊的(de)大小盡量地(dì)貼近需要存儲的(de)數據,以提高(gāo)內(nèi)存的(de)利用率。
還有一(yī)個值得注意的(de)事情就是,由于Memcached在計算到底哪個服務實例記錄了具有特定鍵的(de)數據時并不會考慮用來組成緩存系統中各個服務器的(de)差異性。如(rú)果每個服務器上隻安裝了一(yī)個Memcached實例,那麽各個Memcached實例所擁有的(de)可(kě)用內(nèi)存将存在着數倍的(de)差異。但是由于各個實例被選中的(de)概率基本相同,因此具有較大內(nèi)存的(de)Memcached實例将無法被充分利用。我們可(kě)以通過在具有較大內(nèi)存的(de)服務器上部署多個Memcached實例來解決這個問題:
例如(rú)上圖所展示的(de)緩存系統是由兩個服務器組成。這兩個服務器中的(de)內(nèi)存大小并不相同。第一(yī)個服務器的(de)內(nèi)存大小為(wèi)32G,而第二個服務器的(de)內(nèi)存大小僅僅有8G。為(wèi)了能夠充分利用這兩個服務器的(de)內(nèi)存,我們在具有32G內(nèi)存的(de)服務器上部署了4個Memcached實例,而在隻有8G內(nèi)存的(de)服務器上部署了1個Memcached實例。在這種情況下,32G內(nèi)存服務器上的(de)4個Memcached實例将總共得到4倍于8G服務器所得到的(de)負載,從而充分地(dì)利用了32G內(nèi)存服務器上的(de)內(nèi)存。
當然,由于緩存系統擁有有限的(de)資源,因此其會在某一(yī)時刻被服務所産生的(de)數據填滿。如(rú)果此時緩存系統再次接收到一(yī)個緩存數據的(de)請求,那麽它就會根據LRU(Least recently used)算法以及數據的(de)過期時間來決定需要從緩存系統中移除的(de)數據。而Memcached所使用的(de)過期算法比較特殊,又被稱為(wèi)延遲過期(Lazy expiration):當用戶從Memcached實例中讀取數據的(de)時候,其将首先通過配置中所設置的(de)過期時間來決定該數據是否過期。如(rú)果是,那麽在下一(yī)次寫入數據卻沒有足夠空間的(de)時候,Memcached會選擇該過期數據所在的(de)內(nèi)存塊作為(wèi)新數據的(de)目标地(dì)址。如(rú)果在寫入時沒有相應的(de)記錄被标記為(wèi)過期,那麽LRU算法才被執行,從而找到最久沒有被使用的(de)需要被替換的(de)數據。
這裏的(de)LRU是在Slab範圍內(nèi)的(de),而不是全局的(de)。假設Memcached緩存系統中的(de)最常用的(de)數據都存儲在100K的(de)塊中,而該系統中還存在着另外一(yī)種類型的(de)Slab,其塊大小是300K,但是存在于其中的(de)數據并不常用。當需要插入一(yī)條99K的(de)數據而Memcached已經沒有足夠的(de)內(nèi)存再次分配一(yī)個Slab實例的(de)時候,其并不會釋放具有300K塊大小的(de)Slab,而是在100K塊大小的(de)各個Slab中找到需要釋放的(de)塊,并将新數據添加到該塊中。
高(gāo)可(kě)用性
在企業級應用中,我們常常強調一(yī)個系統需要擁有高(gāo)可(kě)用性和(hé)高(gāo)可(kě)靠性。而對于一(yī)個組成而言,其需要能夠穩定地(dì)運行,并在出現異常的(de)時候盡量使得異常的(de)影響限制在某個特定的(de)範圍內(nèi),而不會導緻整個系統不能正常工作。而且在出現異常之後,該組成需要能較為(wèi)容易地(dì)恢複到正常的(de)工作狀态。
那麽Memcached需要什麽樣的(de)高(gāo)可(kě)用性呢(ne)?在講解這個問題之前,我們先來看看在一(yī)個大型服務中Memcached所組成的(de)服務端緩存是什麽樣的(de):
從上圖中可(kě)以看到,在一(yī)個大型服務中,由Memcached所組成的(de)服務端緩存實際上是由非常多的(de)Memcached實例組成的(de)。在前面我們已經介紹過,Memcached實例實際上是完全獨立的(de),不存在Memcached實例之間的(de)相互交互。因此在其中一(yī)個發生了故障的(de)時候,其它的(de)各個Memcached服務實例并不會受到影響。如(rú)果一(yī)個擁有了16個Memcached實例的(de)服務端緩存系統中的(de)一(yī)個Memcached實例發生了故障,那麽整個系統将還有93.75%的(de)緩存容量可(kě)以繼續工作。雖然緩存容量的(de)減少會略微增加其後的(de)各個服務實例的(de)壓力,但是一(yī)個應用所經曆的(de)負載波動常常比這個大得多,因此該服務應該還是能夠正常工作的(de)。
而這也恰恰表明了Memcached所具有的(de)獨立性的(de)正确性。由于Memcached本身緻力于創建一(yī)個高(gāo)效而且簡單,卻具有較強擴展性的(de)緩存組件,因此其并沒有強調數據的(de)安全性。一(yī)旦其中的(de)一(yī)個Memcached實例發生了故障,那麽我們還可(kě)以從數據庫及服務端再次計算得到該數據,并将其記錄在其它可(kě)用的(de)Memcached實例上。
我想您讀到這裏一(yī)定會想:“不,還有一(yī)個問題,那就是由于Memcached實例的(de)個數變化會導緻哈希計算的(de)結果發生變化,從而導緻所有對數據的(de)請求會導向到不正确的(de)Memcached實例,使得由Memcached實例集群所提供的(de)緩存服務全部失效,從而導緻數據庫的(de)壓力驟增。”
是的(de),這也是我曾經有所顧慮的(de)地(dì)方。而且這不僅僅在服務端緩存失效的(de)時候存在。隻要服務端緩存中Memcached實例的(de)數量發生了變化,那麽該問題就會發生。
Memcached所使用的(de)解決方法就是Consistent Hashing。在該算法的(de)幫助下,Memcached實例數量的(de)變化将隻可(kě)能導緻其中的(de)一(yī)小部分鍵的(de)哈希值發生改變。那該算法到底是怎麽運行的(de)呢(ne)?
首先請考慮一(yī)個圓,在該圓上分布了多個點,以表示整數0到1023。這些整數平均分布在整個圓上:
而在上圖中,我們則突出地(dì)顯示了6個藍點。這六個藍點基本上将該圓進行了六等分。而它們所對應的(de)就是在當前Memcached緩存系統中所包含的(de)三個Memcached實例m1,m2以及m3。好,接下來我們則對當前需要存儲的(de)數據執行哈希計算,并找到該哈希結果900在該圓上所對應的(de)點:
可(kě)以看到,該點在順時針距離(lí)上離(lí)表示0的(de)那個藍點最近,因此這個具有哈希值900的(de)數據将記錄在Memcached實例m1中。
如(rú)果其中的(de)一(yī)個Memcached實例失效了,那麽需要由該實例所記錄的(de)數據将暫時失效,而其它實例所記錄的(de)數據仍然還在:
從上圖中可(kě)以看到,在Memcached實例m1失效的(de)情況下,值為(wèi)900的(de)數據将失效,而其它的(de)值為(wèi)112和(hé)750的(de)數據将仍然記錄在Memcached實例m2及m3上。也就是說,一(yī)個節點的(de)失效現在将隻會導緻一(yī)部分數據不再在緩存系統中存在,而并沒有導緻其它實例上所記錄的(de)數據的(de)目标實例發生變化。
但是我們還不得不考慮另一(yī)個問題,那就是在一(yī)個服務的(de)服務端緩存僅僅由一(yī)個或幾個Memcached實例組成的(de)情況。在這種情況下,其中一(yī)個Memcached實例失效是較為(wèi)緻命的(de),因為(wèi)數據庫以及服務器實例将接收到大量的(de)需要進行複雜計算的(de)請求,并将最終導緻服務器實例和(hé)數據庫過載。因此在設計服務端緩存時,我們常常采取超出需求容量的(de)方法來定義這些緩存。例如(rú)在服務實際需要5個Memcached結點時我們設計一(yī)個包含6個節點的(de)服務端緩存系統,以增加整個系統的(de)容錯能力。
編輯:--ns868