在Web服務開發中,服務端緩存是服務實現中所經常採用的一種提升服務性能的方法。其經過記錄某部分計算結果來嘗試避免再次執行獲得該結果所須要的複雜計算,從而提升了服務的運行效率。html
除了可以提升服務的運行效率以外,服務端緩存還經常用來提升服務的擴展性。所以一些大規模的Web應用,如Facebook,經常構建一個龐大的服務端緩存。而它們所最常使用的就是Memcached。算法
在本文中,咱們就將對Memcached進行簡單地介紹。數據庫
Memcached簡介緩存
在介紹Memcached以前,讓咱們首先經過一個示例瞭解什麼是服務端緩存。安全
相信你們都玩過一些網絡聯機遊戲吧。在我那個年代(03年左右),這些遊戲經常添加了對戰功能,並提供了天梯來顯示具備最優秀戰績的玩家以及當前玩家在天梯系統中的排名。這是遊戲開發商所經常採用的一種聚攏玩家人氣的手段。而但願在遊戲中證實本身的玩家則會由此激發鬥志,進而花費更多時間來在天梯中取得更好的成績。服務器
就天梯系統來講,其最主要的功能就是爲玩家提供天梯排名的信息,而並不容許玩家對該系統中所記錄的數據做任何修改。這樣設定的結果就是,整個天梯系統的讀操做居多,而寫操做不多。反過來,因爲一個遊戲中的玩家可能有上千萬甚至上億人,並且在線人數經常達到上萬人,所以對天梯的訪問也會是很是頻繁的。這樣的話,即便每秒鐘只有10我的訪問天梯中的排名,對這上億個玩家的天梯排名進行讀取及排序也是一件很是消耗性能的事情。網絡
一個天然而然的想法就是:在對天梯排名進行一次計算後,咱們在服務端將該天梯排名緩存起來,並在其它玩家訪問的時候直接返回該緩存中所記錄的結果。而在必定時間段以後,如一個小時,咱們再對緩存中的數據進行更新。這樣咱們就再也不須要每一個小時執行成千上萬次的天梯排名計算了。數據結構
而這就是服務端緩存所提供的最重要功能。其既能夠提升單個請求的響應速度,又能夠下降服務層及數據庫層的壓力。除此以外,多個服務實例均可以讀取該服務端緩存所緩存的信息,所以咱們也再也不須要擔憂這些數據在各個服務實例中都保存了一份進而須要彼此同步的問題,也便是提升了擴展性。架構
而Memcached就是一個使用了BSD許可的服務端緩存實現。可是與其它服務端緩存實現不一樣的是,其主要由兩部分組成:獨立運行的Memcached服務實例,以及用於訪問這些服務實例的客戶端。所以相較於普通服務端緩存實現中各個緩存都運行在服務實例之上的狀況,Memcached服務實例則是在服務實例以外獨立運行的:運維
從上圖中能夠看出,因爲Memcached緩存實例是獨立於各個應用服務器實例運行的,所以應用服務實例能夠訪問任意的緩存實例。而傳統的緩存則與特定的應用實例綁定,所以每一個應用實例將只能訪問特定的緩存。這種綁定一方面會致使整個應用所可以訪問的緩存容量變得很小,另外一方面也可能致使不一樣的緩存實例中存在着冗餘的數據,從而下降了緩存系統的總體效率。
在運行時,Memcached服務實例只須要消耗很是少的CPU資源,卻須要使用大量的內存。所以在決定如何組織您的服務端緩存結構以前,您首先須要搞清當前服務中各個服務實例的負載狀況。若是一個服務器的CPU使用率很是高,卻存在着很是多的空餘內存,那麼咱們就徹底能夠在其上運行一個Memcached實例。而若是當前服務中的全部服務實例都沒有過多的空餘內存,那麼咱們就須要使用一系列獨立的服務實例來搭建服務端緩存。一個大型服務經常擁有上百個Memcached實例。而在這上百個Memcached實例中所存儲的數據則不盡相同。因爲這種數據的異構性,咱們須要在訪問由Memcached所記錄的信息以前決定在該服務端緩存系統中到底由哪一個Memcached實例記錄了咱們所想要訪問的數據:
如上圖所示,用戶須要經過一個Memcached客戶端來完成對緩存服務所記錄信息的訪問。該客戶端知道服務端緩存系統中所包含的全部Memcached服務實例。在須要訪問具備特定鍵值的數據時,該客戶端內部會根據所須要讀取的數據的鍵值,如「foo」,以及當前Memcached緩存服務的配置來計算相應的哈希值,以決定究竟是哪一個Memcached實例記錄了用戶所須要訪問的信息。在決定記錄了所須要信息的Memcached實例以後,Memcached客戶端將從配置中讀取該Memcached服務實例所在地址,並向該Memcached實例發送數據訪問請求,以從該Memcached實例中讀取具備鍵值「foo」的信息。在各個論壇的討論中,這被稱爲是Memcached的兩階段哈希(Two-stage hash)。
而對數據的記錄也使用了相似的流程:假設用戶但願經過服務端緩存記錄數據「bar」,併爲其指定鍵值「foo」。那麼Memcached客戶端將首先對用戶所賦予的鍵值「foo」及當前服務端緩存所記錄的可用服務實例個數執行哈希計算,並根據哈希計算結果來決定存儲該數據的Memcached服務實例。接下來,客戶端就會向該實例發送請求,以在其中記錄具備鍵值「foo」的數據「bar」。
這樣作的好處則在於,每一個Memcached服務實例都是獨立的,而彼此之間並無任何交互。在這種狀況下,咱們能夠省略不少複雜的功能邏輯,如各個節點之間的數據同步以及結點之間消息的廣播等等。這種輕量級的架構能夠簡化不少操做。如在一個節點失效的時候,咱們僅僅須要使用一個新的Memcached節點替代老節點便可。而在對緩存進行擴容的時候,咱們也只須要添加額外的服務並修改客戶端配置。
這些記錄在服務端緩存中的數據是全局可見的。也就是說,一旦在Memcached服務端緩存中成功添加了一條新的記錄,那麼其它使用該緩存服務的應用實例將一樣能夠訪問該記錄:
在Memcached中,每條記錄都由四部分組成:記錄的鍵,有效期,一系列可選的標記以及表示記錄內容的數據。因爲記錄內容的數據中並不包含任何數據結構,所以咱們在Memcached中所記錄的數據須要是通過序列化以後的表示。
內存管理
在使用緩存時,咱們不得不考慮的一個問題就是如何對這些緩存數據的生存期進行管理。這其中包括如何使一個記錄在緩存中的數據過時,如何在緩存空間不夠時執行數據的替換等。所以在本節中,咱們將對Memcached的內存管理機制進行介紹。
首先咱們來看一看Memcached的內存管理模型。一般狀況下,一個內存管理算法所最須要考慮的問題就是內存的碎片化(Fragmentation):在長時間地分配及回收以後,被系統所使用的內存將趨向於散落在不連續的空間中。這使得系統很難找到連續內存空間,一方面增大了內存分配失敗的機率,另外一方面也使得內存分配工做變得更爲複雜,下降了運行效率。
爲了解決這個問題,Memcached使用了一種叫Slab的結構。在該分配算法中,內存將按照1MB的大小劃分爲頁,而該頁內存則會繼續被分割爲一系列具備相同大小的內存塊:
所以Memcached並非直接根據須要記錄的數據的大小來直接分配相應大小的內存。在一條新的記錄到來時,Memcached會首先檢查該記錄的大小,並根據記錄的大小選擇記錄所須要存儲到的Slab類型。接下來,Memcached就會檢查其內部所包含的該類型Slab。若是這些Slab中有空餘的塊,那麼Memcached就會使用該塊記錄該條信息。若是已經沒有Slab擁有空閒的具備合適大小的塊,那麼Memcached就會建立一個新的頁,並將該頁按照目標Slab的類型進行劃分。
一個須要考慮的特殊狀況就是對記錄的更新。在對一個記錄進行更新的時候,記錄的大小可能會發生變化。在這種狀況下,其所對應的Slab類型也可能會發生變化。所以在更新時,記錄在內存中的位置可能會發生變化。只不過從用戶的角度來講,這並不可見。
Memcached使用這種方式來分配內存的好處則在於,其能夠下降因爲記錄的屢次讀寫而致使的碎片化。反過來,因爲Memcached是根據記錄的大小選擇須要插入到的塊類型,所以爲每一個記錄所分配的塊的大小經常大於該記錄所實際須要的內存大小,進而形成了內存的浪費。固然,您能夠經過Memcached的配置文件來指定各個塊的大小,從而儘量地減小內存的浪費。
可是須要注意的是,因爲默認狀況下Memcached中每頁的大小爲1MB,所以其單個塊最大爲1MB。除此以外,Memcached還限制每一個數據所對應的鍵的長度不能超過250個字節。
通常來講,Slab中各個塊的大小以及塊大小的遞增倍數可能會對記錄所載位置的選擇及內存利用率有很大的影響。例如在當前的實現下,各個Slab中塊的大小默認狀況下是按照1.25倍的方式來遞增的。也就是說,在一個Memcached實例中,某種類型Slab所提供的塊的大小是80K,而提供稍大一點空間的Slab類型所提供的塊的大小就將是100K。若是如今咱們須要插入一條81K的記錄,那麼Memcached就會選擇具備100K塊大小的Slab,並嘗試找到一個具備空閒塊的Slab以存入該記錄。
同時您也須要注意到,咱們使用的是100K塊大小的Slab來記錄具備81K大小的數據,所以記錄該數據所致使的內存浪費是19K,即19%的浪費。而在須要存儲的各條記錄的大小平均分佈的狀況下,這種內存浪費的幅度也在9%左右。該幅度實際上取決於咱們剛剛提到的各個Slab中塊大小的遞增倍數。在Memcached的初始實現中,各個Slab塊的遞增倍數在默認狀況下是2,而不是如今的1.25,從而致使了平均25%左右的內存浪費。而在從此的各個版本中,該遞增倍數可能還會發生變化,以優化Memcached的實際性能。
若是您一旦知道了您所須要緩存的數據的特徵,如一般狀況下數據的大小以及各個數據的差別幅度,那麼您就能夠根據這些數據的特徵來設置上面所提到的各個參數。若是數據在一般狀況下都比較小,那麼咱們就須要將最小塊的大小調整得小一些。若是數據的大小變更不是很大,那麼咱們能夠將塊大小的遞增倍數設置得小一些,從而使得各個塊的大小盡可能地貼近須要存儲的數據,以提升內存的利用率。
還有一個值得注意的事情就是,因爲Memcached在計算到底哪一個服務實例記錄了具備特定鍵的數據時並不會考慮用來組成緩存系統中各個服務器的差別性。若是每一個服務器上只安裝了一個Memcached實例,那麼各個Memcached實例所擁有的可用內存將存在着數倍的差別。可是因爲各個實例被選中的機率基本相同,所以具備較大內存的Memcached實例將沒法被充分利用。咱們能夠經過在具備較大內存的服務器上部署多個Memcached實例來解決這個問題:
例如上圖所展現的緩存系統是由兩個服務器組成。這兩個服務器中的內存大小並不相同。第一個服務器的內存大小爲32G,而第二個服務器的內存大小僅僅有8G。爲了可以充分利用這兩個服務器的內存,咱們在具備32G內存的服務器上部署了4個Memcached實例,而在只有8G內存的服務器上部署了1個Memcached實例。在這種狀況下,32G內存服務器上的4個Memcached實例將總共獲得4倍於8G服務器所獲得的負載,從而充分地利用了32G內存服務器上的內存。
固然,因爲緩存系統擁有有限的資源,所以其會在某一時刻被服務所產生的數據填滿。若是此時緩存系統再次接收到一個緩存數據的請求,那麼它就會根據LRU(Least recently used)算法以及數據的過時時間來決定須要從緩存系統中移除的數據。而Memcached所使用的過時算法比較特殊,又被稱爲延遲過時(Lazy expiration):當用戶從Memcached實例中讀取數據的時候,其將首先經過配置中所設置的過時時間來決定該數據是否過時。若是是,那麼在下一次寫入數據卻沒有足夠空間的時候,Memcached會選擇該過時數據所在的內存塊做爲新數據的目標地址。若是在寫入時沒有相應的記錄被標記爲過時,那麼LRU算法才被執行,從而找到最久沒有被使用的須要被替換的數據。
這裏的LRU是在Slab範圍內的,而不是全局的。假設Memcached緩存系統中的最經常使用的數據都存儲在100K的塊中,而該系統中還存在着另一種類型的Slab,其塊大小是300K,可是存在於其中的數據並不經常使用。當須要插入一條99K的數據而Memcached已經沒有足夠的內存再次分配一個Slab實例的時候,其並不會釋放具備300K塊大小的Slab,而是在100K塊大小的各個Slab中找到須要釋放的塊,並將新數據添加到該塊中。
高可用性
在企業級應用中,咱們經常強調一個系統須要擁有高可用性和高可靠性。而對於一個組成而言,其須要可以穩定地運行,並在出現異常的時候儘可能使得異常的影響限制在某個特定的範圍內,而不會致使整個系統不能正常工做。並且在出現異常以後,該組成須要能較爲容易地恢復到正常的工做狀態。
那麼Memcached須要什麼樣的高可用性呢?在講解這個問題以前,咱們先來看看在一個大型服務中Memcached所組成的服務端緩存是什麼樣的:
從上圖中能夠看到,在一個大型服務中,由Memcached所組成的服務端緩存其實是由很是多的Memcached實例組成的。在前面咱們已經介紹過,Memcached實例其實是徹底獨立的,不存在Memcached實例之間的相互交互。所以在其中一個發生了故障的時候,其它的各個Memcached服務實例並不會受到影響。若是一個擁有了16個Memcached實例的服務端緩存系統中的一個Memcached實例發生了故障,那麼整個系統將還有93.75%的緩存容量能夠繼續工做。雖然緩存容量的減小會略微增長其後的各個服務實例的壓力,可是一個應用所經歷的負載波動經常比這個大得多,所以該服務應該仍是可以正常工做的。
而這也偏偏代表了Memcached所具備的獨立性的正確性。因爲Memcached自己致力於建立一個高效並且簡單,卻具備較強擴展性的緩存組件,所以其並無強調數據的安全性。一旦其中的一個Memcached實例發生了故障,那麼咱們還能夠從數據庫及服務端再次計算獲得該數據,並將其記錄在其它可用的Memcached實例上。
我想您讀到這裏必定會想:「不,還有一個問題,那就是因爲Memcached實例的個數變化會致使哈希計算的結果發生變化,從而致使全部對數據的請求會導向到不正確的Memcached實例,使得由Memcached實例集羣所提供的緩存服務所有失效,從而致使數據庫的壓力驟增。」
是的,這也是我曾經有所顧慮的地方。並且這不只僅在服務端緩存失效的時候存在。只要服務端緩存中Memcached實例的數量發生了變化,那麼該問題就會發生。
Memcached所使用的解決方法就是Consistent Hashing。在該算法的幫助下,Memcached實例數量的變化將只可能致使其中的一小部分鍵的哈希值發生改變。那該算法究竟是怎麼運行的呢?
首先請考慮一個圓,在該圓上分佈了多個點,以表示整數0到1023。這些整數平均分佈在整個圓上:
而在上圖中,咱們則突出地顯示了6個藍點。這六個藍點基本上將該圓進行了六等分。而它們所對應的就是在當前Memcached緩存系統中所包含的三個Memcached實例m1,m2以及m3。好,接下來咱們則對當前須要存儲的數據執行哈希計算,並找到該哈希結果900在該圓上所對應的點:
能夠看到,該點在順時針距離上離表示0的那個藍點最近,所以這個具備哈希值900的數據將記錄在Memcached實例m1中。
若是其中的一個Memcached實例失效了,那麼須要由該實例所記錄的數據將暫時失效,而其它實例所記錄的數據仍然還在:
從上圖中能夠看到,在Memcached實例m1失效的狀況下,值爲900的數據將失效,而其它的值爲112和750的數據將仍然記錄在Memcached實例m2及m3上。也就是說,一個節點的失效如今將只會致使一部分數據再也不在緩存系統中存在,而並無致使其它實例上所記錄的數據的目標實例發生變化。
可是咱們還不得不考慮另外一個問題,那就是在一個服務的服務端緩存僅僅由一個或幾個Memcached實例組成的狀況。在這種狀況下,其中一個Memcached實例失效是較爲致命的,由於數據庫以及服務器實例將接收到大量的須要進行復雜計算的請求,並將最終致使服務器實例和數據庫過載。所以在設計服務端緩存時,咱們經常採起超出需求容量的方法來定義這些緩存。例如在服務實際須要5個Memcached結點時咱們設計一個包含6個節點的服務端緩存系統,以增長整個系統的容錯能力。
使用Memcached搭建緩存系統
OK,在對Memcached內部運行原理介紹完畢以後,咱們就來看看如何使用Memcached爲您的服務搭建緩存系統。
首先,您須要從Memcached官方網站上下載Memcached的安裝文件,並在您做爲緩存服務器的系統上進行安裝。在安裝時,您須要對Memcached進行適當地配置,如其所須要偵聽的端口,爲其所分配的內存大小等。在Memcached正確配置並啓動以後,咱們就能夠經過一系列客戶端軟件訪問這些Memcached實例並對其進行操做了。因爲我並非一個運維人員,所以在這裏咱們將再也不對這些配置進行詳細地介紹。
而咱們要介紹的,則是如何用Memcached的Java客戶端去讀寫數據。 而一個較爲常見的Memcached客戶端則是SpyMemcached。就讓咱們來看一看Spy Memcached的Main函數中是如何對其所提供的功能進行使用的:
1 public static void main(String[] args) throws Exception{ 2 if(args.length < 2){ 3 System.out.println("Please specify command line options"); 4 return; 5 } 6 7 MemcachedClient memcachedClient = new MemcachedClient(AddrUtil.getAddresses("127.0.0.1:11211")); 8 if (commandName.equals("get")){ 9 String keyName= args[1]; 10 System.out.println("Key Name " + keyName); 11 System.out.println("Value of key " + memcachedClient.get(keyName)); 12 } else if(commandName.equals("set")){ 13 String keyName =args[1]; 14 String value=args[2]; 15 System.out.println("Key Name " + keyName + " value=" + value); 16 Future<Boolean> result= memcachedClient.set(keyName, 0, value); 17 System.out.println("Result of set " + result.get()); 18 } else if(commandName.equals("add")){ 19 String keyName =args[1]; 20 String value=args[2]; 21 System.out.println("Key Name " + keyName + " value=" + value); 22 Future<Boolean> result= memcachedClient.add(keyName, 0, value); 23 System.out.println("Result of add " + result.get()); 24 } else if(commandName.equals("replace")){ 25 String keyName =args[1]; 26 String value=args[2]; 27 System.out.println("Key Name " + keyName + " value=" + value); 28 Future<Boolean> result= memcachedClient.replace(keyName, 0, value); 29 System.out.println("Result of replace " + result.get()); 30 } else if(commandName.equals("delete")){ 31 String keyName =args[1]; 32 System.out.println("Key Name " + keyName ); 33 Future<Boolean> result= memcachedClient.delete(keyName); 34 System.out.println("Result of delete " + result.get()); 35 } else{ 36 System.out.println("Command not found"); 37 } 38 memcachedClient.shutdown(); 39 }
能夠看到,在該客戶端的幫助下,對存儲在Memcached實例中的數據的讀取已經變得很是簡單了。咱們能夠簡單地經過調用該客戶端的get(),set(),add(),replace()以及delete()函數來完成對數據的操做。
轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4681643.html
商業轉載請事先與我聯繫:silverfox715@sina.com