緩存就是利劍

說是支持1pv/天,也許有點誇張,也是爲了吸引您能點進來,如果您能認真看完相信也不會讓您失望,當然,肯定有很多“高手”會對此會嗤之以鼻,沒關係,有很多眼高手低的人總喜歡評論別人卻從不會看清自己。

 

如果大家真想支持我、支持中國人開源項目,請把該文貼到自己的博客中或者收藏本文,記得包含文檔的下載地址!!!!!!!謝謝。

 

我說的系統主要是構建在hibernate之上的高效數據庫緩存系統,其中包含了分佈式解決方案,該系統已經應用在捨得網上了,沒有發現大問題,本人也相信該系統已經足夠強大,應付數百萬IP/天的應用都不是問題,我這麼說肯定有人會對此表示懷疑,其實系統到底能撐多少IP/天不在於系統本身而是在於使用該系統的人。

 

代碼看上去很簡單,其實卻是兩年經驗的總結,整過過程也遇到了很多難點,最後一一解決了,所以也請各位珍惜他人的勞動成果。本系統非常簡潔易用,主程序BaseManager.java不到1000行代碼,用“精悍”來形容絕對不爲過,1000行代碼卻包含了數據庫對象的緩存、列表和長度的緩存、按字段散列緩存、update延時更新、自動清除列表緩存等功能,用它來實現像論壇、博客、校友錄、交友社區等絕大部分應用網站都足夠了。

 

我在理想狀態下做了壓力測試,在沒有數據庫操作的jsp頁面(捨得網新首頁)裏可以完成2000requests每秒(正常情況可能有1/1000request有數據庫查詢,其餘999/1000都是直接從緩存裏讀取),物品詳情頁每秒可完成3000requests,純靜態html頁面也只能完成7000requests/秒,我對首頁進行了三個小時的壓力測試,完成了24850800requestsjava一點事都沒有,內存沒有上漲。按照2000requests/秒算,一天按15小時計算,那麼每天能完成3600*15*2000=1億零8百萬requests,當然這是理想狀態,實際狀態就算打一折,還能完成1000pv/天,要知道,這只是一個普通13千塊錢買的服務器,內存4GCPU2個,LinuxAS4系統,apache2.0.63/resin2.1.17/jdk6.0的環境。

 

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。現在進入正題。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

 

爲什麼要用緩存?如果問這個問題說明你還是新手,數據庫吞吐量畢竟有限,每秒讀寫5000次了不起了,如果不用緩存,假設一個頁面有100個數據庫操作,50個用戶併發數據庫就歇菜,這樣最多能支撐的pv也就50*3600*15=270萬,而且數據庫服務器累得半死,搞不好什麼時候就累死了。我的這套緩存系統比單獨用memcached做緩存還要強大,相當於在memcached上再做了兩級緩存,大家都知道memcached很強了,但是吞吐量還是有限,每秒20000getput當遇到超大規模的應用時還是會歇菜,本地HashMap每秒可執行上百萬次putget,在這上面損耗的性能幾乎可以忽略不記了。溫馨提示:能不用分佈式的時候就不要用分佈式,非用分佈式的時候再考慮用memcached,我的緩存系統在這方面都已經實現了,改個配置就可以了,有興趣的可以仔細測試測試!

 

一般數據庫緩存在我看來包含四種。第一種:單個對象的緩存(一個對象就是數據庫一行記錄),對於單個對象的緩存,用HashMap就可以了,稍微複雜一點用LRU算法包裝一個HashMap,再複雜一點的分佈式用memcached即可,沒什麼太難的;第二種:列表緩存,就像論壇裏帖子的列表;第三種:長度的緩存,比如一個論壇板塊裏有多少個帖子,這樣才方便實現分頁。第四種:複雜一點的groupsumcount查詢,比如一個論壇裏按點擊數排名的最HOT的帖子列表。第一種比較好實現,後面三種比較困難,似乎沒有通用的解決辦法,我暫時以列表緩存(第二種)爲例分析。

 

mysqlhibernate的底層在做通用的列表緩存時都是根據查詢條件把列表結果緩存起來,但是隻要該表的記錄有任何變化(增加/刪除/修改),列表緩存要全部清除,這樣只要一個表的記錄經常變化(通常情況都會這樣),列表緩存幾乎失效,命中率太低了。

 

本人想了一個辦法改善了列表緩存,當表的記錄有改變時,遍歷所有列表緩存,只有那些被影響到的列表緩存纔會被刪除,而不是直接清除所有列表緩存,比如在一個論壇版(id=1)裏增加了一個帖子,那麼只要清除id=1這個版對應的列表緩存就可以了,版id=2就不用清除了。這樣處理有個好處,可以緩存各種查詢條件(如等於、大於、不等於、小於)的列表緩存,但也有個潛在的性能問題,由於需要遍歷,CPU符合比較大,如果列表緩存最大長度設置成10000,兩個4核的CPU每秒也只能遍歷完300多次,這樣如果每秒有超過300insert/update/delete,系統就吃不消了。

 

