1、 利用場景描寫
系統主要為教師在線學習提供服務,其中視頻學習網站支持教師在線視頻學習,教師在視頻學習進程中其學習進程會被記錄下來。每一個專題下對應多個教學視頻,每一個教學視頻時長不盡1致。現在的記錄規則是:教師在看視頻的時候,視頻所在的頁面每分鐘提交1次要求,記錄該視頻已學習時長,并將該記錄更新到http://www.vxbq.cn/db/。
目前http://www.vxbq.cn/db/中有8266個教師用戶,在***政策下,極有可能在某1段時間內大部份教師用戶同時在線看視頻。這意味著在極端情況下,每分鐘可能會提交6000+個要求,對利用http://www.vxbq.cn/server/帶來了很大的壓力。另外,我們在更新學習時長記錄前,會將其與已學時長(需要實時的查詢)相比較,如果當條件交的時長比已學時長大則更新,否則不更新,頻繁的查詢與更新http://www.vxbq.cn/db/嚴重下降了系統的響應速度。對學習時長的記錄進程進行優化燃眉之急。
2、 硬軟件情況
硬件:1臺http://www.vxbq.cn/server/,核心4核,內存
軟件:Windows Server 2008 64位操作系統, Tomcat 7, jdk1.6,http://www.vxbq.cn/access/5.5
3、 優化進程
1》第1階段
分析:首先想到的是,這個進程主要的壓力在于大量的http://www.vxbq.cn/server/要求和頻繁的http://www.vxbq.cn/db/連接,那末,合并要求,利用緩存機制應當可以解決問題。
解決方案:將用戶的要求置入緩存,定時集中處理,合并更新操作。
具體做法:利用線程安全的包裝后的HashMap作為用戶要求緩存。
public static Map<LearnTime, Integer> map = Collections.synchronizedMap(new LinkedHashMap<LearnTime,Integer>()); //用戶要求來了以后將要求置入緩存 protected static void put(String userName, String videoId, int topicId, int totalTime, int learnTime) { LearnTime learn = new LearnTime(userName, videoId, topicId, totalTime); //可以保證緩存中的時間比看過的時長要大 if(map.containsKey(learn)) { if(learnTime > map.get(learn)) { map.put(learn, learnTime); } }else { //每一個"用戶-視頻"在1個緩存時間內只查1次 int learnedTime = dao.getLearnedTime(userName, videoId, topicId);已學習的時間 if(learnTime > learnedTime) { map.put(learn, learnTime); }else{ map.put(learn, learnedTime); } } } |
每隔1個小時,對緩存數據做1次處理,將學習記錄更新到http://www.vxbq.cn/db/:遍歷HashMap數據項,生成sql語句,拼接到1起,然后在1個連接以內處理完。
Iterator<LearnTime> i = s.iterator(); StringBuilder str = new StringBuilder(); while (i.hasNext()) { learn = i.next(); userName = learn.getUserName(); videoId = learn.getVideoId(); topicId = learn.getTopicId(); learnTime = map.get(learn); totalTime = learn.getTotalTime(); if(learnTime < totalTime){ //分數保存兩位小數 String sScore = new DecimalFormat("#.00").format(0.5*learnTime/totalTime); Double score = Double.valueOf(sScore); str.append(sql語句); }else if(learnTime >= totalTime) { learnedTime = dao.getLearnedTime(userName, videoId, topicId);已學習的時間 if(learnedTime != totalTime) { str.append("sql語句"); } } } |
這樣處理以后,系統性能得到了1定的改良,但是http://www.vxbq.cn/db/連接的壓力還是挺大的,從程序代碼中可以看到,在往緩存中添加學習記錄和更新之前,都有連接http://www.vxbq.cn/db/進行查詢的操作,對http://www.vxbq.cn/db/連接也有較大的消耗。另外,用戶端對http://www.vxbq.cn/server/的大量要求并沒有得到較好的解決。因此還需要繼續優化。
2》第2階段
分析:前面已分析了依然存在的兩個問題,其1是利用http://www.vxbq.cn/server/的大量并發要求,第2是http://www.vxbq.cn/db/的頻繁訪問。還有1個沒有提到過的,當緩存正在被讀取時,往緩存里面寫數據是要被阻塞的,如果緩存遍歷和更新處理過慢,則會致使長時間的要求阻塞。
解決方案:對利用http://www.vxbq.cn/server/的要求,由于每一個要求做的事情僅僅是做1個查詢,然后向緩存里面更新數據,這個進程是非常短的,我們可以利用Tomcat配置1個較大的線程池,以響應如此多的要求;對http://www.vxbq.cn/db/的頻繁訪問,不難發現,其實更新進程已合并了,只是查詢已學時長還是單個做的,查詢僅僅為了校驗是不是更新,若把是不是更新交給http://www.vxbq.cn/db/去決定,那末所有的查詢要求都會合并到更新中去,這樣這個問題就解決了;緩存的遍歷快慢由緩存的大小決定,需要選擇適合的緩存周期;更新的處理可以剝離出來,在遍歷緩存的同時,將數據取出,另外開1個線程來處理更新操作,讓HashMap的鎖釋放,減少阻塞時長,遍歷并生成sql語句進程應當可以控制在幾秒內,影響不大。
具體做法:
1.將要求置入緩存
public static Map<LearnTime, Integer> map = Collections.synchronizedMap(new LinkedHashMap<LearnTime,Integer>()); //用戶要求來了以后將要求置入緩存 protected static void put(String userName, String videoId, int topicId, int totalTime, int learnTime) { LearnTime learn = new LearnTime(userName, videoId, topicId, totalTime); //可以保證緩存中的時間比看過的時長要大 if(map.containsKey(learn)) { if(learnTime > map.get(learn)) { map.put(learn, learnTime); } }else { //直接放入緩存,判斷是不是更新在http://www.vxbq.cn/db/處理 map.put(learn, learnTime); } } |
2.更新進程剝離
static class UpdateTask implements Runnable{ private String sql; public UpdateTask(String sql) { this.sql = sql; } @Override public void run() { dao.updateLearnTime(sql); } } |
3.緩存周期清算
count++; //每處理1次更新,計數加1 //到達緩存周期時長,將緩存清算掉,計數清零 if(count % clearCycle == 0) { map.clear(); count = 0; } |
4.校驗進程都在http://www.vxbq.cn/db/做,即更新語句作限制,略。
5.利用http://www.vxbq.cn/server/Tomcat連接池配置。
1)下載tcnative⑴.dll,以支持APR要求 2)將dll文件復制到windows/system32下面,或將其加入path 3)配置Tomcat下的server.xml <Executor name="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="100" prestartminSpareThreads="true" /> <Connector executor="tomcatThreadPool" URIEncoding="utf⑻" port="80" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="20000" redirectPort="8453" maxThreads="1000" minSpareThreads="200" acceptCount="1000" /> |
4、 優化總結
該實際場景的優化主要在4個方面:1、合并http://www.vxbq.cn/db/連接要求;2、增加利用http://www.vxbq.cn/server/響應線程數;3、實際更新處理與緩存周期剝離以減少阻塞;4、權衡緩存大小和用戶使用習慣,公道設置緩存清算周期。
另外,該場景最初的瓶頸在于頻繁的http://www.vxbq.cn/db/連接,而正好可以通過合并連接來優化。在極端情況下,沒法合并連接呢?這就必須要在http://www.vxbq.cn/db/訪問層利用連接池進行優化,在現有架構下,還不知道如何配置http://www.vxbq.cn/db/連接池,這是1個需要摸索的重要優化點。
5、 測試數據
實際場景下,假定8000+用戶同時在線看視頻,1分鐘有8000+次要求,平均每秒140次。每一個要求由1個線程來履行,在測試時,摹擬這個進程。
測試用例:每100毫秒提交20個要求,也就是1秒200個要求,每一個要求開啟1個新的線程履行,共提交2000000要求,測試時長10000秒。以3000用戶(由于不好實際摹擬,采取將http://www.vxbq.cn/db/中數據提取出來的方式,用隨機提交要求的方式在程序中測試)在線看視頻,每3分鐘處理1次更新,每8個處理周期清算1次緩存。測試結果毫無壓力。可能在實際場景中,需要遍歷的緩存項會多1些,但是根據經驗,http://www.vxbq.cn/server/履行更新語句可以到達每秒3000條以上,況且其實不會對緩存阻塞,因此完全滿足性能需求。
把要求頻率改成每100毫秒提交50個要求,也就是每秒500個要求,其他條件不變,測試時長4000秒。測試結果也很理想,沒有多大改變,也就是說支持3萬用戶的時長記錄要求木有問題。http://www.vxbq.cn/server/資源有浪費啊!
測試代碼以下:
Random random = new Random(); LearnTime learn; int index = 0; int learnTime = 0; for(int i = 0;i < 2000000;i++){ index = random.nextInt(3000); learn = learns[index]; learnTime = random.nextInt(learn.getTotalTime()+1); Thread thread = new Thread(new PutTask(learn, learnTime)); thread.start(); if(i%50 == 0){ Thread.sleep(100); } } //要求線程摹擬以下: static class PutTaskimplements Runnable{ private LearnTimelearn; privateintlearnTime; public PutTask(LearnTime learn,int learnTime){ this.learn = learn; this.learnTime = learnTime; } publicvoid run() { LearnTimeHandleServlet.put(learn,learnTime); } } |