提高ASP.Net應用程式效能的十大方法
作者:Eve Cole
更新時間:2009-06-30 15:49:49
本文討論:
提高asp.net應用程式效能的常說的神話有用的提高asp.net應用程式效能的技巧
Asp.net應用程式操作資料庫的建議
Asp.net中的快取與後台處理程序
現在寫一個asp.net的web應用程式變得非常的簡單,許多的程式設計師都不願花時間去建立一個效能良好的應用程式。本文將要討論提高web應用程式效能的十大方法。我將不限於只討論asp.net應用程式的內容,因為它們只是web應用程式的子集。本文也不能提供一個完整提升web應用程式效能的指南,因為這需要一本書的篇幅。本文只提供一個提高web應用程式效能的良好的開端。 (剩下的只有我們自己慢慢研究了)。
在工作這之外,我經常去攀岩,在每次攀岩之前,我都會重溫一下攀岩線路圖及看一下前面的成功的攀岩者的建議。因為我們需要它們的成功經驗。同樣的,當你需要修改某個有效能問題的程式或是要開發一個高效能的網站時,你也需要學習怎麼樣寫一個高效能的web應用程式。
我個人的經驗主要來自於在微軟的asp.net群組擔任程式經理,運行和管理www.asp.net網站,和協助開發Community Server(它是asp.net Forums,.Text, and nGallery的整合升級版本軟體).我想這些經驗能讓我讓來幫助大家。
你也許會想到把你的應用程式分割成不同的邏輯層。你也可能聽過三層物理架構或N層架構,這是最常用的架構模式,它把不同的程式功能物理的分配給各個硬體來執行。這樣,如果我們想提高應用程式的效能的話,加一些硬體就可以達到目的了。按理說這種方法能提高應用程式的效能,但是我們應該避免使用這種方法。所以,只要有可能,我們都應該把asp.net頁面和它用到的元件放到一個應用程式中運行。
因為分散式的佈署,要用到web services或Remoting,它將使應用程式的效能下降20%或更多。
對於資料層有點不同,最好還是把它獨立出來佈署,用一個單獨的硬體來運行它。雖然這樣,但是資料庫仍然是應用程式效能的瓶頸。因此,當你想優化你的程式的時候,首先想到的地方就應該是優化資料層了。
在修改應用程式的出現效能問題的地方之前,你要先確認出問題的地方的程式看起來很嚴密,效能分析器對於查找應用程式哪些地方花費了多長時間非常有用。這些地方是我們直覺感覺不到的。
本文討論兩種類型的效能最佳化:一種是大的效能最佳化(big optimizations),如用asp.net的Cache;另一種是小的效能最佳化(tiny optimizations)。小幅的效能優化有時候非常有用。你只對你的程式碼作一個小的改到,然後一次呼叫它一千或一萬次。作一次大的效能優化,你會發生你的應用程式的速度會有一個很大的提升。作一次小的效能最佳化,也許每次請求只能提高一微秒,但是如果每天的請求量很大的話,那麼應用程式就有很顯著的效能提升。
資料層的效能
當你要優化一個應用程式的效能的時候,你可以按下面的順序工作:你的程式碼要存取資料庫?如果要,存取資料庫頻率怎麼樣?同樣,這種測試方法也可以用在用web services或Remoting的程式碼中。本文將不討論用Web services和Remoting的程式優化的問題。
如果在你的程式碼中有一段必須存取資料庫的請求,而你在其它的地方又看到實現同樣的功能的程式碼,那麼你首先要優化它。修改和完善繼續測試,除非你有一個非常大的效能問題,你的時間最好花在最佳化查詢,連接資料庫,回傳資料集的大小,以及一次查詢往回傳的時間。
根據經驗的總結,讓我們來看看十個能幫助你提升你的應用程式效能的經驗,我將按將它們提升效率的多少從大到小小依次說明。
一、返回多個資料集
檢查你的存取資料庫的程式碼,看是否存在著要回傳多次的請求。每次往返都降低了你的應用程式的每秒鐘能夠回應請求的次數。透過在單一資料庫請求中傳回多個結果集,可以減少與資料庫通訊的時間,使你的系統具有擴展性,也可以減少資料庫伺服器回應請求的工作量。
如果你是用動態的SQL語句來傳回多個資料集,那我建議你用預存程序來取代動態的SQL語句。是否把業務邏輯寫到預存過程中,這個有點爭議。但是我認為,把業務邏輯寫到儲存過程裡面可以限制回傳結果集的大小,減少網路數據的流量,在邏輯層也不用在過濾數據,這是個好事情。
用SqlCommand對象的ExecuteReader方法傳回一個強型別的業務對象,再呼叫NextResult方法來移動資料集指針來定位資料集。範例一示範了一個傳回多個ArrayList強類型物件的範例。只從資料庫傳回你需要的資料可以大大的減少你的伺服器所耗用的記憶體。
二、對資料進行分頁
ASP。 NET的DataGrid有一個非常有用的功能:分頁。如果DataGrid允許分頁,在某一時刻它只下載某一頁的數據,另外,它有一個數據分頁的濟覽導航欄,它讓你可以選擇瀏覽某一頁,而且每次只下載一頁的數據。
但它有一個小小的缺點,就是你必須把所有的資料都綁定到DataGrid。也就是說,你的資料層必須回傳所有的數據,然後DataGrid再根據目前頁過濾出目前頁所需的數據顯示出來。如果有一個一萬筆記錄的結果集要用DataGrid進行分頁,假設DataGrid每頁只顯示25個數據,那就表示每次請求都有9975個數據都是要丟棄的。每次請求都要回傳這麼大的資料集,對應用程式的效能影響是非常大的。
一個好的解決方案是寫一個分頁的預存程序,例子2是一個用於對Northwind資料庫orders表的分頁預存程序。你只需要傳當前頁碼,每頁顯示的條數兩個參數進來,預存程序會傳回對應的結果。
在伺服器端,我特別寫了一個分頁的控制項來處理資料的分頁,在這裡,我用了第一個方法,在一個預存程序裡面傳回了兩個結果集:資料記錄總數和要求的結果集。
傳回的記錄總數取決於要執行查詢,例如,一個where條件可以限制傳回的結果集的大小。因為在分頁介面中必須要根據資料集記錄的大小來計算總的頁數,所以必須傳回結果集的記錄數。例如,如果一共有1000000筆記錄,如果用where條件就可以過濾成只回傳1000筆記錄,預存程序的分頁邏輯就應該知道傳回那些需要顯示的資料。
三、連接池
用TCP來連接你的應用程式與資料庫是一件昂貴的事情(很費時的事情),微軟的開發者可以透過使用連接池來重複的使用資料庫的連接。比起每次請求都用TCP來連一次資料庫,連線池只有在不存在有效的連線時才新建一個TCP連線。當關閉一個連接的時候,它會被放到池中,它仍然會保持與資料庫的連接,這樣就可以減少與資料庫的TCP連接次數。
當然,你要注意那些忘記關的連接,你應該在每次用完連接後馬上關閉它。我要強調的是:無論什麼人說.net framework中的GC(垃圾收集器)總是會在你用完連接對象後調用連接對象的Close或者Dispose方法顯式的關閉你的連接。不要期望CLR會在你想像的時間內關掉連接,雖然CLR最終都要銷毀物件和關閉邊接,但是我們並不能確定它到底會在什麼時候做這些事情。
要用連接池優化,有兩條規則,第一,打開連接,處理數據,然後關閉連接。如果你必須在每次請求中多次打開或關閉連接,這好過一直打開一個邊接,然後把它傳到各個方法中。第二,用相同的連接字串(或用相同的使用者標識,當你用整合認證的時候)。如果你沒有使用相同的連接字串,如你使用基於登入使用者的連接字串,這將無法利用連接池的最佳化功能。如果你用的是整合的論證,因為使用者很多,所以你也無法充分利用連線池的最佳化功能。 .NET CLR提供了一個資料效能計數器,它在我們需要追蹤程式效能特性的時候非常有用,當然也包含連線池的追蹤了。
無論你的應用程式何時要連在另一台機子的資源,如資料庫,你都應該專注於優化你連資源所花的時間,接收和傳送資料的時間,以及往返回之間的次數。優化你的應用程式中的每一個處理點(process hop),它是提高你的應用程式的效能的出發點。
應用程式層包含與資料層連接,傳送資料到相應的類別的實例以及業務處理的邏輯。例如,在Community Server中,要組裝一個Forums或Threads集合,然後應用業務邏輯,如授權,更重要的,這裡要完成快取邏輯。
四、 ASP。 NET快取API
在寫應用程式之前,你要做的第一件事是讓應用程式最大化的利用ASP.NET的快取功能。
如果你的元件是要在Asp.net應用程式中執行,你只要把System.Web.dll引用到你的專案中就可以了。然後用HttpRuntime.Cache屬性就可存取Cache了(也可以透過Page.Cache或HttpContext.Cache存取)。
有以下幾條快取資料的規則。第一,資料可能會被頻繁的被使用,這種資料可以快取。第二,資料的存取頻率非常高,或一個資料的存取頻率不高,但是它的生存週期很長,這樣的資料最好也快取起來。第三是一個常常被忽略的問題,有時候我們快取了太多數據,通常在一台X86的機子上,如果你要快取的數據超過800M的話,就會出現記憶體溢出的錯誤。所以說緩存是有限的。換名話說,你應該估計快取集的大小,把快取集的大小限制在10以內,否則它可能會出問題。在Asp.net中,如果快取過大的話也會回報記憶體溢出錯誤,特別是如果快取大的DataSet物件的時候。
這裡有幾個你必須了解的重要的快取機制。首先是快取實現了「最近使用」原則( a least-recently-used algorithm),當快取少的時候,它會自動的強制清除那些無用的快取。其次「條件依賴」強制清除原則(expiration dependencies),條件可以是時間,關鍵字和檔案。以時間作為條件是最常用的。在asp.net2.0中增加一更強的條件,就是資料庫條件。當資料庫中的資料發生變化時,就會強制清除快取。更深入的了解資料庫條件依賴請看Dino Esposito 在MSDN雜誌2004年七月刊的Cutting Edge專欄文章。 Asp.net的快取架構如下圖所示:
五、 預請求緩存
在前面,我提到即使我們只對某些地方作了一個小小的效能改進也可以獲得大的效能提升,我非常喜歡用預請求快取來提升程式的效能。
雖然Cache API設計成用來保存某段時間的數據,而預先請求快取只是保存某個時期的某個請求的內容。如果某個請求的存取頻率高,而且這個請求只需要提取,應用,修改或更新資料一次。那麼就可以預先快取該請求。我們舉個例子來說明。
在CS的論壇應用程式中,每個頁面的伺服器控制項都要求得到用於決定它的皮膚(skin)的自訂的數據,以決定用哪個樣式表及其它的一些個性化的東西。這裡面的某些數據可能要長時間的保存,有些時間則不然,如控件的skin數據,它只需要應用一次,而後就可以一直使用。
要實現預先請求緩存,請使用Asp.net 的HttpContext類,HttpContext類的實例在每個請求中創建,在請求期間的任何地方都可以透過HttpContext.Current屬性存取。 HttpContext類別有一個Items集合屬性,在請求期間所有的物件和資料都被加入到這個集合中快取起來。跟你用Cache快取存取頻率高資料一樣,你可以用HttpContext.Items快取那些每個請求都要用到的基礎資料。它背後的邏輯很簡單:我們在HttpContext.Items中加入一個數據,然後再從它裡面讀出數據。
六、 後台處理
通過上面的方法你的應用程式應該運行得很快了,是不是?但是在某些時候,程式中的一次請求中可能要執行一個非常耗時的任務。如發送郵件或檢查提交的資料的正確性等。
當我們把asp.net Forums 1.0整合在CS中的時侯,發現提交一個新的帖子的時候會非常的慢。每次新增一個帖子的時侯,應用程式首先要檢查這個帖子是不是重複提的,然後用“badword”過濾器來過濾,檢查圖片附加碼,作帖子的索引,把它添加到合適的隊列中,驗證它的附件,最後,發送電子郵件到它的訂閱者郵件箱。顯然,這個工作量很大。
結果是它把大量的時間都花在做索引和發送郵件中了。做貼文的索引是一項很耗時的操作,而發送電子郵件給訂閱都需要連接到SMTP服務,然後給每個訂閱者都發一封郵件,隨著訂閱用戶的增加,發送郵件的時間會更長。
索引和發郵件並不需要在每次請求時觸發,理想狀態下,我們想要批量的處理這些操作,每次只發25封郵件或每隔5分鐘把所有的要發的新郵件發一次。我們決定使用與資料庫原型快取一樣的程式碼,但失敗了,所以又不得不回到VS.NET 2005。
我們在System.Threading命名空間下找到了Timer類,這個類別非常有用,但很少人知道,Web開發人員則更少有人知道了。一旦他建立了該類別的實例,每隔一個指定的時間,Timer類別就會從線程池中的一個線程中呼叫指定的回調函數。這意味著你的asp.net應用程式可以在沒有請求的時候也可以運行。這就是後以處理的解決方案。你就可以讓做索引和發郵件工作在後台運行,而不是在每次請求的時候必須執行。
後台運行的技術有兩個問題,第一是,當你的應用程式網域卸載後,Timer類別實例就會停止運作了。也就是不會呼叫回調方法了。另外,因為CLR的每個進程中都有許多的執行緒在運行,你將很難讓Timer獲得一個執行緒來執行它,或能執行它,但會延時。 Asp.net層要盡量少的使用這種技術,以減少行程中執行緒的數量,或只讓請求用一小部分的執行緒。當然如果你有大量的非同步工作的話,那就只能用它了。
這裡沒有足夠的空間有貼程式碼,你可以從http://www.rob-howard.net/中下載範例程序,請下載Blackbelt TechEd 2004的範例程式。
七、 頁面輸出快取和代理服務
Asp.net是你的介面層(或者說應該是),它包含頁面,用戶控件,伺服器控制項(HttpHandlers 和HttpModules)以及它們生成的內容。如果你有一個Asp.net頁面用來輸出html,xml,imgae或是其它的數據,對每一個請求你都用程式碼來產生相同的輸出內容,你就很有必要考慮用頁面輸出快取了。
你只要簡單的把下面的這一行程式碼複製到你的頁面中就可以實現了:
<%@ PageOutputCache VaryByParams=”none” Duration=”60” %>
你就可以有效的利用第一次請求裡產生的頁面輸出快取內容,60秒後重新產生一道頁面內容。這種技術其實也是運用一些低層的Cache API來實作。用頁面輸出快取有幾個參數可以配置,如上面所說的VaryByParams參數,該參數表示何時觸發重輸出的條件,也可以指定在Http Get或Http Post 請求模式下快取輸出。例如當我們設定該參數為VaryByParams=”Report”的時候,default.aspx?Report=1或default.aspx?Report=2請求的輸出都會被快取起來。參數的值可以是多個用分號隔開參數。
許多人都沒有意識到當用頁面輸出快取的時候,asp.net也會產生HTTP頭集(HTTP Header)保存在下游的快取伺服器中,這些資訊可以用於Microsoft Internet安全性中以及加速伺服器的回應速度。當HTTP快取的頭被重置時,請求的內容會被緩在網路資源中,當客戶端再次要求該內容時,就不會再從來源伺服器上取得內容了,而直接從快取中取得內容。
雖然用頁面輸出快取不提高你的應用程式效能,但它能減少了從的伺服器載入已快取頁面內容的次數。當然,這僅限於快取匿名用戶可以訪問的頁面。因為一旦頁面被快取後,就不能再執行授權操作了。
八、 用IIS6.0的Kernel Caching
如果你的應用程式沒用運行在IIS6.0(windows server 2003)中,那麼你就失去了一些很好的提升應用程式效能的方法。在第七個方法中,我講了用頁面輸出快取來提高應用程式的效能的方法。在IIS5.0中,當一個請求到來到IIS後,IIS會把它轉給asp.net,當應用了頁面輸出快取時,ASP.NET中的HttpHandler會接到該請求,HttpHandler從快取中把內容取出並返回。
如果你用的是IIS6.0,它有一個非常好的功能就是Kernel Caching,而且你不必修改asp.net程式中任何程式碼。當asp.net接到一個已快取的請求,IIS的Kernel Cache會從快取中得到它的一份拷貝。當從網路中傳來一個請求的時,Kernel層會得到該請求,如果該請求被緩存起來了,就直接把緩存的資料返回,這樣就完工了。這意味著當你用IIS的Kernel Caching來快取頁面輸出時,你將獲得不可置信的效能提升。在開發VS.NET 2005的asp.net時有一點,我是專門負asp.net效能的程式經理,我的程式設計師用了這個方法,我看了所有日報表數據,發現用kernel model caching的結果總是最快的。它們的一個共同的特徵就是網路的請求和回應量很大,但IIS只佔用了5%的CPU資源。這是令人驚奇的。有許多讓你用IIS6.0的理由,但kernel cashing是最好的一個。
九、 用Gzip壓縮數據
除非你的CPU佔用率太高了,才有必要用提升伺服器效能的技巧。用gzip壓縮資料的方法可以減少你傳送到服務端的資料量,也可以提高頁面的運作速度,同時也減少了網路的流量。怎麼樣更好的壓縮數據取決於你要發送的數據,還有就是客戶端的瀏覽器支不支援(IIS把用gzip壓縮後的數據發送到客戶端,客戶端要支援gzip才能解析,IE6.0和Firefox都支援)。這樣你的伺服器每秒能多回應一些請求,同樣,你也減少了發送回應的資料量,也就能多發送一些請求了。
好消息,gzip壓縮已經被整合在IIS6.0中了,它比IIS5.0中gzip更好。不幸的是,在IIS6.0中啟用gzip壓縮,你不能在IIS6.0的屬性對話中設定。 IIS開發團隊把gzip壓縮功能開發出來了,但他們忘了在管理員視窗中讓管理員能很方便的啟用它。要啟用gzip壓縮,你只能深入IIS6.0的xml設定檔中修改它的設定。
除了閱讀本文以外,只好再看看Brad Wilson寫的<<IIS6 壓縮>>一文( http://www.dotnetdevs.com/articles/IIS6compression.aspx );另外還有一篇介紹aspx壓縮基礎知識的文章, Enable ASPX Compression in IIS。但要注意,在IIS6中動態壓縮和kernel cashing是互斥的。
十、 伺服器控制項的ViewState
ViewState是asp.net中的特性,它用來把生成頁面要用的一狀態值保存在一個隱藏域中。當頁面被回傳到伺服器時,伺服器要解析,校驗和應用ViewState中的資料以還原頁面的控制項樹。 ViewState是一個非常有用的特性,它能持久化客戶端的狀態而不用cookie或伺服器的記憶體。大部分的伺服器控制項都是用ViewState來持久化那些在頁面中與使用者互動的元素的狀態值。例如,用以儲存用於分頁的目前頁的頁碼。
用ViewState會帶來一些負面的影響。首先,它加大的伺服器的回應和請求的時間。其次,每次回傳時都增加了序列化和反序列化資料的時間。最後,它還消耗了伺服器更多的記憶體。
許多的伺服器控制項很趨於使用ViewState,例如眾所周知的DataGrid,而有時是沒有必須使用的。預設是允許使用ViewState的,如果你不想使用ViewState的話,你可以在控製或頁面層級把關閉它。在控制項中,你只要把EnableViewState屬性設為False就可以了;你也可以在頁面中設置,使它的範圍擴展到整個頁面中:
<%@ Page EnableViewState=”false” %>
如果頁面無需回傳或每次請求頁面只是呈現控制項。你就應該在頁面層級中把ViewState關掉。
總結
我只是提供我幾個我認為有助於提高寫入高效能的asp.net應用程式的技巧,本文提到的提高asp.net效能的技巧只是一個起步,更多的資訊請參考《Improving ASP.NET Performance》一書。只有透過自己的實踐,你才能找到對你的專案最有幫助的技巧。然而,在你的開發旅程中,這些技巧可以扮演一些指導性的角色。在軟體開發中,這些都不是絕對有用的,因為各個專案都不一樣。