注:本篇博客參考於兩本書。
- 《memcached全面剖析》,該書籍市面上應該沒有,我傳到了百度雲盤,鏈接如下:http://pan.baidu.com/s/1qX00Lti
- 《大型網站技術架構:核心原理與案例分析》
前提:
- 本文是基於memcached1.4版本的,之前的版本與該版本在一些地方是不一樣的(eg.《memcached全面剖析》的memcached1.2的內存管理方式就與1.4不同)
- 在看本文之前,最好先看一下memcached在實際開發中怎麼進行操作的,鏈接《第八章 企業項目開發--分佈式緩存memcached》
1、memcached特徵
- 協議簡單(文本協議、二進制協議)
- 基於libevent的事件處理,libevent封裝了Linux的epoll模型的時間處理功能。
- slab存儲模型
- 集羣中服務器間互不通信(在大集羣的情況下,其性能遠超其他同步更新緩存的緩存器,當然小集羣下,memcached的性能也十分優秀)
2、memcached訪問模型
說明:
Xmemcached的具體使用代碼查看"Java企業項目開發實踐"系列博客的《第八章 企業項目開發--分佈式緩存memcached》,下面的解釋會依據該代碼進行。
在上圖中,memcached客戶端假設使用XMemcached
- 服務器列表:在根pom.xml文件中進行了配置
- 路由算法有兩種:(可以在程序中指定)
- 一致性hash算法(推薦)
- 簡單求餘法
- 通信模塊:
- 通信協議:TCP協議
- 序列化協議:二進制協議(推薦)、文本協議
- Memcached API(緩存的增刪改查):在程序中編寫
整個流程:
應用程序(AdminService)調用Memcached API(假設爲add操作),向memcached服務器添加緩存,這時候,程序會首先根據配置的路由算法(假設是一致性hash算法)在服務器列表中選出一臺服務器(假設是node1),之後該API通過序列化協議序列化對象(當然,這個是可無的,eg.value是一個String),並通過TCP協議將將要存儲的key-value對存入相應的服務器。在get時,只要採用的是與add時相同的hash算法,就會選中add時的那一臺服務器。
看完這一段,流程明白了。但是有幾點疑問:
- 兩種路由算法是怎樣實現的?爲什麼使用一致性hash算法
- 緩存到達服務器的時候究竟怎麼存儲?(slab內存模型)
- 當緩存超過一定的容量後,緩存的自動刪除是採用什麼策略,怎樣刪除的?(LRU)
- 兩種序列化協議有什麼優缺點?
3、hash算法
3.1、簡單求餘法
原理步驟:求得key的整數hash值(對於Java對象而言,直接使用其hashCode()方法就好),再除以服務器臺數,獲取餘數,根據該餘數選擇服務器。
注意:如果選擇的服務器無法連接時,會進行rehash,即:將連接次數添加到鍵中,重新計算hash值後,再重新連接。當然可以禁止rehash。
優點:
- 簡單
- hash分散性好(因爲hashCode()的值具有隨機性)
缺點:
- 添加或刪除服務器的時候,緩存的獲取就會出問題了(因爲服務器臺數變了,求餘的時候分母變了,餘數也就可能變了),假設在99臺memcached服務器中又新添加而一臺,則緩存的不命中率是99%,即n/(n+1),n表示原有的服務器。
注意:
- 在XMemcached中仍保留了該算法
- 適用於不需要考慮集羣伸縮性的時候(即機器總數不變)
3.2、一致性hash算法
對於絕大部分系統,集羣的伸縮性是五個非功能需求中比較重要的一個,也就是說必須克服"簡單求餘法"的缺點。
- 原理:先構造一個長度爲0~232的整數環(使用二叉樹構造),根據節點(memcached服務器)名稱的hash值將緩存服務器節點放置在這個hash環上,然後根據需要緩存的數據的key來計算其hash值,然後在hash環上順時針查找距離這個key的hash值最近的緩存服務器節點,完成key到服務器的hash映射查找。
- 如果超過232還找不到,則存在第一臺memcached上(依舊是順時針)
- 存在的問題:當服務器數量比較少的情況下,有可能造成負載不均衡的情況,爲了防止這種情況的發生,使用將物理服務器先虛擬化成多臺虛擬服務器,然後將這些虛擬服務器的hash值放在環上,當客戶端路由到某臺虛擬服務器上時,找到該虛擬服務器所對應的物理服務器即可。
- 一般而言,一臺物理服務器虛擬化爲150臺虛擬服務器最合適,太少會造成負載不均,太多會影響性能
- Memcached採用這樣的算法,在我們新加入服務器或集羣中的某臺服務器宕機時,都不會有太大的影響,只會影響一小段(見下圖),確保了集羣的可用性與伸縮性
注意:
- hash環是一個二叉樹,最後邊葉子與最左邊相連成環
- 整個緩存的查找過程就是找一個剛剛大於等於查找數的最小值
疑問:(這一點沒查到資料)
- 服務器的hash算法是怎樣的
- 計算緩存key的hash算法是否要與服務器的一致,還能不能使用原來的hashCode()
思路:hash算法實際上就是"先將字符串轉化爲整數,然後再將該整數放到相應的服務器上或環上",對於key不用講,我們可以用crc32將字符串的key轉化爲整數,之後放在0~232的環上的一點,對於服務器我們可以採用將"ip:port"這個字符串使用crc32轉化爲整數,之後放在環上(當然這裏我們需要將一個實例"ip:port"虛擬化成一堆虛擬節點,每臺虛擬節點可以使用"ip:port-i"作爲節點名稱,其中i是>0的整數,將每臺虛擬節點的名稱採用crc32算法算出整數放到環上)。
4、slab內存模型
4.1、爲什麼使用slab內存模型?
在最一開始的內存分配與回收是通過malloc和free來處理的,該方式會產生內存碎片,加重內存管理器的負擔,嚴重緩存操作影響效率。
slab模型的出現就是爲了:
- 提高緩存操作效率
- 完全的解決內存碎片問題。
注意:
- 第一個目的:已經實現了(因爲直接定位合適的chunk會很快)
- 第二個目的:採用slab機制依舊會產生內存碎片,或者說成是內存浪費
4.2、slab模型原理
說明:該圖摘自一篇博客(圖中有標記,但是看不清),但是是很久以前摘的了,忘記了。以後找到了,我會標明出處的。
memcached的內存分配就是下面這一句話:採用分組管理、預分配方式。
4.2.1、分組管理
- 分組方式:Memcached將內存空間分爲一組slab,每個slab的大小固定爲1M,每個slab裏又包含一組chunk,同一個slab裏的每個chunk大小相同。根據這些slab中的chunk的大小,將這些slab編號slab class(也就是上圖中的Classes i)。
- 存儲原理:當來一個要存儲的key-value對時,我們查看這個數據的大小,選擇最適合的slab class中的空閒chunk放置該對象。
- 最合適的chunk:即該chunk的大小剛剛大於等於所存儲數據的大小,而比該chunk小一級的大小剛剛比所要存儲的數據小。
以上這種方式會造成內存大量浪費(我認爲這也是內存碎片)。
- 減少內存浪費的方式:預估自己的緩存數據的大小,然後在啓動Memcached時合理的指定參數-f(增長因子)和-n(chunk最小尺寸)來劃分內存大小,根據公式chunk size = 80*f*(n-1)將內存分配爲若干個slab class。
疑問:上邊這個若干到底是多少?
我們可以根據f,n,以及一個slab最大爲1M來確定。(例子,我不舉了,自己想想)
4.2.2、預分配
在啓動Memcached時通過-m參數爲Memcached分配可用內存(假設-m 1024,即分配了1G內存),但是啓動的時候不會把這些內存一次全部分配出去,而是默認先分配若干個slab class(數量取決於-f與-n參數),當其中的一個slab class被用完之後,Memcached就會再次申請1M空間,產生一個該slab class。這一塊兒結合緩存刪除機制中的LRU算法來看。(這一塊如果有誤,請大神幫忙指出來)
5、緩存刪除機制
- memcached不會釋放已分配的內存,記錄超時後,其存儲空間即可重複使用
- memcached內部不會監視緩存是否過期(即memcached不會在過期監視上耗費CPU時間),在get時查看緩存的時間戳,檢查緩存是否過期
- memcached會優先使用已超時的緩存的空間,但是當所有空間都沒有超時,所有內存都已經分配完了,就刪除最近最少使用(LRU)的緩存,將其空間分配給新緩存(注意,假設防止一個100k的數據,而最合適的chunk是112k,假設最合適的chunk全部用完了,這時候就取剩下的內存分配112k chunk的slab,若是剩下的內存頁分配完了,不會使用剛剛大於112k的144k chunk,而是會採用LRU算法刪除最近最少使用的元素,其實這樣的話,就會有一個可能,就是原本112k中的數據還未過期,就有可能被踢出去了,這就是"老數據被踢現象")
注意:第三條與內存分配部分的預分配結合來看。
LRU算法原理:
當某個單元被請求時,維護一個計數器,通過計數器來判斷最近最少被使用的元素被踢出去。
6、兩種序列化協議
- 文本協議:
- XML、JSON
- key的長度爲256字節
- 二進制協議:相較於文本協議
- jdk序列化機制、protobuf
- 不需要文本協議的解析處理,速度更快
- 具有更長的key,理論上最大可使用65536字節長度的key
- 出現在1.4,推薦使用
注意:對於以上兩種協議,自己選擇吧。
- 二進制協議+JDK的序列化機制,那麼由於JDK自己的序列化機制低效,所以在速度上未必會比使用了fastjson的文本協議更快
- 二進制協議+protobuf,速度很快,但是使用起來不太方便
- 文本協議+fastjson
7、部分API
- add:僅當存儲空間中不存在相同key的數據時才保存
- replace:替換。即僅當存儲空間中存在相同的數據時才保存
- set:add+replace。即無論何時都保存
- delete(key, '阻塞時間(秒)')
- 增1、減1操作,做計數器
- get_multi(key1, key2):一次性非同步的同時(即併發的)獲取多個鍵,比循環調用getKIA數十倍
注意點:
- 對於memcached的監視:可以採用"nagios"