search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

緩存那些事

作者簡介:熊明輝,美團點評酒旅事業群酒店住宿研發團隊B端商家業務平台負責人,主導構建商家業務平台系統,支撐美團點評酒店住宿業務的飛速發展需求。曾任職於聯想集團、百度。

導語:在網路分層應用服務中,緩存的使用已比較普及,本文將結合作者實際工作經驗總結,講述在不同的場景下如何選擇和使用適用的緩存框架,以達到提升服務質量,優化系統架構的目的。

一般而言,現在互聯網模式(一個網站或一個應用),整體流程可以概括描述為 瀏覽器→應用伺服器→資料庫或文件(存儲)→應用伺服器→瀏覽器,這是一個標準流程,通過瀏覽器(或App界面)發起請求,經過伺服器、資料庫計算整合后反饋瀏覽器呈現內容。隨著互聯網的普及,內容信息越來越複雜,使用者和訪問量越來越大,我們的應用需要支撐更多的併發量,同時我們的應用伺服器和資料庫伺服器所做的計算也越來越多。但是往往我們的應用伺服器資源是有限的,且技術變革是緩慢的,資料庫每秒能接受的請求次數也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供儘可能大的吞吐量?一個有效的辦法就是減少計算量,縮短請求流程——這就是緩存。緩存的出現就是打破上述的標準流程,其中的任何一個環節都可以被截斷,請求可以從緩存中直接獲取目標數據並返回。通過這種打破常規的方式,有效減少計算量,縮短請求流程,有效提升響應速度,節省硬體資源,讓有限的資源服務更多的用戶。

如圖1所示,緩存的使用可以出現在 1-4的各個環節中,每個環節的緩存方案與使用各有特點。

圖1 網路應用一般流程

緩存特徵

根據面向對象的軟體思維來看,緩存就是一個對象類型,那麼必然有它的屬性:

命中率

命中率=返回正確結果數/請求緩存次數,命中率問題是緩存中的一個非常重要的問題,它是衡量緩存有效性的重要指標。命中率越高,表明緩存的使用率越高。

最大元素(或最大空間)

緩存中可以存放的最大元素的數量,一旦緩存中元素數量超過這個值(或者緩存數據所佔空間超過其最大支持空間),那麼將會觸發緩存啟動清空策略根據不同的場景合理的設置最大元素值往往可以一定程度上提高緩存的命中率,從而更有效的時候緩存。

清空策略

如上描述,緩存的存儲空間有限制,當緩存空間被用滿時,如何保證在穩定服務的同時有效提升命中率?這就由緩存清空策略來處理,設計適合自身數據特徵的情況策略能有效提升命中率。常見的一般策略有:

a. FIFO(first in first out)

先進先出策略,最先進入緩存的數據在緩存空間不夠的情況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的數據。策略演算法主要比較緩存元素的創建時間。

b. LFU(less frequently used)

最少使用策略,無論是否過期,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略演算法主要比較元素的hitCount(命中次數)。

c. LRU(least recently used)

最近最少使用策略,無論是否過期,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略演算法主要比較元素最近一次被get使用時間。

除此之外,還有一些簡單策略比如:

根據過期時間判斷,清理過期時間最長的元素;

根據過期時間判斷,清理最近要過期的元素;

隨機清理;

根據關鍵字(或元素內容)長短清理等。

緩存介質

(從硬體介質上來看,無非就是內存和硬碟兩種)從技術上劃分,可以分成幾種,內存、硬碟文件、資料庫。

  • 內存:將緩存存儲於內存中是最快的選擇,無需額外的I/O開銷,但是內存的缺點是沒有持久化落地物理磁碟,一旦應用異常break down,重新啟動數據很難或者無法復原。

  • 硬碟:一般來說,很多緩存框架會結合使用內存和硬碟,在內存分配空間滿了或是在異常的情況下,可以被動或主動的將內存空間數據持久化到硬碟中,達到釋放空間或備份數據的目的。

  • 資料庫:前面我們有提到,增加緩存的策略的目的之一就是為了減少資料庫的I/O壓力。現在使用資料庫做緩存介質是不是又回到了老問題上了?其實,資料庫也有很多種類型,像那些不支持SQL,只是簡單的key、value的存儲結構的特殊資料庫(如berkleydb),響應速度和吞吐量都遠遠高於我們常用的關係型資料庫等。

