分布式之MemCache詳細(xì)解讀
MemCache是什么
MemCache是一個(gè)自由、源碼開放、高性能、分布式的分布式內(nèi)存對(duì)象緩存系統(tǒng),用于動(dòng)態(tài)Web應(yīng)用以減輕數(shù)據(jù)庫(kù)的負(fù)載。它通過(guò)在內(nèi)存中緩存數(shù)據(jù)和對(duì)象來(lái)減少讀取數(shù)據(jù)庫(kù)的次數(shù),從而提高了網(wǎng)站訪問(wèn)的速度。MemCaChe是一個(gè)存儲(chǔ)鍵值對(duì)的HashMap,在內(nèi)存中對(duì)任意的數(shù)據(jù)(比如字符串、對(duì)象等)所使用的key-value存儲(chǔ),數(shù)據(jù)可以來(lái)自數(shù)據(jù)庫(kù)調(diào)用、API調(diào)用,或者頁(yè)面渲染的結(jié)果。MemCache設(shè)計(jì)理念就是小而強(qiáng)大,它簡(jiǎn)單的設(shè)計(jì)促進(jìn)了快速部署、易于開發(fā)并解決面對(duì)大規(guī)模的數(shù)據(jù)緩存的許多難題,而所開放的API使得MemCache能用于Java、C/C++/C#、Perl、Python、PHP、Ruby等大部分流行的程序語(yǔ)言。
另外,說(shuō)一下MemCache和MemCached的區(qū)別:
1、MemCache是項(xiàng)目的名稱
2、MemCached是MemCache服務(wù)器端可以執(zhí)行文件的名稱
MemCache的官方網(wǎng)站為http://memcached.org/
MemCache訪問(wèn)模型
為了加深理解,我模仿著原阿里技術(shù)專家李智慧老師《大型網(wǎng)站技術(shù)架構(gòu) 核心原理與案例分析》一書MemCache部分,自己畫了一張圖:
特別澄清一個(gè)問(wèn)題,MemCache雖然被稱為"分布式緩存",但是MemCache本身完全不具備分布式的功能,MemCache集群之間不會(huì)相互通信(與之形成對(duì)比的,比如JBoss Cache,某臺(tái)服務(wù)器有緩存數(shù)據(jù)更新時(shí),會(huì)通知集群中其他機(jī)器更新緩存或清除緩存數(shù)據(jù)),所謂的"分布式",完全依賴于客戶端程序的實(shí)現(xiàn),就像上面這張圖的流程一樣。
同時(shí)基于這張圖,理一下MemCache一次寫緩存的流程:
1、應(yīng)用程序輸入需要寫緩存的數(shù)據(jù)
2、API將Key輸入路由算法模塊,路由算法根據(jù)Key和MemCache集群服務(wù)器列表得到一臺(tái)服務(wù)器編號(hào)
3、由服務(wù)器編號(hào)得到MemCache及其的ip地址和端口號(hào)
4、API調(diào)用通信模塊和指定編號(hào)的服務(wù)器通信,將數(shù)據(jù)寫入該服務(wù)器,完成一次分布式緩存的寫操作
讀緩存和寫緩存一樣,只要使用相同的路由算法和服務(wù)器列表,只要應(yīng)用程序查詢的是相同的Key,MemCache客戶端總是訪問(wèn)相同的客戶端去讀取數(shù)據(jù),只要服務(wù)器中還緩存著該數(shù)據(jù),就能保證緩存命中。
這種MemCache集群的方式也是從分區(qū)容錯(cuò)性的方面考慮的,假如Node2宕機(jī)了,那么Node2上面存儲(chǔ)的數(shù)據(jù)都不可用了,此時(shí)由于集群中Node0和Node1還存在,下一次請(qǐng)求Node2中存儲(chǔ)的Key值的時(shí)候,肯定是沒(méi)有命中的,這時(shí)先從數(shù)據(jù)庫(kù)中拿到要緩存的數(shù)據(jù),然后路由算法模塊根據(jù)Key值在Node0和Node1中選取一個(gè)節(jié)點(diǎn),把對(duì)應(yīng)的數(shù)據(jù)放進(jìn)去,這樣下一次就又可以走緩存了,這種集群的做法很好,但是缺點(diǎn)是成本比較大。
一致性Hash算法
從上面的圖中,可以看出一個(gè)很重要的問(wèn)題,就是對(duì)服務(wù)器集群的管理,路由算法至關(guān)重要,就和負(fù)載均衡算法一樣,路由算法決定著究竟該訪問(wèn)集群中的哪臺(tái)服務(wù)器,先看一個(gè)簡(jiǎn)單的路由算法。
1、余數(shù)Hash
比方說(shuō),字符串str對(duì)應(yīng)的HashCode是50、服務(wù)器的數(shù)目是3,取余數(shù)得到2,str對(duì)應(yīng)節(jié)點(diǎn)Node2,所以路由算法把str路由到Node2服務(wù)器上。由于HashCode隨機(jī)性比較強(qiáng),所以使用余數(shù)Hash路由算法就可以保證緩存數(shù)據(jù)在整個(gè)MemCache服務(wù)器集群中有比較均衡的分布。
如果不考慮服務(wù)器集群的伸縮性(什么是伸縮性,請(qǐng)參見大型網(wǎng)站架構(gòu)學(xué)習(xí)筆記),那么余數(shù)Hash算法幾乎可以滿足絕大多數(shù)的緩存路由需求,但是當(dāng)分布式緩存集群需要擴(kuò)容的時(shí)候,就難辦了。
就假設(shè)MemCache服務(wù)器集群由3臺(tái)變?yōu)?臺(tái)吧,更改服務(wù)器列表,仍然使用余數(shù)Hash,50對(duì)4的余數(shù)是2,對(duì)應(yīng)Node2,但是str原來(lái)是存在Node1上的,這就導(dǎo)致了緩存沒(méi)有命中。如果這么說(shuō)不夠明白,那么不妨舉個(gè)例子,原來(lái)有HashCode為0~19的20個(gè)數(shù)據(jù),那么:
現(xiàn)在我擴(kuò)容到4臺(tái),加粗標(biāo)紅的表示命中:
如果我擴(kuò)容到20+的臺(tái)數(shù),只有前三個(gè)HashCode對(duì)應(yīng)的Key是命中的,也就是15%。當(dāng)然這只是個(gè)簡(jiǎn)單例子,現(xiàn)實(shí)情況肯定比這個(gè)復(fù)雜得多,不過(guò)足以說(shuō)明,使用余數(shù)Hash的路由算法,在擴(kuò)容的時(shí)候會(huì)造成大量的數(shù)據(jù)無(wú)法正確命中(其實(shí)不僅僅是無(wú)法命中,那些大量的無(wú)法命中的數(shù)據(jù)還在原緩存中在被移除前占據(jù)著內(nèi)存)。這個(gè)結(jié)果顯然是無(wú)法接受的,在網(wǎng)站業(yè)務(wù)中,大部分的業(yè)務(wù)數(shù)據(jù)度操作請(qǐng)求上事實(shí)上是通過(guò)緩存獲取的,只有少量讀操作會(huì)訪問(wèn)數(shù)據(jù)庫(kù),因此數(shù)據(jù)庫(kù)的負(fù)載能力是以有緩存為前提而設(shè)計(jì)的。當(dāng)大部分被緩存了的數(shù)據(jù)因?yàn)榉?wù)器擴(kuò)容而不能正確讀取時(shí),這些數(shù)據(jù)訪問(wèn)的壓力就落在了數(shù)據(jù)庫(kù)的身上,這將大大超過(guò)數(shù)據(jù)庫(kù)的負(fù)載能力,嚴(yán)重的可能會(huì)導(dǎo)致數(shù)據(jù)庫(kù)宕機(jī)。
這個(gè)問(wèn)題有解決方案,解決步驟為:
(1)在網(wǎng)站訪問(wèn)量低谷,通常是深夜,技術(shù)團(tuán)隊(duì)加班,擴(kuò)容、重啟服務(wù)器
(2)通過(guò)模擬請(qǐng)求的方式逐漸預(yù)熱緩存,使緩存服務(wù)器中的數(shù)據(jù)重新分布
2、一致性Hash算法
一致性Hash算法通過(guò)一個(gè)叫做一致性Hash環(huán)的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)Key到緩存服務(wù)器的Hash映射,看一下我自己畫的一張圖:
具體算法過(guò)程為:先構(gòu)造一個(gè)長(zhǎng)度為232的整數(shù)環(huán)(這個(gè)環(huán)被稱為一致性Hash環(huán)),根據(jù)節(jié)點(diǎn)名稱的Hash值(其分布為[0, 232-1])將緩存服務(wù)器節(jié)點(diǎn)放置在這個(gè)Hash環(huán)上,然后根據(jù)需要緩存的數(shù)據(jù)的Key值計(jì)算得到其Hash值(其分布也為[0, 232-1]),然后在Hash環(huán)上順時(shí)針查找距離這個(gè)Key值的Hash值最近的服務(wù)器節(jié)點(diǎn),完成Key到服務(wù)器的映射查找。
就如同圖上所示,三個(gè)Node點(diǎn)分別位于Hash環(huán)上的三個(gè)位置,然后Key值根據(jù)其HashCode,在Hash環(huán)上有一個(gè)固定位置,位置固定下之后,Key就會(huì)順時(shí)針去尋找離它最近的一個(gè)Node,把數(shù)據(jù)存儲(chǔ)在這個(gè)Node的MemCache服務(wù)器中。使用Hash環(huán)如果加了一個(gè)節(jié)點(diǎn)會(huì)怎么樣,看一下:
看到我加了一個(gè)Node4節(jié)點(diǎn),只影響到了一個(gè)Key值的數(shù)據(jù),本來(lái)這個(gè)Key值應(yīng)該是在Node1服務(wù)器上的,現(xiàn)在要去Node4了。采用一致性Hash算法,的確也會(huì)影響到整個(gè)集群,但是影響的只是加粗的那一段而已,相比余數(shù)Hash算法影響了遠(yuǎn)超一半的影響率,這種影響要小得多。更重要的是,集群中緩存服務(wù)器節(jié)點(diǎn)越多,增加節(jié)點(diǎn)帶來(lái)的影響越小,很好理解。換句話說(shuō),隨著集群規(guī)模的增大,繼續(xù)命中原有緩存數(shù)據(jù)的概率會(huì)越來(lái)越大,雖然仍然有小部分?jǐn)?shù)據(jù)緩存在服務(wù)器中不能被讀到,但是這個(gè)比例足夠小,即使訪問(wèn)數(shù)據(jù)庫(kù),也不會(huì)對(duì)數(shù)據(jù)庫(kù)造成致命的負(fù)載壓力。
至于具體應(yīng)用,這個(gè)長(zhǎng)度為232的一致性Hash環(huán)通常使用二叉查找樹實(shí)現(xiàn),至于二叉查找樹,就是算法的問(wèn)題了,可以自己去查詢相關(guān)資料。
MemCache實(shí)現(xiàn)原理
首先要說(shuō)明一點(diǎn),MemCache的數(shù)據(jù)存放在內(nèi)存中,存放在內(nèi)存中個(gè)人認(rèn)為意味著幾點(diǎn):
1、訪問(wèn)數(shù)據(jù)的速度比傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)要快,因?yàn)镺racle、MySQL這些傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)為了保持?jǐn)?shù)據(jù)的持久性,數(shù)據(jù)存放在硬盤中,IO操作速度慢
2、MemCache的數(shù)據(jù)存放在內(nèi)存中同時(shí)意味著只要MemCache重啟了,數(shù)據(jù)就會(huì)消失
3、既然MemCache的數(shù)據(jù)存放在內(nèi)存中,那么勢(shì)必受到機(jī)器位數(shù)的限制,這個(gè)之前的文章寫過(guò)很多次了,32位機(jī)器最多只能使用2GB的內(nèi)存空間,64位機(jī)器可以認(rèn)為沒(méi)有上限
然后我們來(lái)看一下MemCache的原理,MemCache最重要的莫不是內(nèi)存分配的內(nèi)容了,MemCache采用的內(nèi)存分配方式是固定空間分配,還是自己畫一張圖說(shuō)明:
這張圖片里面涉及了slab_class、slab、page、chunk四個(gè)概念,它們之間的關(guān)系是:
1、MemCache將內(nèi)存空間分為一組slab
2、每個(gè)slab下又有若干個(gè)page,每個(gè)page默認(rèn)是1M,如果一個(gè)slab占用100M內(nèi)存的話,那么這個(gè)slab下應(yīng)該有100個(gè)page
3、每個(gè)page里面包含一組chunk,chunk是真正存放數(shù)據(jù)的地方,同一個(gè)slab里面的chunk的大小是固定的
4、有相同大小chunk的slab被組織在一起,稱為slab_class
MemCache內(nèi)存分配的方式稱為allocator,slab的數(shù)量是有限的,幾個(gè)、十幾個(gè)或者幾十個(gè),這個(gè)和啟動(dòng)參數(shù)的配置相關(guān)。
MemCache中的value過(guò)來(lái)存放的地方是由value的大小決定的,value總是會(huì)被存放到與chunk大小最接近的一個(gè)slab中,比如slab[1]的chunk大小為80字節(jié)、slab[2]的chunk大小為100字節(jié)、slab[3]的chunk大小為128字節(jié)(相鄰slab內(nèi)的chunk基本以1.25為比例進(jìn)行增長(zhǎng),MemCache啟動(dòng)時(shí)可以用-f指定這個(gè)比例),那么過(guò)來(lái)一個(gè)88字節(jié)的value,這個(gè)value將被放到2號(hào)slab中。放slab的時(shí)候,首先slab要申請(qǐng)內(nèi)存,申請(qǐng)內(nèi)存是以page為單位的,所以在放入第一個(gè)數(shù)據(jù)的時(shí)候,無(wú)論大小為多少,都會(huì)有1M大小的page被分配給該slab。申請(qǐng)到page后,slab會(huì)將這個(gè)page的內(nèi)存按chunk的大小進(jìn)行切分,這樣就變成了一個(gè)chunk數(shù)組,最后從這個(gè)chunk數(shù)組中選擇一個(gè)用于存儲(chǔ)數(shù)據(jù)。
如果這個(gè)slab中沒(méi)有chunk可以分配了怎么辦,如果MemCache啟動(dòng)沒(méi)有追加-M(禁止LRU,這種情況下內(nèi)存不夠會(huì)報(bào)Out Of Memory錯(cuò)誤),那么MemCache會(huì)把這個(gè)slab中最近最少使用的chunk中的數(shù)據(jù)清理掉,然后放上最新的數(shù)據(jù)。針對(duì)MemCache的內(nèi)存分配及回收算法,總結(jié)三點(diǎn):
1、MemCache的內(nèi)存分配chunk里面會(huì)有內(nèi)存浪費(fèi),88字節(jié)的value分配在128字節(jié)(緊接著大的用)的chunk中,就損失了30字節(jié),但是這也避免了管理內(nèi)存碎片的問(wèn)題
2、MemCache的LRU算法不是針對(duì)全局的,是針對(duì)slab的
3、應(yīng)該可以理解為什么MemCache存放的value大小是限制的,因?yàn)橐粋€(gè)新數(shù)據(jù)過(guò)來(lái),slab會(huì)先以page為單位申請(qǐng)一塊內(nèi)存,申請(qǐng)的內(nèi)存最多就只有1M,所以value大小自然不能大于1M了
再總結(jié)MemCache的特性和限制
上面已經(jīng)對(duì)于MemCache做了一個(gè)比較詳細(xì)的解讀,這里再次總結(jié)MemCache的限制和特性:
1、MemCache中可以保存的item數(shù)據(jù)量是沒(méi)有限制的,只要內(nèi)存足夠
2、MemCache單進(jìn)程在32位機(jī)中最大使用內(nèi)存為2G,這個(gè)之前的文章提了多次了,64位機(jī)則沒(méi)有限制
3、Key最大為250個(gè)字節(jié),超過(guò)該長(zhǎng)度無(wú)法存儲(chǔ)
4、單個(gè)item最大數(shù)據(jù)是1MB,超過(guò)1MB的數(shù)據(jù)不予存儲(chǔ)
5、MemCache服務(wù)端是不安全的,比如已知某個(gè)MemCache節(jié)點(diǎn),可以直接telnet過(guò)去,并通過(guò)flush_all讓已經(jīng)存在的鍵值對(duì)立即失效
6、不能夠遍歷MemCache中所有的item,因?yàn)檫@個(gè)操作的速度相對(duì)緩慢且會(huì)阻塞其他的操作
7、MemCache的高性能源自于兩階段哈希結(jié)構(gòu):第一階段在客戶端,通過(guò)Hash算法根據(jù)Key值算出一個(gè)節(jié)點(diǎn);第二階段在服務(wù)端,通過(guò)一個(gè)內(nèi)部的Hash算法,查找真正的item并返回給客戶端。從實(shí)現(xiàn)的角度看,MemCache是一個(gè)非阻塞的、基于事件的服務(wù)器程序
8、MemCache設(shè)置添加某一個(gè)Key值的時(shí)候,傳入expiry為0表示這個(gè)Key值永久有效,這個(gè)Key值也會(huì)在30天之后失效,見memcache.c的源代碼:
這個(gè)失效的時(shí)間是mem
cache源碼里面寫的,開發(fā)者沒(méi)有辦法改變MemCache的Key值失效時(shí)間為30天這個(gè)限制
MemCache指令匯總
上面說(shuō)過(guò),已知MemCache的某個(gè)節(jié)點(diǎn),直接telnet過(guò)去,就可以使用各種命令操作MemCache了,下面看下MemCache有哪幾種命令:
stats指令解讀
stats是一個(gè)比較重要的指令,用于列出當(dāng)前MemCache服務(wù)器的狀態(tài),拿一組數(shù)據(jù)舉個(gè)例子:
這些參數(shù)反映著MemCache服務(wù)器的基本信息,它們的意思是:
參 數(shù) 名作 用pidMemCache服務(wù)器的進(jìn)程id uptime服務(wù)器已經(jīng)運(yùn)行的秒數(shù)time服務(wù)器當(dāng)前的UNIX時(shí)間戳 versionMemCache版本 pointer_size當(dāng)前操作系統(tǒng)指針大小,反映了操作系統(tǒng)的位數(shù),64意味著MemCache服務(wù)器是64位的 rusage_user進(jìn)程的累計(jì)用戶時(shí)間 rusage_system 進(jìn)程的累計(jì)系統(tǒng)時(shí)間 curr_connections 當(dāng)前打開著的連接數(shù)total_connections 當(dāng)服務(wù)器啟動(dòng)以后曾經(jīng)打開過(guò)的連接數(shù)connection_structures 服務(wù)器分配的連接構(gòu)造數(shù) cmd_get get命令總請(qǐng)求次數(shù) cmd_setset命令總請(qǐng)求次數(shù) cmd_flush flush_all命令總請(qǐng)求次數(shù) get_hits 總命中次數(shù),重要,緩存最重要的參數(shù)就是緩存命中率,以get_hits / (get_hits + get_misses)表示,比如這個(gè)緩存命中率就是99.2% get_misses 總未命中次數(shù) auth_cmds 認(rèn)證命令的處理次數(shù) auth_errors 認(rèn)證失敗的處理次數(shù) bytes_read 總讀取的字節(jié)數(shù)bytes_written 總發(fā)送的字節(jié)數(shù) limit_maxbytes分配給MemCache的內(nèi)存大小(單位為字節(jié)) accepting_conns 是否已經(jīng)達(dá)到連接的最大值,1表示達(dá)到,0表示未達(dá)到listen_disabled_num 統(tǒng)計(jì)當(dāng)前服務(wù)器連接數(shù)曾經(jīng)達(dá)到最大連接的次數(shù),這個(gè)次數(shù)應(yīng)該為0或者接近于0,如果這個(gè)數(shù)字不斷增長(zhǎng), 就要小心我們的服務(wù)了threads 當(dāng)前MemCache總線程數(shù),由于MemCache的線程是基于事件驅(qū)動(dòng)機(jī)制的,因此不會(huì)一個(gè)線程對(duì)應(yīng)一個(gè)用戶請(qǐng)求 bytes 當(dāng)前服務(wù)器存儲(chǔ)的items總字節(jié)數(shù)current_items 當(dāng)前服務(wù)器存儲(chǔ)的items總數(shù)量 total_items 自服務(wù)器啟動(dòng)以后存儲(chǔ)的items總數(shù)量
stats slab指令解讀
如果對(duì)上面的MemCache存儲(chǔ)機(jī)制比較理解了,那么我們來(lái)看一下各個(gè)slab中的信息,還是拿一組數(shù)據(jù)舉個(gè)例子:
首先看到,第二個(gè)slab的chunk_size(144)/第一個(gè)slab的chunk_size(96)=1.5,第三個(gè)slab的chunk_size(216)/第二個(gè)slab的chunk_size(144)=1.5,可以確定這個(gè)MemCache的增長(zhǎng)因子是1.5,chunk_size以1.5倍增長(zhǎng)。然后解釋下字段的含義:
看到這個(gè)命令的輸出量很大,所有信息都很有作用。舉個(gè)例子吧,比如第一個(gè)slab中使用的chunks很少,第二個(gè)slab中使用的chunks很多,這時(shí)就可以考慮適當(dāng)增大MemCache的增長(zhǎng)因子了,讓一部分?jǐn)?shù)據(jù)落到第一個(gè)slab中去,適當(dāng)平衡兩個(gè)slab中的內(nèi)存,避免空間浪費(fèi)。
MemCache的Java實(shí)現(xiàn)實(shí)例
講了這么多,作為一個(gè)Java程序員,怎么能不寫寫MemCache的客戶端的實(shí)現(xiàn)呢?MemCache的客戶端有很多第三方j(luò)ar包提供了實(shí)現(xiàn),其中比較好的當(dāng)屬XMemCached了,XMemCached具有效率高、IO非阻塞、資源耗費(fèi)少、支持完整的協(xié)議、允許設(shè)置節(jié)點(diǎn)權(quán)重、允許動(dòng)態(tài)增刪節(jié)點(diǎn)、支持JMX、支持與Spring框架集成、使用連接池、可擴(kuò)展性好等諸多優(yōu)點(diǎn),因而被廣泛使用。這里利用XMemCache寫一個(gè)簡(jiǎn)單的MemCache客戶單實(shí)例,也沒(méi)有驗(yàn)證過(guò),純屬拋磚引玉:
