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

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