在目前的應用服務框架中,我們對緩存的分類劃分更常用的是根據緩存與應用的耦合程度,劃分為local cache(本地緩存)和remote cache(分散式緩存):

  • Local cache:指的是在應用中的緩存組件,其最大的優點是應用和cache是在同一個進程內部,請求緩存非常快速,沒有過多的網路開銷等,在單應用不需要集群支持或者集群情況下各節點無需互相通知的場景下使用本地緩存較合適;同時,它的缺點也是應為緩存跟應用程序耦合,多個應用程序無法直接的共享緩存,各應用或集群的各節點都需要維護自己的單獨緩存,對內存是一種浪費。

  • Remote cache::指的是與應用分離的緩存組件或服務,其最大的優點是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享緩存。

目前各種類型的緩存都活躍在成千上萬的應用服務中,還沒有一種緩存方案可以解決一切的業務場景或數據類型,我們需要根據自身的特殊場景和背景,選擇最適合的緩存方案。緩存的使用是程序員、架構師的必備技能,好的程序員能根據數據類型、業務場景來準確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達到最優的目的。

本地緩存

編程式緩存實現

a. 成員變數或局部變數實現

簡單代碼示例如圖2所示。

圖2 簡單代碼示例圖

以局部變數map結構緩存部分業務數據,減少頻繁的重複資料庫I/O操作。缺點僅限於類的自身作用域內,類間無法共享緩存。

b. 靜態變數實現

最常用的單例實現靜態資源緩存,代碼示例如下:

