项目优化经验——垃圾回收导致的性能问题
談?wù)勛罱鼉?yōu)化一個網(wǎng)站項(xiàng)目的經(jīng)驗(yàn),首先說一下背景情況:
1) 在頁面后臺代碼中我們把頁面上大部分的HTML都使用字符串來拼接生成然后直接賦值給LiteralControl。
2) 網(wǎng)站CPU很高,基本都在80%左右,即使使用了StringBuilder來拼接字符串性能也不理想。
3) 為了改善性能,把整個字符串保存在memcached中,性能還是不理想。
在比較了這個網(wǎng)站和其它網(wǎng)站服務(wù)器上相關(guān)性能監(jiān)視器指標(biāo)后發(fā)現(xiàn)有一個參數(shù)特別顯眼:
就是其中的每秒分配字節(jié)數(shù),這個性能比較差的網(wǎng)站每秒分配2GB的內(nèi)存(而且需要注意由于性能監(jiān)視器是每秒更新一下,對于一個非常健康的網(wǎng)站這個值應(yīng)該經(jīng)常看到是0才對)!而其它一些網(wǎng)站只分配200M左右的內(nèi)存。服務(wù)器配備4G內(nèi)存,而每秒分配2G內(nèi)存,我想垃圾回收器一定需要不斷運(yùn)行來回收這些內(nèi)存。觀察%Time in GC可以發(fā)現(xiàn),這個值一直在10%左右,也就是說上次回收到這次回收間隔10秒的話,這次垃圾回收1秒,由于回收的時間相對固定,那么這個值可以反映回收的頻繁度。
知道了這個要點(diǎn)就知道了方向,在項(xiàng)目中找可能的問題點(diǎn):
1) 是否分配了大量臨時的小對象
2) 是否分配了數(shù)量不多但比較大的大對象
在經(jīng)歷了一番查找之后,發(fā)現(xiàn)一個比較大的問題,雖然使用了memcached來緩存整個頁面的HTML,但是在輸出之前居然進(jìn)行了幾次string的Replace操作,這樣就產(chǎn)生了幾個大的字符串,我們來做一個實(shí)驗(yàn)?zāi)M這種場景:
public partial class _Default : System.Web.UI.Page {static string template;protected void Page_Load(object sender, EventArgs e){if (template == null){StringBuilder sb = new StringBuilder();for (int i = 0; i < 10000; i++)sb.Append("1234567890");template = sb.ToString(); }Stopwatch sw = Stopwatch.StartNew();for (int i = 0; i < 1; i++){long mem1 = GC.GetTotalMemory(false);string s = template + i;long mem2 = GC.GetTotalMemory(false);Response.Write((mem2 - mem1).ToString("N0"));Response.Write("<br/>");GC.KeepAlive(s);}for (int i = 0; i < 100000; i++){double d = Math.Sqrt(i);}Thread.Sleep(30);Response.Write(sw.ElapsedMilliseconds);} }在這段代碼中:
1) 我們首先使用一個靜態(tài)變量模擬緩存中的待輸出的HTML
2) 我們中間的一段代碼測算一下這個字符串占用的內(nèi)存空間
3) 隨后我們做了一些消耗CPU的運(yùn)算操作來模擬頁面的一些計算
4) 然后休眠一段時間
4) 最后我們輸出了頁面執(zhí)行時間
我們這么做的目的是模擬一個比較“正常的”ASP.NET頁面需要做的一些工作:
1) 內(nèi)存上的分配
2) 一些計算
3) 涉及到IO訪問的一些等待
來看看輸出結(jié)果:
這里可以看到,我們這個字符串占用差不多200K的字節(jié),字符串是字符數(shù)組,CLR中字符采用Unicode雙字節(jié)存儲,因此10萬長度的字符串占用200千字節(jié),并且也可以看到這個頁面執(zhí)行時間30毫秒,差不多是一個正常aspx頁面的時間,而200K不到的字符串也差不多相當(dāng)于這個頁面的HTML片段,現(xiàn)在我們來改一下其中的一段代碼模擬優(yōu)化前進(jìn)行的Replace操作帶來的幾個大字符串:
for (int i = 0; i < 10; i++) {//long mem1 = GC.GetTotalMemory(false);string s = template + i;//long mem2 = GC.GetTotalMemory(false);//Response.Write((mem2 - mem1).ToString("N0"));//Response.Write("<br/>");//GC.KeepAlive(s); }然后使用IDE自帶壓力測試1000常量用戶來測試這個頁面:
可以看到每秒分配了超過400M字節(jié)(這和我們線上環(huán)境比還差點(diǎn)畢竟請求少),CPU占用基本在120-160左右(雙核),我們?nèi)サ裘棵敕峙鋬?nèi)存這個數(shù)值,來看看垃圾回收頻率和CPU占用兩個值的圖表:
可以看到紅色的CPU波動基本和藍(lán)色的垃圾回收波動保持一致(這里不太準(zhǔn)確的另外一個原因是壓力測試客戶端運(yùn)行于本機(jī),而為w3wp關(guān)聯(lián)2個處理器)!為什么說垃圾回收會帶來CPU的波動,從理論上來說有以下原因:
1) 垃圾回收的時候會暫時掛起所有線程,然后GC會檢測掃描每一個線程棧上可回收對象,然后會移動對象,并且重新設(shè)置對象指針,這整個過程首先是消耗CPU的
2) 而且在這個過程之后恢復(fù)線程執(zhí)行,這個時候CPU往往會引起一個高峰因?yàn)橐呀?jīng)有更多的請求等待了
我們把Math.Sqrt這段代碼注釋掉并且把w3wp和VSTestHost關(guān)聯(lián)到不同的處理器來看看對于CPU計算很少的頁面,上圖更明顯的對比:
?
這說明垃圾回收的確會占用很多CPU資源,但這只是一部分,其實(shí)我覺得網(wǎng)站的CPU壓力來自于幾個地方:
1) 就是大量的內(nèi)存分配帶來的垃圾回收所占用的CPU,對于ASP.NET框架內(nèi)部的很多行為無法控制,但是可以在代碼中盡量避免在堆上產(chǎn)生很多不必要的對象
2) 是實(shí)際的CPU運(yùn)算,不涉及IO的運(yùn)算,這些可以通過改良算法來優(yōu)化,但是優(yōu)化比較有限
3) 是IO操作這塊,數(shù)據(jù)量的多少很關(guān)鍵,還有要考慮memcached等外部緩存對象序列化反序列化的消耗
4) 雖然很多IO操作不占用CPU資源,線程處于休眠狀態(tài),但是很多時候其實(shí)是依托新線程進(jìn)行的,帶來的就是線程切換和線程創(chuàng)建消耗的消耗,這一塊可以通過合理使用多線程來優(yōu)化
發(fā)現(xiàn)了這個問題之后優(yōu)化就很簡單了,把Replace操作放到memcached的Set操作之前,取出之后不產(chǎn)生過多大字符串,把for循環(huán)改為一次,再來看一下:
?
這次內(nèi)存分配明顯少了很多,CPU降下來了,降的不多,但從壓力測試監(jiān)視器中看到頁面執(zhí)行平均時間從5秒變?yōu)?秒了,每秒平均請求數(shù)從170到了200(最高從200到了300)。在這里要說明一點(diǎn)很多時候網(wǎng)站的性能優(yōu)化不能光看CPU還要對比優(yōu)化前后網(wǎng)站的負(fù)載,因?yàn)樵趦?yōu)化之后頁面執(zhí)行時間降低了,負(fù)載量就增大了CPU消耗也隨之增大。并且可以看到垃圾回收頻率的縮短很明顯,從長期在30%到幾十秒一次30%。
最后想補(bǔ)充幾點(diǎn):
1) 有的時候我們會使用GC.GetTotalMemory(true); 來得到垃圾回收之后內(nèi)存分配數(shù),類似這樣涉及到垃圾回收的代碼在項(xiàng)目上線后千萬不能出現(xiàn),否則很可能會% Time in GC達(dá)到80%以上大量占用CPU。
2) 對于放在緩存中的對象我們往往會覺得性能得到保障大量去使用,其實(shí)緩存實(shí)現(xiàn)的只是把創(chuàng)造這個對象過程的時間轉(zhuǎn)化為空間,而在拿到這個對象之后再進(jìn)行很多運(yùn)算帶來的大量空間始終會進(jìn)行垃圾回收。做網(wǎng)站和做應(yīng)用程序不一樣,一個操作如果申請200K堆內(nèi)存,一個頁面執(zhí)行這個操作10次,一秒200多個請求,大家可以自己算一下平均每秒需要分配多少內(nèi)存,這個數(shù)值是相當(dāng)可怕的,網(wǎng)站是一個多線程的環(huán)境,我們對內(nèi)存的使用要考慮更多。
總結(jié)
以上是生活随笔為你收集整理的项目优化经验——垃圾回收导致的性能问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google电子地图基础及应用
- 下一篇: squid代理服务器详解