解決 ASP.NET 網站重新編譯導致反應龜速的問題

這篇的標題可能不是很恰當,因為最初只是想紀錄一下 ASP.NET 網站效能調校的一個小技巧,結果後來想起以前的一些效能調校心得,就一併整理進來了,所以後面的內容可能有點雜。如果你碰到類似的效能問題,可以看看第一段和第二段,後面的則大可跳過。

1. 問題

我們碰到的問題是,ASP.NET 網站的程式檔案數量很龐大,造成每次有共用的類別檔案(例如 App_Code 底下的檔案)更新時,「享用」此更新版本的第一個使用者就會因為整個網站重新編譯而等個好幾分鐘,網頁才出得來(其實看起來就像網站當掉了)。

網站的程式檔案數量有多大呢?我把圖抓下來:



22,714 個檔案,扣掉一些圖片、CSS、靜態網頁等程式檔案,保守估計也有兩萬個程式檔案。由於這個專案還挺大的(對某些人來說,這樣的規模或許還算普通吧),裡面很多子系統都還在陸續開發中,因此經常會有 user 火線作業時必須立即更新程式檔案的情況,例如:緊急 bug 修正。

結果,每當更新程式時,整個 ASP.NET 網站就像掛掉一樣,完全無法回應。用工作管理員觀察,可以看出是 ASP.NET runtime 正在編譯網站。雖然我們的系統有九台機器分擔負載,可是九台都是一起更新,也就是每一台伺服器都同時在重新編譯網站,因此當使用者正在火線作業時,會突然發現網站好像掛掉,持續大約五、六分鐘以後才恢復正常的反應速度。這樣的狀況我想沒有任何一個使用者可以接受,無論你怎麼解釋(如:第一個存取網站的人會比較慢)都很難平息他們的怒火。

2. 解法

結果是在 web.config 裡面加上這段就解決了:

<compilation debug="false" batch="true" maxBatchGeneratedFileSize="10000"
maxBatchSize="10000">

這樣調整之後速度差多少呢?現在每次更新檔案之後,第一個幸運的使用者只會感覺大約有三十秒左右的延遲時間(原本是五、六分鐘),之後又開始變快了。碰到這種情形,他們通常會認為只是網路短暫的不穩而已(該不會是被我們洗腦了 @_@)。

就這麼一行,速度差別竟然這麼大!怎麼回事?

當然我們也有調整別的地方,但是影響速度最大的還是前面那行設定。這主要和 ASP.NET 網站的編譯模型有關;由於我們的網站檔案數量龐大,因此每次網站重新編譯時,都會產生為數眾多的 DLL 檔。你可以參考 Dino Esposito 在 MSDN Magazine 的 Cutting Edge 專欄發表的文章:The Server Side of ASP.NET Pages。從這篇文章你可以了解 ASP.NET 編譯網站時會產生哪些暫存檔案。

文章裡面提到一個跟編譯模型有關的 web.config 元素: <compilation> 的 batch 屬性。順著這條線索,可以到 MSDN 網站上查到其他相關的屬性。你可以看到,batch 屬性預設已經是 true 了,所以關鍵處在於 maxBatchGeneratedFileSize 和 maxBatchSize 屬性。簡單地說,這兩個屬性會影響 ASP.NET 網站編譯時,要將多少個網頁編譯到同一個 DLL 裡面。也就是說,當你指定的 DLL 檔案 size 愈大,一個 DLL 就能容納愈多編譯的網頁類別,所以編譯時產生的 DLL 數量就會變少,這就是速度變快的主要原因(太多零碎的小檔案會造成頻繁的 I/O,以致於拖跨效能)。

當然囉,DLL 變大了,在記憶體的運用方面就沒有零碎檔案這麼靈活,但我們的機器有很充足的記憶體,用空間換取時間,很划算。

3. 小小心得

就像大部分的效能調校過程一樣,我們並不是第一次就找到癥結點。我們曾懷疑:
  • 硬體等級太差(這點很快就排除了)
  • 某些元件採用 J# 撰寫,造成 CLR 的負擔
  • 可能是 production 環境裡面包含許多 Subversion 版本控制檔案的緣故,增加不少編譯時間