在前面兩種解決辦法都不完美的情況下,本人和同事經過幾個星期的思索,總算得出了根據表的某幾個字段做散列的緩存辦法,這種辦法無需大規模遍歷,所以CPU符合非常小,由於這種列表緩存按照字段做了散列,所以命中率極高。思路如下:每個表有3個緩存Mapkey=value鍵值對),第一個Map是對象緩存A,在A中,key是數據庫的idValue是數據庫對象(也就是一行數據);第二個Map是通用列表緩存BB的最大長度一般1000左右,在B中,key是查詢條件拼出來的String(如start=0,length=15#active=0#state=0),Value是該條件查詢下的所有id組成的List;第三個Map是散列緩存C,在C中,key是散列的字段(如根據userId散列的話,其中某個key就是userId=109這樣的String)組成的Stringvalue是一個和B類似的HashMap。其中只有B這個Map是需要遍歷的,不知道說明白了沒有,看完小面這個例子應該就明白了,就用論壇的回覆表作說明,假設回覆表T中假設有字段idtopicIdpostUserId等字段(topicId就是帖子的idpostUserId是發佈者id)。

 

第一種情況,也是最常用的情況,就是獲取一個帖子對應的回覆,sql語句應該是象

select id from T where topicId=2008 order by createTime desc limit 0,5

select id from T where topicId=2008 order by createTime desc limit 5,5

select id from T where topicId=2008 order by createTime desc limit 10,5

的樣子,那麼這種列表很顯然用topicId做散列是最好的,把上面三個列表緩存(可以是N個)都散列到keytopicId=2008這一個Map中,當id2008的帖子有新的回覆時,系統自動把keytopicId=2008的散列Map清除即可。由於這種散列不需要遍歷,因此可以設置成很大,例如100000,這樣10萬個帖子對應的所有回覆列表都可以緩存起來,當有一個帖子有新的回覆時,其餘99999個帖子對應的回覆列表都不會動,緩存的命中率極高。

 

第二種情況,就是後臺需要顯示最新的回覆,sql語句應該是象

select id from T order by createTime desc limit 0,50

的樣子,這種情況不需要散列,因爲後臺不可能有太多人訪問,常用列表也不會太多,所以直接放到通用列表緩存B中即可。

 

第三種情況,獲取一個用戶的回覆,sql語句象

select id from T where userId=2046 order by createTime desc limit 0,15

select id from T where userId=2046 order by createTime desc limit 15,15

select id from T where userId=2046 order by createTime desc limit 30,15

的樣子,那麼這種列表和第一種情況類似,用userId做散列即可。

 

第四種情況,獲取一個用戶對某個帖子的回覆,sql語句象

select id from T where topicId=2008 and userId=2046 order by createTime desc limit 0,15

select id from T where topicId=2008 and userId=2046 order by createTime desc limit 15,15

的樣子,這種情況比較少見,一般以topicId=2008爲準,也放到keytopicId=2008這個散列Map裏即可。

 

那麼最後的緩存結構應該是下面這個樣子:

 

緩存A是:

Key鍵(long型)

Value值(類型T

11

Id=11T對象

22

Id=22T對象

133

Id=133T對象

……

 

列表緩存B是:

Key鍵(String型)

Value值(ArrayList型)

from T order by createTime desc limit 0,50

ArrayList,對應取出來的所有id

from T order by createTime desc limit 50,50

ArrayList,對應取出來的所有id

from T order by createTime desc limit 100,50

ArrayList,對應取出來的所有id

……

 

散列緩存C是:

Key鍵(String型)

Value值(HashMap

userId=2046

Key鍵(String型)

Value值(ArrayList

userId=2046#0,5

id組成的List

userId=2046#5,5

id組成的List

userId=2046#15,5

id組成的List

……

 

userId=2047

Key鍵(String型)

Value值(ArrayList

userId=2047#0,5

id組成的List

userId=2047#5,5

id組成的List

userId=2047#15,5

id組成的List

……

 

userId=2048

Key鍵(String型)

Value值(ArrayList

userId=2048#topicId=2008#0,5

id組成的List

userId=2048#5,5

id組成的List

userId=2048#15,5

id組成的List

……

 

……

 

總結:這種緩存思路可以存儲大規模的列表,緩存命中率極高,因此可以承受超大規模的應用,但是需要技術人員根據自身業務邏輯來配置需要做散列的字段,一般用一個表的索引鍵做散列(注意順序,最散的字段放前面),假設以userId爲例,可以存儲N個用戶的M種列表,如果某個用戶的相關數據發生變化,其餘N-1個用戶的列表緩存紋絲不動。以上說明的都是如何緩存列表,緩存長度和緩存列表思路完全一樣,如緩存象select count(*) from T where topicId=2008這樣的長度,也是放到topicId=2008這個散列Map中。如果再配合好使用mysql的內存表和memcached,加上F5設備做分佈式負載均衡,該系統對付像1000IP/天這種規模級的應用都足夠了,除搜索引擎外一般的應用網站到不了這種規模。

 

再次申明:系統到底是不是強大不在系統本身而在於使用該系統的人!!!

 

這個緩存系統是我和同事幾年經驗的總結,看似簡單,其實也沒那麼簡單,把它作爲開源有下面幾個目的:第一,真的希望有很多人能用它;第二:希望更多的人能夠完善和改進它;第三:希望大家能聚到一起爲通用高效數據庫緩存構架作出貢獻,畢竟,數據庫操作是各種應用最常用的操作,也是最容易產生性能瓶頸的地方。

 

Zip包中包含了配置方法和測試用的jsp,只要把它配置成一個web應用就可以快速調試並看到緩存的力量了。

 

配置說明文件在docs/開始配置.txt裏有說明。

 

最後囉嗦一句,如果大家真想支持我、支持中國人開源項目,請把該文貼到自己的博客中或者收藏本文,記得包含文檔的下載地址!!!!!!!謝謝。thank you and Good luck

 

QQ羣:24561583

JVM參數調優是一個很頭痛的問題,可能和應用有關係,下面是本人一些調優的實踐經驗,希望對讀者能有幫助,環境LinuxAS4,resin2.1.17,JDK6.0,2CPU,4G內存,dell2950服務器,網站是http://shedewang.com

一:串行垃圾回收,也就是默認配置,完成10request用時153秒,JVM參數配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ";
這種配置一般在resin啓動24小時內似乎沒有大問題,網站可以正常訪問,但查看日誌發現,在接近24小時時,Full GC執行越來越頻繁,大約每隔3分鐘就有一次Full GC,每次Full GC系統會停頓6秒左右,作爲一個網站來說,用戶等待6秒恐怕太長了,所以這種方式有待改善。MaxTenuringThreshold=7表示一個對象如果在救助空間移動7次還沒有被回收就放入年老代,GCTimeRatio=19表示java可以用5%的時間來做垃圾回收,1/(1+19)=1 /20=5%

二:並行回收,完成10request用時117秒,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xmx2048M -Xms2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=500 -XX:+UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 ";
並行回收我嘗試過多種組合配置,似乎都沒什麼用,resin啓動3小時左右就會停頓,時間超過10 秒。也有可能是參數設置不夠好的原因,MaxGCPauseMillis表示GC最大停頓時間,在resin剛啓動還沒有執行Full GC時系統是正常的,但一旦執行Full GCMaxGCPauseMillis根本沒有用,停頓時間可能超過20秒,之後會發生什麼我也不再關心了,趕緊重啓resin,嘗試其他回收策略。

三:併發回收,完成10request用時60秒,比並行回收差不多快一倍,是默認回收策略性能的2.5倍,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 ";
這個配置雖然不會出現10秒連不上的情況,但系統重啓3個小時左右,每隔幾分鐘就會有5秒連不上的情況,查看gc.log,發現在執行ParNewGC時有個promotion failed錯誤,從而轉向執行Full GC,造成系統停頓,而且會很頻繁,每隔幾分鐘就有一次,所以還得改善。UseCMSCompactAtFullCollection是表是執行Full GC後對內存進行整理壓縮,免得產生內存碎片,CMSFullGCsBeforeCompaction=N表示執行NFull GC後執行內存壓縮。

四:增量回收,完成10request用時171秒,太慢了,配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xincgc ";
似乎回收得也不太乾淨,而且也對性能有較大影響,不值得試。

五:併發回收的I-CMS模式,和增量回收差不多,完成10request用時170秒。
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=10 -XX:-TraceClassUnloading ";
採用了sun推薦的參數,回收效果不好,照樣有停頓,數小時之內就會頻繁出現停頓,什麼sun推薦的參數,照樣不好使。

六:遞增式低暫停收集器,還叫什麼火車式回收,不知道屬於哪個系,完成10request用時153
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseTrainGC ";
該配置效果也不好,影響性能,所以沒試。

七:相比之下,還是併發回收比較好,性能比較高,只要能解決ParNewGC(並行回收年輕代)時的promotion failed錯誤就一切好辦了,查了很多文章,發現引起promotion failed錯誤的原因是CMS來不及回收(CMS默認在年老代佔到90%左右纔會執行),年老代又沒有足夠的空間供GC把一些活的對象從年輕代移到年老代,所以執行Full GCCMSInitiatingOccupancyFraction=70表示年老代佔到約70%時就開始執行CMS,這樣就不會出現Full GC了。SoftRefLRUPolicyMSPerMB這個參數也是我認爲比較有用的,官方解釋是softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap,我覺得沒必要等1秒,所以設置成0。配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSPermGenSweepingEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -Xloggc:log/gc.log ";
上面這個配置內存上升的很慢,24小時之內幾乎沒有停頓現象,最長的只停滯了0.8sParNew GC30秒左右才執行一次,每次回收約0.2秒,看來問題應該暫時解決了。

參數不明白的可以上網查,本人認爲比較重要的幾個參數是:-Xms -Xmx -Xmn MaxTenuringThreshold GCTimeRatio UseConcMarkSweepGC CMSInitiatingOccupancyFraction SoftRefLRUPolicyMSPerMB

 

Memcached是什麼?
Memcached是高性能的,分佈式的內存對象緩存系統,用於在動態應用中減少數據庫負載,提升訪問速度。
MemcachedDanga Interactive開發,用於提升LiveJournal.com訪問速度的。LJ每秒動態頁面訪問量幾千次,用戶700萬。Memcached將數據庫負載大幅度降低,更好的分配資源,更快速訪問。

如何使用memcached-Server?
在服務端運行:
# ./memcached -d -m 2048 -l 10.0.0.40 -p 11211
這將會啓動一個佔用2G內存的進程,並打開11211端口用於接收請求。由於32位系統只能處理4G內存的尋址,所以在大於4G內存使用PAE32位服務器上可以運行2-3個進程,並在不同端口進行監聽。

如何使用memcached-Client?Java版是這樣的,參看http://www.whalin.com/memcached/#download
String serverStr = "218.241.154.12:12321";
String[] serverlist = {serverStr};
//String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
//Integer[] weights = { new Integer(5), new Integer(2) }; 
int initialConnections = 100;
int minSpareConnections = 50;
int maxSpareConnections = 500; 
long maxIdleTime = 1000 * 60 * 30; // 30 minutes
long maxBusyTime = 1000 * 60 * 5; // 5 minutes
long maintThreadSleep = 1000 * 5; // 5 seconds
int socketTimeOut = 1000 * 3; // 3 seconds to block on reads
//int socketConnectTO = 1000 * 3; // 3 seconds to block on initial connections. If 0, then will use blocking connect (default)
//boolean failover = false; // turn off auto-failover in event of server down 
boolean nagleAlg = false; // turn off Nagle's algorithm on all sockets in pool 
//boolean aliveCheck = false; // disable health check of socket on checkout

pool = SockIOPool.getInstance("mymemcache");
pool.setServers( serverlist );
//pool.setWeights( weights ); 
pool.setInitConn( initialConnections );
pool.setMinConn( minSpareConnections );
pool.setMaxConn( maxSpareConnections );
pool.setMaxIdle( maxIdleTime );
pool.setMaxBusyTime( maxBusyTime );
pool.setMaintSleep( maintThreadSleep );
pool.setSocketTO( socketTimeOut );
pool.setNagle( nagleAlg ); 
pool.setHashingAlg( SockIOPool.NEW_COMPAT_HASH );
pool.setAliveCheck( true );
pool.initialize();

mCachedClient = new MemCachedClient( "mymemcache" );
mCachedClient.setCompressEnable( false );
mCachedClient.setCompressThreshold(4096);
然後用mCachedClientset/get/delete方法就可以了。memcached的吞吐量每秒大概能get兩萬次左右,這比mysqlselect提高了好幾倍,所以很多網站都用這個來做緩存,如豆瓣。

我在系統種也用了memcached,我的建議是在有分佈式的時候才考慮用memcached,如果只有一臺應用服務器就沒有必要用memcached,畢竟Memcached的吞吐量還是有限,本地HashMap每秒可以get一百萬次,用本地HashMap做緩存纔是最快的。用了分佈式後,緩存同步和分佈式session都是比較難處理的問題,所以建議1000pv/天以下的應用不要用分佈式。 

用了分佈式後,緩存同步和分佈式session都是比較難處理的問題,所以建議1000pv/天以下的應用不要用分佈式。



一般來說讀多寫少,即數據庫更新不太頻繁的數據,是很適合做memcached;另外,附帶說一下memcachedkey & value的限制:
key250個字符;value1M的對象

最近一直在思考,如何才能解決數據庫的瓶頸,讀可以採取很多種手段來cache,如果寫的併發很大的時候,如訂單的集中提交,改如何優化呢? 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章