Memcached是一種集中式Cache,支持分佈式橫向擴展。這裏須要解釋說明一下,不少開發者以爲Memcached是一種分佈式緩存系統, 可是其實Memcached服務端自己是單實例的,只是在客戶端實現過程當中能夠根據存儲的主鍵作分區存儲,而這個區就是Memcached服務端的一個或 者多個實例,若是將客戶端也囊括到Memcached中,那麼能夠部分概念上說是集中式的。其實回顧一下集中式的構架,無非兩種狀況:一是節點均衡的網狀 (JBoss Tree Cache),利用JGroup的多播通訊機制來同步數據;二是Master-Slaves模式(分佈式文件系統),由Master來管理Slave,比 如如何選擇Slave,如何遷移數據等都是由Master來完成,可是Master自己也存在單點問題。下面再總結幾個它的特色來理解一下其優勢和限制。 java
內存存儲:不言而喻,速度快,但對於內存的要求高。這種狀況對CPU要求很低,因此經常採用將 Memcached服務端和一些CPU高消耗內存、低消耗應用部署在一塊兒。(咱們的某個產品正好有這樣的環境,咱們的接口服務器有多臺,它們對CPU要求 很高——緣由在於WS-Security的使用,可是對於內存要求很低,所以能夠用做Memcached的服務端部署機器)。 算法
集中式緩存(Cache):避開了分佈式緩存的傳播問題,可是須要非單點來保證其可靠性,這個就是後面集成中所做的集羣(Cluster)工做,能夠將多個Memcached做爲一個虛擬的集羣,同時對於集羣的讀寫和普通的Memcached的讀寫性能沒有差異。 數據庫
分佈式擴展:Memcached很突出的一個優勢就是採用了可分佈式擴展的模式。能夠將部署在一臺機器上的多個 Memcached服務端或者部署在多個機器上的Memcached服務端組成一個虛擬的服務端,對於調用者來講則是徹底屏蔽和透明的。這樣作既提升了單 機的內存利用率,也提供了向上擴容(Scale Out)的方式。 服務器
Socket通訊:這兒須要注意傳輸內容的大小和序列化的問題,雖然Memcached一般會被放置到內網做爲 緩存,Socket傳輸速率應該比較高(當前支持TCP和UDP兩種模式,同時根據客戶端的不一樣能夠選擇使用NIO的同步或者異步調用方式),可是序列化 成本和帶寬成本仍是須要注意。這裏也提一下序列化,對於對象序列化的性能每每讓你們頭痛,可是若是對於同一類的Class對象序列化傳輸,第一次序列化時 間比較長,後續就會優化,也就是說序列化最大的消耗不是對象序列化,而是類的序列化。若是穿過去的只是字符串,這種狀況是最理想的,省去了序列化的操做, 所以在Memcached中保存的每每是較小的內容。 網絡
特殊的內存分配機制:首先要說明的是Memcached支持最大的存儲對象爲1M。它的內存分配比較特殊,可是 這樣的分配方式其實也是基於性能考慮的,簡單的分配機制能夠更容易回收再分配,節省對CPU的使用。這裏用一個酒窖作比來講明這種內存分配機制,首先在 Memcached啓動的時候能夠經過參數來設置使用的全部內存——酒窖,而後在有酒進入的時候,首先申請(一般是1M)的空間,用來建酒架,而酒架根據 這個酒瓶的大小將本身分割爲多個小格子來安放酒瓶,並將一樣大小範圍內的酒瓶都放置在一類酒架上面。例如20釐米半徑的酒瓶放置在能夠容納20-25釐米 的酒架A上,30釐米半徑的酒瓶就放置在容納25-30釐米的酒架B上。回收機制也很簡單,首先新酒入庫,看看酒架是否有能夠回收的地方,若是有就直接使 用,若是沒有則申請新的地方,若是申請不到,就採用配置的過時策略。從這個特色來看,若是要放的內容大小十分離散,同時大小比例相差梯度很明顯的話,那麼 可能對於空間使用來講效果很差,由於極可能在酒架A上就放了一瓶酒,但卻佔用掉了一個酒架的位置。 多線程
緩存機制簡單:有時候不少開源項目作的面面俱到,但到最後由於過於注重一些非必要的功能而拖累了性能,這裏提到 的就是Memcached的簡單性。首先它沒有什麼同步,消息分發,兩階段提交等等,它就是一個很簡單的緩存,把東西放進去,而後能夠取出來,若是發現所 提供的Key沒有命中,那麼就很直白地告訴你,你這個Key沒有任何對應的東西在緩存裏,去數據庫或者其餘地方取;當你在外部數據源取到的時候,能夠直接 將內容置入到緩存中,這樣下次就能夠命中了。這裏介紹一下同步這些數據的兩種方式:一種是在你修改了之後馬上更新緩存內容,這樣就會即時生效;另外一種是說 允許有失效時間,到了失效時間,天然就會將內容刪除,此時再去取的時候就不會命中,而後再次將內容置入緩存,用來更新內容。後者用在一些實時性要求不高, 寫入不頻繁的狀況。 併發
客戶端的重要性:Memcached是用C寫的一個服務端,客戶端沒有規定,反正是Socket傳輸,只要語言 支持Socket通訊,經過Command的簡單協議就能夠通訊。可是客戶端設計的合理十分重要,同時也給使用者提供了很大的空間去擴展和設計客戶端來滿 足各類場景的須要,包括容錯、權重、效率、特殊的功能性需求和嵌入框架等等。 框架
幾個應用點:小對象的緩存(用戶的Token、權限信息、資源信息);小的靜態資源緩存;SQL結果的緩存(這部分若是用的好,性能提升會至關大,同時因爲Memcached自身提供向上擴容,那麼對於數據庫向上擴容的老大難問題無疑是一劑好藥);ESB消息緩存。 異步
MemCached在大型網站被應用得愈來愈普遍,不一樣語言的客戶端也都在官方網站上有提供,可是Java開發者的選擇並很少。因爲如今的 MemCached服務端是用C寫的,所以我這個C不太熟悉的人也就沒有辦法去優化它。固然對於它的內存分配機制等細節仍是有所瞭解,所以在使用的時候也 會十分注意,這些文章在網絡上有不少。這裏我重點介紹一下對於MemCache系統的Java客戶端優化的兩個階段。
第一階段主要是在官方推薦的Java客戶端之一whalin開源實現基礎上作再次封裝。
所以,在每個客戶端中都內置了一個有超時機制的本地緩存(採用Lazy Timeout機制),在獲取數據的時候,首先在本地查詢數據是否存在,若是不存在則再向Memcache發起請求,得到數據之後,將其緩存在本地,並設置有效時間。方法定義以下:
/** * 下降memcache的交互頻繁形成的性能損失,所以採用本地cache結合memcache的方式 * @param key * @param 本地緩存失效時間單位秒 * @return**/ public Object get(String key,int localTTL);
第一階段的封裝基本上已經能夠知足現有的需求,也被本身的項目和其餘產品線所使用,可是不經意的一句話,讓我開始了第二階段的優化。有同事告訴我說 Memcached客戶端的SocketIO代碼裏面有太多的Synchronized(同步),多多少少會影響性能。雖然過去看過這部分代碼,可是當時 只是關注裏面的Hash算法。根據同事所說的回去一看,果真有很多的同步,多是做者當時寫客戶端的時候JDK版本較老的緣故形成的,如今 Concurrent包被普遍應用,所以優化並非一件很難的事情。可是因爲原有Whalin沒有提供擴展的接口,所以不得不將Whalin除了 SockIO,其他所有歸入到封裝過的客戶端的設想,而後改造SockIO部分。
結果也就有了這個放在Google上的開源客戶端:http://code.google.com/p/memcache-client-forjava/。
上面兩部分的工做不管是否提高了性能,可是對於客戶端自己來講都是有意義的,固然提高性能給應用帶來的吸引力更大。這部分細節內容能夠參看代碼實現部分,對於調用者來講徹底沒有任何功能影響,僅僅只是性能。
在這個壓力測試以前,其實已經作過不少次壓力測試了,測試中的數據自己並無衡量Memcached的意義,由於測試是使用我本身的機器,其中性 能、帶寬、內存和網絡IO都不是服務器級別的,這裏僅僅是將使用原有的第三方客戶端和改造後的客戶端做一個比較。場景就是模擬多用戶多線程在同一時間發起 緩存操做,而後記錄下操做的結果。
Client版本在測試中有兩個:2.0和2.2。2.0是封裝調用Whalin Memcached Client 2.0.1版本的客戶端實現。2.2是使用了新SockIO的無第三方依賴的客戶端實現。checkAlive指的是在使用鏈接資源之前是否須要驗證鏈接 資源有效(發送一次請求並接受響應),所以啓用該設置對於性能來講會有很多的影響,不過建議仍是使用這個檢查。
單個緩存服務端實例的各類配置和操做下比較:
緩存配置 | 用戶 | 操做 | 客戶端 版本 | 總耗時(ms) | 單線程耗時(ms) | 提升處理能力百分比 |
checkAlive | 100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
No checkAlive | 100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
7200285 4667239 |
72002 46672 |
+35.2% |
checkAlive | 100 | 1000 put simple obj 2000 get simple obj |
2.0 2.2 |
20385457 11494383 |
203854 114943 |
+43.6% |
No checkAlive | 100 | 1000 put simple obj 2000 get simple obj |
2.0 2.2 |
11259185 7256594 |
112591 72565 |
+35.6% |
checkAlive | 100 | 1000 put complex obj 1000 get complex obj |
2.0 2.2 |
15004906 9501571 |
150049 95015 |
+36.7% |
No checkAlive | 100 | 1000 put complex obj 1000 put complex obj |
2.0 2.2 |
9022578 6775981 |
90225 67759 |
+24.9% |
從上面的壓力測試能夠看出這麼幾點,首先優化SockIO提高了很多性能,其次SockIO優化的是get的性能,對於put沒有太大的做用。原覺得獲取數據越大性能效果提高越明顯,但結果並非這樣。
單個緩存實例和雙緩存實例的測試比較:
緩存配置 | 用戶 | 操做 | 客戶端 版本 | 總耗時(ms) | 單線程耗時(ms) | 提升處理能力百分比 |
One Cache instance checkAlive |
100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
Two Cache instance checkAlive |
100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
13596841 7696684 |
135968 76966 |
+43.4% |
結果顯示,單個客戶端對應多個服務端實例性能提高略高於單客戶端對應單服務端實例。
緩存集羣的測試比較:
緩存配置 | 用戶 | 操做 | 客戶端 版本 | 總耗時(ms) | 單線程耗時(ms) | 提升處理能力百分比 |
No Cluster checkAlive |
100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
Cluster checkAlive |
100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
25044268 8404606 |
250442 84046 |
+66.5% |
這部分和SocketIO優化無關。2.0採用的是向集羣中全部客戶端更新成功之後才返回的策略,2.2採用了異步更新,而且是分佈式客戶端節點獲取的方式來分散壓力,所以提高效率不少。