publicclassCityUtils {

privatestaticfinal HttpClient httpClient = ServerHolder.createClientWithPool; privatestaticMap<integer,string=""> cityIdNameMap =newHashMap<integer,string="">;

privatestaticMap<integer,string=""> districtIdNameMap =newHashMap<integer,string="">;

static{ HttpGetget=newHttpGet("http://gis-in.sankuai.com/api/location/city/all"); BaseAuthorizationUtils.generateAuthAndDateHeader(get, BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC, BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);

try{ String resultStr = httpClient.execute(getnewBasicResponseHandler); JSONObject resultJo =newJSONObject(resultStr);

JSONArray dataJa = resultJo.getJSONArray("data");

for(inti =0; i < dataJa.length; i++) { JSONObject itemJo = dataJa.getJSONObject(i); cityIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name")); } }catch(Exception e) {

thrownewRuntimeException("Init City List Error!", e); } } static{ HttpGetget=newHttpGet("http://gis-in.sankuai.com/api/location/district/all"get, BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC, BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);

try{ String resultStr = httpClient.execute(getnewBasicResponseHandler); JSONObject resultJo =newJSONObject(resultStr); JSONArray dataJa = resultJo.getJSONArray("data");

for(inti =0; i < dataJa.length; i++) { JSONObject itemJo = dataJa.getJSONObject(i); districtIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name")); } }catch(Exception e) {

thrownewRuntimeException("Init District List Error!", e); } }

publicstaticStringgetCityName(intcityId) { String name = cityIdNameMap.get(cityId);

if(name ==null) { name ="未知"; }

returnname; }

publicstaticStringgetDistrictName(intdistrictId) { String name = districtIdNameMap.get(districtId);

if(name ==null) { name ="未知"; }

returnname; } }</integer,></integer,></integer,></integer,>

業務中常用的城市基礎基本信息判斷,通過靜態變數一次獲取緩存內存中,減少頻繁的I/O讀取,靜態變數實現類間可共享,進程內可共享,緩存的實時性稍差。

為了解決本地緩存數據的實時性問題,目前大量使用的是結合ZooKeeper的自動發現機制,實時變更本地靜態變數緩存:

MtConfig基礎組件,採用的就是類似原理,使用靜態變數緩存,結合ZooKeeper的統一管理,做到自動動態更新緩存,如圖3所示。

圖3 Mtconfig實現圖

這類的緩存實現,優點是直接的在heap區內讀寫,最快也最方便;缺點同樣是受heap區域影響,緩存的數據量非常有限,同時緩存時間受GC影響。主要滿足單機場景下的小數據量緩存需求,同時對緩存數據的變更無需太敏感感知,如上一般配置管理、基礎靜態數據等場景。

EhCache

Ehcache是現在最流行的純Java開源緩存框架,配置簡單、結構清晰、功能強大,是一個非常輕量級的緩存實現,我們常用的Hibernate裡面就集成了相關緩存功能。

圖4 ehcache框架圖

從圖4中我們可以了解到,ehcache的核心定義主要包括:

cache manager:緩存管理器,以前是只允許單例的,不過現在也可以多實例了。

cache:緩存管理器內可以放置若干cache,存放數據的實質,所有cache都實現了Ehcache介面,這是一個真正使用的緩存實例;通過緩存管理器的模式,可以在單個應用中輕鬆隔離多個緩存實例,獨立服務於不同業務場景需求,緩存數據物理隔離,同時需要時又可共享使用。

element:單條緩存數據的組成單位。

system of record(SOR):可以取到真實數據的組件,可以是真正的業務邏輯、外部介面調用、存放真實數據的資料庫等,緩存就是從SOR中讀取或者寫入到SOR中去的。

在上層可以看到,整個ehcache提供了對 JSR、JMX等的標準支持,能夠較好的兼容和移植,同時對各類對象有較完善的監控管理機制。它的緩存介質涵蓋堆內存(heap)、堆外內存(BigMemory 商用版本支持)和磁碟,各介質可獨立設置屬性和策略。ehcache最初是獨立的本地緩存框架組件,在後期的發展中,結合Terracotta服務陣列模型,可以支持分散式緩存集群,主要有RMI、JGroups、JMS和Cache Server等傳播方式進行節點間通信,如圖4的左側部分描述。

整體數據流轉包括這樣幾類行為:

  • Flush:緩存條目向低層次移動。

  • Fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發現自己的該緩存條目已經失效,就觸- 發了Fault行為。

  • Eviction:把緩存條目除去。

  • Expiration:失效狀態。

  • Pinning:強制緩存條目保持在某一層。

圖5反應了數據在各個層之間的流轉,同時也體現了各層數據的一個生命周期。 以下看下ehcache的配置使用:

<ehcache

<!-- 指定一個文件目錄,當EHCache把數據寫到硬碟上時,將把數據寫到這個文件目錄下 -->

<diskstorepath="java.io.tmpdir"

<!-- 設定緩存的默認數據過期策略 -->

<defaultcachemaxelementsinmemory="10000"eternal="false"overflowtodisk="true"timetoidleseconds="0"timetoliveseconds="0"diskpersistent="false"diskexpirythreadintervalseconds="120"

<!-- 設定具體的命名緩存的數據過期策略 cache元素的屬性: name:緩存名稱 maxElementsInMemory:內存中最大緩存對象數 maxElementsOnDisk:硬碟中最大緩存對象數,若是0表示無窮大 eternal:true表示對象永不過期,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認為false overflowToDisk:true表示當內存緩存的對象數目達到了maxElementsInMemory界限后,會把溢出的對象寫到硬碟緩存中。注意:如果緩存的對象要寫入到硬碟中的話,則該對象必須實現了Serializable介面才行。 diskSpoolBufferSizeMB:磁碟緩存區大小,默認為30MB。每個Cache都應該有自己的一個緩存區。 diskPersistent:是否緩存虛擬機重啟期數據 diskExpiryThreadIntervalSeconds:磁碟失效線程運行時間間隔,默認為120秒 timeToIdleSeconds: 設定允許對象處於空閑狀態的最長時間,以秒為單位。當對象自從最近一次被訪問后,如果處於空閑狀態的時間超過了timeToIdleSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清空。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地處於空閑狀態 timeToLiveSeconds:設定對象允許存在於緩存中的最長時間,以秒為單位。當對象自從被存放到緩存中后,如果處於緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清除。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地存在於緩存中。timeToLiveSeconds必須大於timeToIdleSeconds屬性,才有意義 memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。可選策略有:LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)。

整體上看,ehcache的使用還是相對簡單便捷的,提供了完整的各類API介面。需要注意的是,雖然ehcache支持磁碟的持久化,但是由於存在兩級緩存介質,在一級內存中的緩存,如果沒有主動的刷入磁碟持久化的話,在應用異常down機等情形下,依然會有緩存數據丟失的出現,為此可以根據需要將緩存刷到磁碟,將緩存條目刷到磁碟的操作可以通過cache.flush方法來執行,需要注意的是,對於對象的磁碟寫入,前提是要將對象進行序列化。

主要特性:

  • 快速,針對大型高併發系統場景,ehcache的多線程機制有相應的優化改善。

  • 簡單,很小的jar包,簡單配置就可直接使用,單機場景下無需過多的其他服務依賴。

  • 支持多種的緩存策略,靈活。

  • 緩存數據有兩級:內存和磁碟,與一般的本地內存緩存相比,有了磁碟的存儲空間,將可以支持更大量的數據緩存需求。

  • 具有緩存和緩存管理器的偵聽介面,能更簡單方便的進行緩存實例的監控管理。

  • 支持多緩存管理器實例,以及一個實例的多個緩存區域。

注意:ehcache的超時設置主要是針對整個cache實例設置整體的超時策略,而沒有較好的處理針對單獨的key的個性的超時設置(有策略設置,但是比較複雜,就不描述了),因此,在使用中要注意過期失效的緩存元素無法被GC回收,時間越長緩存越多,內存佔用也就越大,內存泄露的概率也越大。

Guava Cache

Guava Cache是google開源的java重用工具集庫里的一款緩存工具,其主要實現的緩存功能有:

  • 自動將entry節點載入進緩存結構中;

  • 當緩存的數據超過設置的最大值時,使用LRU演算法移除;

  • 具備根據entry節點上次被訪問或者寫入時間計算它的過期機制;

  • 緩存的key被封裝在 WeakReference引用內;

  • 緩存的value被封裝在 WeakReference或SoftReference引用內;

  • 統計緩存使用過程中命中率、異常率、未命中率等統計數據。

圖5 guavacache數據結構圖

Guava Cache的架構設計來源於ConcurrentHashMap的靈感,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量數據的緩存,但是如果結果可能隨時間改變或者是希望存儲的數據空間可控的話,自己實現這種數據結構還是有必要的。

Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細粒度鎖,在保證線程安全的同時,支持高併發場景需求。Cache類似於Map,它是存儲鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等演算法邏輯,需要一些額外信息來實現這些操作。對此,根據面向對象思想,需要做方法與數據的關聯封裝。如圖6示cache的內存數據模型,可以看到,使用ReferenceEntry介面來封裝一個鍵值對,而用ValueReference來封裝Value值,之所以用Reference命令,是因為Cache要支持WeakReference Key和SoftReference、WeakReference value。

圖6 緩存數據流轉圖

ReferenceEntry是對一個鍵值對節點的抽象,它包含了key和值的ValueReference抽象類,Cache由多個Segment組成,而每個Segment包含一個ReferenceEntry數組,每個ReferenceEntry數組項都是一條ReferenceEntry鏈,且一個ReferenceEntry包含key、hash、valueReference、next欄位。除了在ReferenceEntry數組項中組成的鏈,在一個Segment中,所有ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue)(後面會介紹鏈的作用)。ReferenceEntry可以是強引用類型的key,也可以WeakReference類型的key,為了減少內存使用量,還可以根據是否配置了expireAfterWrite、expireAfterAccess、maximumSize來決定是否需要write鏈和access鏈確定要創建的具體Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。