但因為整個網站的檔案數量成長速度太快了,而且都是更新檔案時才出現反應龜速的情形(憤怒的使用者:「根本就是完全不動!」),因此很快就把嫌疑犯指向檔案更新時的網站重新編譯動作;而要解決重新編譯 ASP.NET 網站造成的速度延遲,自然就得從編譯模型下手了。

在我參與過的 multi-tier 架構的專案裡(不多,十個指頭數得出來),大部分都曾出現過效能問題。開發人員碰到這類問題時,往往不知從何下手,常見的反應是來個亂槍打鳥:這邊調一下 SQL、那邊資料表加個索引等等(大部分還真管用),如果都沒效,就擴充 CPU 和 RAM,先撐個一陣子再說。

效能調教方面的知識和技能,我覺得有點像保險,經常是沒碰到的時候嫌太多,需要的時候嫌太少。如果學習新技術時能夠盡量往底層挖,打好了硬底子,將來碰到效能問題時,會有更多 idea 和工具,知道有哪些面向要考量,定出效能調校的方向和策略。方向抓對了,問題可以說已經解決一半了。

4. 當年勇

在處理效能問題時,我通常是採取排除嫌疑犯的方式。

資料庫應用程式的效能不好,我會先想辦法排除掉硬體因素(也就是說,硬體都夠力)和網路通訊的因素。比如說,用一個簡單的程式丟到測試或正式環境中跑跑看,如果一個查詢五萬筆資料的 SQL 指令可以在 N 秒內完成,就可以大致確認整個網路和硬體環境沒問題。

說到這個,我想到以前開發過的一個專案,用 Java/JSP 寫的;客戶在台中,開發小組在台北。系統上線後,使用者跳腳:「按鈕按下去竟然要等四、五分鐘才跑出結果!」我聽了很訝異,在公司的測試環境怎麼測都只要幾十秒就完成了啊。由於客戶在台中,我也不可能隨時想到什麼 idea 就跑去客戶端測試,得採取決勝千里外的作法才行。因此,我在程式裡埋了一些測試效能的機制,說穿了很簡單:前端網頁按鈕按下時,用 JavaScript 在網頁的隱藏欄位裡記錄 request 發送的時間(request_send_time),後端 Java servlet 則會紀錄收到 request 的時間(request_receive_time),即將傳回 response 之前也會紀錄一次時間(response_return_time),這些時間變數都會一併傳回用戶端頁面,待用戶端收到 response 時,在前端網頁的 onLoad 事件中紀錄一次時間(response_reseive_time)。最後,在一個獨立的除錯頁框中顯示這幾個時間,就可以清楚看出幾個關鍵的時間區間:
  1. 從前端發出 request 開始到 server 收到的過程中花了多少時間。如果這段時間很長,那麼問題很可能出在網路傳輸。
  2. 後端 server 的處理時間。如果這段時間很長,再朝向資料庫設計、調 SQL、調程式碼的方向進一步追查。
  3. 後端 server 傳回 request 開始到前端網頁載入完畢所花的時間。如果這段時間很長,那麼問題很可能出在網路傳輸。
結果根據當時使用者回報的數據,整個執行時間主要都花在第 1 項和第 3 項,答案呼之欲出。使用者自己心裡也有底了,主動找自己的網管檢查,告訴我們是防火牆設定的問題。

那個計算處理時間的程式並不難寫,可是當我後來看到 ASP.NET 的網頁只要把 trace 功能打開就能自動計算每一次 request 在伺服器端花了多少處理時間,還是小小感動了一下。至於計算用戶端頁面傳送 request 至 server 端的傳輸時間,若不想自己動手,也可以試試其他工具,例如 Fiddler,它會攔截網頁的發出的任何 request(一張 gif 圖片也是一個 request),並顯示每一次 request 的效能統計資訊。

小結

談笑間,系統風馳電掣--這大概每個效能調校人員的夢想吧。不過,如果沒有平日不斷學習技術和累積經驗,每次碰到效能問題時恐怕還是容易流於亂槍打鳥。

沒有留言:

技術提供:Blogger.
回頂端⬆️