Author:文初
Email: wenchu.cenwc@alibaba-inc.com
Blog: http://blog.csdn.net/cenwenchu79/
MemCached Cache在大型網(wǎng)站被應(yīng)用得越來(lái)越廣泛,不同語(yǔ)言的客戶端也都在官方網(wǎng)站上有提供,但是Java的選擇并不多。由于現(xiàn)在的MemCached Cache服務(wù)端是用C寫(xiě)的,因此我這個(gè)C不太熟悉的人也就沒(méi)有辦法去優(yōu)化它,當(dāng)然對(duì)于它的內(nèi)存分配機(jī)制等細(xì)節(jié)還是有所了解,因此在使用的時(shí)候也會(huì)十分注意,這些文章Google一把應(yīng)該也有很多了。這里就說(shuō)說(shuō)對(duì)于MemCache Java客戶端的優(yōu)化的兩個(gè)階段。
First Stage
我也和其他使用Memcached Cache的同學(xué)一樣,看了官方網(wǎng)站的內(nèi)容,然后去下載了whalin memcached Client,后來(lái)Stat的時(shí)候遇到問(wèn)題,就給作者發(fā)了郵件說(shuō)明了情況,作者讓我下載 2.0.1 版本,這個(gè)版本也是比較不錯(cuò)的一個(gè)版本,后續(xù)的封裝也是基于這個(gè)版本之上。
第一階段主要是在whalin的客戶端作了再次封裝。
1. Cache服務(wù)接口化。
定義了IMemCache接口,在應(yīng)用部分僅僅只是使用接口,為將來(lái)替換Cache服務(wù)實(shí)現(xiàn)提供基礎(chǔ)。
2. 使用配置代替代碼初始化客戶端。
通過(guò)配置客戶端和SocketIO Pool屬性,直接交管由CacheManager來(lái)維護(hù)Cache Client Pool的生命周期,方便實(shí)用以及單元測(cè)試。
3. KeySet的實(shí)現(xiàn)。
對(duì)于MemCached來(lái)說(shuō)本身是不提供KeySet的方法的,在接口封裝初期,同事向我提出這個(gè)需求的時(shí)候,我個(gè)人覺(jué)得也是沒(méi)有必要提供,因?yàn)?/span>Cache輪詢是比較低效的,同時(shí)這類場(chǎng)景,往往可以去數(shù)據(jù)源獲取KeySet,而不是從MemCached去獲取。但是SIP的一個(gè)場(chǎng)景的出現(xiàn),讓我不得不去實(shí)現(xiàn)了KeySet。
SIP在作服務(wù)訪問(wèn)頻率控制的時(shí)候需要記錄在控制間隔期內(nèi)的訪問(wèn)次數(shù)和流量,此時(shí)由于是集群,因此數(shù)據(jù)必須放在集中式的存儲(chǔ)或者緩存中,數(shù)據(jù)庫(kù)肯定是撐不住這樣大數(shù)據(jù)量的更新頻率的,因此考慮使用Memcached的很出彩的操作,全局計(jì)數(shù)器(storeCounter,getCounter,inc,dec),但是在檢查計(jì)數(shù)器的時(shí)候如何去獲取當(dāng)前所有的計(jì)數(shù)器,曾考慮使用DB或者文件,但是效率還是問(wèn)題,同時(shí)如果放在一個(gè)字段中并發(fā)還是有問(wèn)題。因此不得不實(shí)現(xiàn)了KeySet,在使用KeySet的時(shí)候有一個(gè)參數(shù),類型是Boolean,這個(gè)字段的存在是因?yàn)椋?/span>Memcached中數(shù)據(jù)的刪除并不是直接刪除,而是標(biāo)注一下,這樣會(huì)導(dǎo)致實(shí)現(xiàn)keySet的時(shí)候取出可能已經(jīng)刪除的數(shù)據(jù),如果對(duì)于數(shù)據(jù)嚴(yán)謹(jǐn)性要求低,速度要求高,那么不需要再去驗(yàn)證key是否真的有效,如果要求key必須正確存在,就需要再多一次的輪詢查找。
4. Cluster的實(shí)現(xiàn)。
Memcached作為集中式Cache,就存在著集中式的致命問(wèn)題:?jiǎn)吸c(diǎn)問(wèn)題,Memcached支持多Instance分布在多臺(tái)機(jī)器上,僅僅只是解決了數(shù)據(jù)全部丟失的問(wèn)題,但是當(dāng)其中一臺(tái)機(jī)器出錯(cuò)以后,還是會(huì)導(dǎo)致部分?jǐn)?shù)據(jù)的丟失,一個(gè)籃子掉在地上還是會(huì)把部分的雞蛋打破。
因此就需要實(shí)現(xiàn)一個(gè)備份機(jī)制,能夠保證Memcached在部分失效以后,數(shù)據(jù)還能夠依然使用,當(dāng)然大家很多時(shí)候都用Cache不命中就去數(shù)據(jù)源獲取的策略,但是在SIP的場(chǎng)景中,如果部分信息找不到就去數(shù)據(jù)庫(kù)查找,那么要把SIP弄垮真的是很容易,因此SIP對(duì)于Memcached中的數(shù)據(jù)認(rèn)為是可信的,因此做Cluster也是必要的。