對於ValueReference,因為Cache支持強引用的Value、SoftReference Value以及WeakReference Value,因而它對應三個實現類:StrongValueReference、SoftValueReference、WeakValueReference。為了支持動態載入機制,它還有一個LoadingValueReference,在需要動態載入一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經在載入了,如果其他線程也要查詢該key對應的值,就能得到該引用,並且等待改值載入完成,從而保證該值只被載入一次,在該值載入完成後,將LoadingValueReference替換成其他ValueReference類型。ValueReference對象中會保留對ReferenceEntry的引用,這是因為在Value因為WeakReference、SoftReference被回收時,需要使用其key將對應的項從Segment的table中移除。

WriteQueue和AccessQueue:為了實現最近最少使用演算法,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個雙向鏈表,通過ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue鏈接而成,但是以Queue的形式表達。WriteQueue和AccessQueue都是自定義了offer、add(直接調用offer)、remove、poll等操作的邏輯,對offer(add)操作,如果是新加的節點,則直接加入到該鏈的結尾,如果是已存在的節點,則將該節點鏈接的鏈尾;對remove操作,直接從該鏈中移除該節點;對poll操作,將頭節點的下一個節點移除,並返回。

了解了cache的整體數據結構后,再來看下針對緩存的相關操作就簡單多了:

Segment中的evict清除策略操作,是在每一次調用操作的開始和結束時觸發清理工作,這樣比一般的緩存另起線程監控清理相比,可以減少開銷,但如果長時間沒有調用方法的話,會導致不能及時的清理釋放內存空間的問題。evict主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是因為WeakReference、SoftReference被垃圾回收時加入的,清理時只需要遍歷整個queue,將對應的項從LocalCache中移除即可,這裡keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要從Cache中移除需要有key,因而ValueReference需要有對ReferenceEntry的引用,這個前面也提到過了。而對後面兩個Queue,只需要檢查是否配置了相應的expire時間,然後從頭開始查找已經expire的Entry,將它們移除即可。

Segment中的put操作:put操作相對比較簡單,首先它需要獲得鎖,然後嘗試做一些清理工作,接下來的邏輯類似ConcurrentHashMap中的rehash,查找位置並注入數據。需要說明的是當找到一個已存在的Entry時,需要先判斷當前的ValueRefernece中的值事實上已經被回收了,因為它們可以是WeakReference、SoftReference類型,如果已經被回收了,則將新值寫入。並且在每次更新時註冊當前操作引起的移除事件,指定相應的原因:COLLECTED、REPLACED等,這些註冊的事件在退出的時候統一調用Cache註冊的RemovalListener,由於事件處理可能會有很長時間,因而這裡將事件處理的邏輯在退出鎖以後才做。最後,在更新已存在的Entry結束后都嘗試著將那些已經expire的Entry移除。另外put操作中還需要更新writeQueue和accessQueue的語義正確性。

Segment帶CacheLoader的get操作:1. 先查找table中是否已存在沒有被回收、也沒有expire的entry,如果找到,並在CacheBuilder中配置了refreshAfterWrite,並且當前時間間隔已經操作這個事件,則重新載入值,否則,直接返回原有的值;2. 如果查找到的ValueReference是LoadingValueReference,則等待該LoadingValueReference載入結束,並返回載入的值;3. 如果沒有找到entry,或者找到的entry的值為null,則加鎖后,繼續在table中查找已存在key對應的entry,如果找到並且對應的entry.isLoading為true,則表示有另一個線程正在載入,因而等待那個線程載入完成,如果找到一個非null值,返回該值,否則創建一個LoadingValueReference,並調用loadSync載入相應的值,在載入完成後,將新載入的值更新到table中,即大部分情況下替換原來的LoadingValueReference。

Guava Cache提供 Builder模式的CacheBuilder生成器來創建緩存的方式,十分方便,並且各個緩存參數的配置設置,類似於函數式編程的寫法,可自行設置各類參數選型。 它提供三種方式載入 到緩存中。分別是:

  • 在構建緩存的時候,使用build方法內部調用CacheLoader方法載入數據;

  • callable 、callback方式載入數據。

圖7 memcached內存結構圖

使用粗暴直接的方式,直接Cache.put 載入數據,但自動載入是首選的,因為它可以更容易的推斷所有緩存內容的一致性。

build生成器的兩種方式都實現了一種邏輯:從緩存中取key的值,如果該值已經緩存過了則返回緩存中的值,如果沒有緩存過可以通過某個方法來獲取這個值,不同的地方在於cacheloader的定義比較寬泛,是針對整個cache定義的,可以認為是統一的根據key值load value的方法,而callable的方式較為靈活,允許你在get的時候指定 load方法。使用示例如下:

/** * CacheLoader */ publicvoidloadingCache { LoadingCache<stringstring=""> graphs =CacheBuilder.newBuilder .maximumSize(1000).build(newCacheLoader<stringstring=""> { @Override

publicStringload(String key) throws Exception { System.out.println("key:"+key); if("key".equals(key)){

return"key return result"; }else{

return"get-if-absent-compute"; } } }); String resultVal =null;

try{ resultVal = graphs.get("key"); }catch(ExecutionException e) { e.printStackTrace; } System.out.println(resultVal); } /** * * Callable */ publicvoidcallablex throws ExecutionException { Cache<stringstring=""> cache = CacheBuilder.newBuilder .maximumSize(1000).build; String result = cache.get("key"newCallable<string> {

publicStringcall {

return"result"; } }); System.out.println(result); }</string></string,></string,></string

總體來看,Guava Cache基於ConcurrentHashMap的優秀設計借鑒,在高併發場景支持和線程安全上都有相應的改進策略,使用Reference引用命令,提升高併發下的數據……訪問速度並保持了GC的可回收,有效節省空間;同時,write鏈和access鏈的設計,能更靈活、高效的實現多種類型的緩存清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;編程式的build生成器管理,讓使用者有更多的自由度,能夠根據不同場景設置合適的模式。

分散式緩存

memcached緩存

memcached是應用較廣的開源remove cache產品之一,它本身其實不提供分散式的解決方案的。在服務端,memcached集群環境實際就是一個個memcached伺服器的堆積,環境搭建較為簡單;cache的分散式主要是在客戶端實現,通過客戶端的路由處理來達到分散式解決方案的目的。客戶端做路由的原理非常簡單,應用伺服器在每次存取某key的value時,通過某種演算法把key映射到某台memcached伺服器nodeA上,因此這個key所有操作都在nodeA上,結構圖如圖7、圖8所示。

memcached客戶端採用一致性hash演算法作為路由策略,如圖8,相對於一般hash(如簡單取模)的演算法,一致性hash演算法除了計算key的hash值外,還會計算每個server對應的hash值,然後將這些hash值映射到一個有限的值域上(比如0~2^32)。通過尋找hash值大於hash(key)的最小server作為存儲該key數據的目標server。如果找不到,則直接把具有最小hash值的server作為目標server。同時,一定程度上,解決了擴容問題,增加或刪除單個節點,對於整個集群來說,不會有大的影響。最近版本,增加了虛擬節點的設計,進一步提升了可用性。

圖8 memcached客戶端路由圖

圖9 memcached一致性hash示例圖

Memcached是一個高效的分散式內存cache,了解memcached的內存管理機制,才能更好的掌握memcached,讓我們可以針對我們數據特點進行調優,讓其更好的為我所用。我們知道memcached僅支持基礎的key-value 鍵值對類型數據存儲。在Memcached內存結構中有兩個非常重要的概念:slab和chunk。

slab是一個內存塊,它是memcached一次申請內存的最小單位。在啟動memcached的時候一般會使用參數-m指定其可用內存,但是並不是在啟動的那一刻所有的內存就全部分配出去了,只有在需要的時候才會去申請,而且每次申請一定是一個slab。Slab的大小固定為1M(1048576 Byte),一個slab由若干個大小相等的chunk組成。每個chunk中都保存了一個item結構體、一對key和value。

雖然在同一個slab中chunk的大小相等的,但是在不同的slab中chunk的大小並不一定相等,在memcached中按照chunk的大小不同,可以把slab分為很多種類(class),默認情況下memcached把slab分為40類(class1~class40),在class 1中,chunk的大小為80位元組,由於一個slab的大小是固定的1048576位元組(1M),因此在class1中最多可以有13107個chunk(也就是這個slab能存最多 13107個小於80位元組的 key-value 數據)。

Memcached內存管理採取預分配、分組管理的方式,分組管理就是我們上面提到的slab class,按照chunk的大小slab被分為很多種類。內存預分配過程是怎樣的呢?向memcached添加一個item時候,memcached首先會根據item的大小,來選擇最合適的slab class:例如item的大小為190位元組,默認情況下class 4的chunk大小為160位元組顯然不合適,class 5的chunk大小為200位元組,大於190位元組,因此該item將放在class 5中(顯然這裡會有10位元組的浪費是不可避免的),計算好所要放入的chunk之後,memcached會去檢查該類大小的chunk還有沒有空閑的,如果沒有,將會申請1M(1個slab)的空間並劃分為該種類chunk。例如我們第一次向memcached中放入一個190位元組的item時,memcached會產生一個slab class 2(也叫一個page),並會用去一個chunk,剩餘5241個chunk供下次有適合大小item時使用,當我們用完這所有的5242個chunk之後,下次再有一個在160~200位元組之間的item添加進來時,memcached會再次產生一個class 5的slab(這樣就存在了2個pages)。

總結來看,memcached內存管理需要注意的幾個方面:

  • hunk是在page裡面劃分的,而page固定為1m,所以chunk最大不能超過1m。

  • chunk實際佔用內存要加48B,因為chunk數據結構本身需要佔用48B。

  • 如果用戶數據大於1m,則memcached會將其切割,放到多個chunk內。

  • 已分配出去的page不能回收。

對於key-value 信息,最好不要超過1m的大小;同時信息長度最好相對是比較均衡穩定的,這樣能夠保障最大限度的使用內存;同時,memcached採用的LRU清理策略,合理甚至過期時間,提高命中率。

無特殊場景下,key-value能滿足需求的前提下,使用memcached分散式集群是較好的選擇,搭建與操作使用都比較簡單;分散式集群在單點故障時,隻影響小部分數據異常,目前還可以通過Magent緩存代理模式,做單點備份,提升高可用;整個緩存都是基於內存的,因此響應時間是很快,不需要額外的序列化、反序列化的程序,但同時由於基於內存,數據沒有持久化,集群故障重啟數據無法恢復。高版本的memcached已經支持CAS模式的原子操作,可以低成本的解決併發控制問題。

……

閱讀原文,查看全部內容。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