(1) 應(yīng)用傳入需要操作的key,通過(guò)CacheManager獲取配置在Cluster中的客戶端。
(2) 當(dāng)獲得Cache Client以后,執(zhí)行Cache操作。
(3) A.如果是讀取操作,當(dāng)不能命中時(shí)去集群其他Cache客戶端獲取數(shù)據(jù),如果獲取到數(shù)據(jù),嘗試寫(xiě)入到本次獲得的Cache客戶端,并返回結(jié)果。(達(dá)到數(shù)據(jù)恢復(fù)的作用)
B.如果是更新操作,在本次獲取得Cache客戶端執(zhí)行更新操作以后,立即返回,將更新集群其他機(jī)器命令提交給客戶端的異步更新線程對(duì)列去異步執(zhí)行。(由于如果是根據(jù)key來(lái)獲取Cache,那么異步執(zhí)行不會(huì)影響到此主鍵的查詢操作)
存在的問(wèn)題:如果是設(shè)置了Timeout的數(shù)據(jù),那么在丟失以后被復(fù)制的過(guò)程中就會(huì)變成永久有效的內(nèi)容。
5. LocalCache結(jié)合Memcached使用,提高數(shù)據(jù)獲取效率。
在第一次壓力測(cè)試過(guò)程中,發(fā)現(xiàn)和原先預(yù)料的一樣,Memcached并不是完全無(wú)損失的,Memcached是通過(guò)Socket數(shù)據(jù)交互來(lái)進(jìn)行通信的,因此機(jī)器的帶寬,網(wǎng)絡(luò)IO,Socket連接數(shù)都是制約Memcached發(fā)揮其作用的障礙。Memcache的一個(gè)突出優(yōu)點(diǎn)就是Timeout的設(shè)置,也就是放入進(jìn)去的數(shù)據(jù)可以設(shè)置有效期,自動(dòng)會(huì)失效,這樣對(duì)于一些不敏感的數(shù)據(jù)就可以在一定的容忍時(shí)間內(nèi)不去更新,提高效率。根據(jù)這個(gè)思想,其實(shí)在集群中的每一個(gè)Memcached客戶端也可以使用本地的Cache,來(lái)緩存獲取過(guò)的數(shù)據(jù),設(shè)置一定的失效時(shí)間,來(lái)減少對(duì)于Memcached的訪問(wèn)次數(shù),提高整體性能。
因此,在每一個(gè)客戶端中內(nèi)置了一個(gè)有超時(shí)機(jī)制的本地緩存(采用lazy timeout機(jī)制),在獲取數(shù)據(jù)的時(shí)候,首先在本地查詢數(shù)據(jù)是否存在,如果不存在則再向Memcache發(fā)起請(qǐng)求,獲得數(shù)據(jù)以后,將其緩存在本地,并設(shè)置有效時(shí)間。方法定義如下:
/**
* 降低memcache的交互頻繁造成的性能損失,因此采用本地cache結(jié)合memcache的方式
* @param key
* @param 本地緩存失效時(shí)間單位秒
* @return
*/
public Object get(String key,int localTTL);
Second Stage
第一階段的封裝基本上已經(jīng)可以滿足現(xiàn)有的需求,也被自己的項(xiàng)目和其他產(chǎn)品線使用,但是不經(jīng)意的一句話,讓我開(kāi)始了第二階段的優(yōu)化。單位里面有個(gè)同學(xué)說(shuō)Memcache客戶端里面在SocketIO代碼里面有太多的synchronized,多多少少會(huì)影響性能。雖然過(guò)去看過(guò)這部分代碼,但是當(dāng)時(shí)只是關(guān)注里面的Hash算法,那天回去后一看,果然有不少的synchronized,可能是與客戶端當(dāng)時(shí)寫(xiě)的時(shí)候Jdk版本較早的緣故造成的,現(xiàn)在Concurrent包被廣泛應(yīng)用,因此優(yōu)化并不是一件很難的事情。但是由于原有whalin沒(méi)有提供擴(kuò)展的接口,因此不得不將whalin除了SockIO部分全部納入到封裝過(guò)的客戶端中,然后改造SockIO部分。
因此也有了這個(gè)放在Google上的
open source: http://code.google.com/p/memcache-client-forjava/
一. 優(yōu)化synchronized部分。在原有代碼中SockIO的資源池分成三個(gè)池(普通Map實(shí)現(xiàn)),Free,Busy,Dead,然后根據(jù)SockIO使用情況來(lái)維護(hù)這三個(gè)資源池。
優(yōu)化方式,首先簡(jiǎn)化資源池,只有一個(gè)資源池,設(shè)置一個(gè)狀態(tài)池,在變更資源狀態(tài)的過(guò)程時(shí)僅僅變更資源池中的內(nèi)容。再次,用ConcurrentMap來(lái)替代Map,同時(shí)使用putIfAbsent方法來(lái)簡(jiǎn)化Synchronized,具體的代碼可以看open source的代碼部分。
二. 原以為這優(yōu)化后,效率應(yīng)該會(huì)有很大的提高,但是在初次壓力測(cè)試后發(fā)現(xiàn),并沒(méi)有明顯的提高,看來(lái)有其他地方的耗時(shí)遠(yuǎn)遠(yuǎn)大于連接池資源維護(hù),因此用JProfiler作了性能分析,發(fā)現(xiàn)了最大的一個(gè)瓶頸:read數(shù)據(jù)部分,原有設(shè)計(jì)中讀取數(shù)據(jù)是按照單字節(jié)讀取,然后逐步分析,為的僅僅就是遇到協(xié)議中的分割符可以識(shí)別,但是循環(huán)read單字節(jié)和批量分頁(yè)read性能相差很大,因此內(nèi)置了讀入緩存頁(yè)(可設(shè)置大小),然后再按照協(xié)議的需求去讀取和分析數(shù)據(jù),效率得到了很大的提高。具體的看最后部分的壓力測(cè)試結(jié)果。
上面兩部分的工作不論是否提升了性能,但是對(duì)于客戶端本身來(lái)說(shuō)都是有意義的,當(dāng)然提升性能給應(yīng)用帶來(lái)的吸引力更大。這部分細(xì)節(jié)內(nèi)容可以參看代碼實(shí)現(xiàn)部分,對(duì)于調(diào)用者來(lái)說(shuō)完全沒(méi)有任何功能影響,僅僅只是性能。
壓力測(cè)試
在這個(gè)壓力測(cè)試之前,其實(shí)已經(jīng)做過(guò)很多次壓力測(cè)試了,測(cè)試中的數(shù)據(jù)本身并沒(méi)有衡量Memcached的意義,因?yàn)闇y(cè)試是使用我自己的機(jī)器,性能,帶寬,內(nèi)存,網(wǎng)絡(luò)IO都不是服務(wù)器級(jí)別的,這里僅僅是將使用原有的第三方客戶端和改造后的客戶端作一個(gè)比較。場(chǎng)景就是模擬多用戶多線程在同一時(shí)間發(fā)起Cache操作,然后記錄下操作的結(jié)果。
Client版本在測(cè)試中有兩個(gè):2.0和2.2。2.0是封裝調(diào)用whalin memcached Client 2.0.1版本的客戶端實(shí)現(xiàn)。2.2是使用了新SockIO的無(wú)第三方依賴的客戶端實(shí)現(xiàn)。
checkAlive指的是在使用連接資源以前是否需要驗(yàn)證連接資源有效(發(fā)送一次請(qǐng)求并接受響應(yīng)),因此打開(kāi)對(duì)于性能來(lái)說(shuō)會(huì)有不少的影響,不過(guò)建議還是使用這個(gè)檢查。
One Cache Server instance各種配置和操作下比較:
Cache配置
|
User
|
操作
|
Client 版本
|
總耗時(shí)(ms)
|
單線程耗時(shí)(ms)
|
提高處理能力百分比
|
checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
13242565
|
132425
|
+41.3%
|
2.2
|
7772767
|
77727
|
No checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
7200285
|
72002
|
+35.2%
|
2.2
|
4667239
|
46672
|
checkAlive
|
100
|
1000 put simple obj
2000 get simple obj
|
2.0
|
20385457
|
203854
|
+43.6%
|
2.2
|
11494383
|
114943
|
No checkAlive
|
100
|
1000 put simple obj
2000 get simple obj
|
2.0
|
11259185
|
112591
|
+35.6%
|
2.2
|
7256594
|
72565
|
checkAlive
|
100
|
1000 put complex obj
1000 get complex obj
|
2.0
|
15004906
|
150049
|
+36.7%
|
2.2
|
9501571
|
95015
|
No checkAlive
|
100
|
1000 put complex obj
1000 get complex obj
|
2.0
|
9022578
|
90225
|
+24.9%
|
2.2
|
6775981
|
67759
|
從上面的壓力測(cè)試可以看出這么幾點(diǎn),首先優(yōu)化SockIO提升了不少性能,其次SockIO優(yōu)化的是get的性能,對(duì)于put沒(méi)有太大的作用。原本以為獲取數(shù)據(jù)越大性能效果提升越明顯,但結(jié)果并不是這樣,這部分在這幾天在看看是否還有更加耗時(shí)的部分存在。
One Cache instance 和Two Cache instance的測(cè)試比較:
Cache配置
|
User
|
操作
|
Client 版本
|
總耗時(shí)(ms)
|
單線程耗時(shí)(ms)
|
提高處理能力百分比
|
One Cache instance
checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
13242565
|
132425
|
+41.3%
|
2.2
|
7772767
|
77727
|
Two Cache instance
checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
13596841
|
135968
|
+43.4%
|
2.2
|
7696684
|
76966
|
單個(gè)客戶端對(duì)應(yīng)多個(gè)服務(wù)端實(shí)例性能提升略高于單客戶端對(duì)應(yīng)單服務(wù)端實(shí)例。
Cache Cluster的測(cè)試比較:
Cache配置
|
User
|
操作
|
Client 版本
|
總耗時(shí)(ms)
|
單線程耗時(shí)(ms)
|
提高處理能力百分比
|
No Cluster
checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
13242565
|
132425
|
+41.3%
|
2.2
|
7772767
|
77727
|
Cluster
checkAlive
|
100
|
1000 put simple obj
1000 get simple obj
|
2.0
|
25044268
|
250442
|
+66.5%
|
2.2
|
8404606
|
84046
|
這部分和SocketIO優(yōu)化無(wú)關(guān)。2.0采用的是向集群中所有Client更新成功以后才返回的策略,2.2采用了異步更新,并且是分布式Client Node獲取的方式來(lái)分散壓力,因此提升效率很多。
開(kāi)源:
其實(shí)封裝后的客戶端一直在內(nèi)部使用,現(xiàn)在作了二次優(yōu)化以后,覺(jué)得應(yīng)該Open出來(lái),一來(lái)可以完善自己的客戶端代碼,二來(lái)也可以和更多的同學(xué)交流使用心得。
在Google Code上傳了這應(yīng)用的代碼,范例,說(shuō)明,有興趣的同學(xué)可以下載下來(lái)測(cè)試一下,比較一下現(xiàn)在用的Java Memcached客戶端的使用方便程度以及性能。
open source: http://code.google.com/p/memcache-client-forjava/
期待更多人能夠分享~~~