使用ASP.NET 編寫Web 應用程式的簡單程度令人不敢相信。正因為如此簡單,所以許多開發人員就不會花時間來設計其應用程式的結構,以獲得更好的效能了。在本文中,我將講述10 個用於編寫高效能Web 應用程式的技巧。但是我並不會將這些建議僅限於ASP.NET 應用程序,因為這些應用程式只是Web 應用程式的一部分。本文不作為Web應用程式進行效能調整的權威性指南— 一整本書恐怕都無法輕鬆講清楚這個問題。請將本文視為一個很好的起點。
成為工作狂之前,我原來喜歡攀岩。在進行任何大型攀岩活動之前,我都會先仔細查看指南中的路線,閱讀以前遊客提出的建議。但是,無論指南怎麼好,您都需要真正的攀岩體驗,然後才能嘗試一個特別具有挑戰性的攀登。與之相似,當您面臨修復效能問題或執行高吞吐量網站的問題時,您只能學習如何編寫高效能Web 應用程式。
我的個人體驗來自在Microsoft 的ASP.NET 部門作為基礎架構程式經理的經驗,在此期間我運行和管理www.ASP.NET ,幫助設計社群伺服器的結構,社群伺服器是幾個著名
ASP.NET 應用程式(組合到一個平台的ASP.NET Forums、.Text 和nGallery)。我確信有些曾經幫助過我的技巧對您肯定也會有所幫助。
您應該考慮將應用程式分為幾個邏輯層。您可能聽過3 層(或n 層)物理體系結構一詞。這些通常都是規定好的體系結構方式,將功能在進程和/或硬體之間進行了物理分離。當系統需要擴大時,可以輕鬆地添加更多的硬體。但是會出現一個與進程和機器跳躍相關的性能下降,因此應該避免。所以,如果可能的話,請盡量在同一個應用程式中一起執行ASP.NET 頁及其相關元件。
因為程式碼分離以及層之間的邊界,所以使用Web 服務或遠端處理將會使得效能下降20% 甚至更多。
資料層有點與眾不同,因為通常情況下,最好具有專用於資料庫的硬體。然而進程跳躍到資料庫的成本依然很高,因此資料層的效能是您在優化程式碼時首先要考慮的問題。
在深入應用程式的效能修復問題之前,請先確保對應用程式進行剖析,以便找出具體的問題所在。主要效能計數器(如表示執行垃圾回收所需時間百分比的計數器)對於找出應用程式在哪些位置花費了其主要時間也非常有用。然而花費時間的位置通常非常不直觀。
本文講述了兩種類型的效能改善:大型最佳化(如使用ASP.NET 快取),和進行自身重複的小型最佳化。這些小型優化有時特別有趣。您對程式碼進行一點小小的更改,就會獲得很多很多時間。使用大型優化,您可能會看到整體效能的較大飛躍。而使用小型最佳化時,對於某個特定請求可能只會節省幾毫秒的時間,但是每天所有請求加起來,則可能會產生巨大的改善。
資料層效能
談到應用程式的效能調整,有一個試紙性的測試可用於對工作進行優先劃分:程式碼是否存取資料庫?如果是,頻率是怎樣的?請注意,這項相同測試也可應用於使用Web 服務或遠端處理的程式碼,但本文對這些內容未做敘述。
如果某個特定的程式碼路徑中必需進行資料庫請求,並且您認為要先優化其他領域(如字串操作),則請停止,然後執行此試紙性測試。如果您的效能問題不是非常嚴重的話,最好花一些時間來優化與資料庫、傳回的資料量、進出資料庫的往返頻率相關的花費時間。
在了解這些常規資訊之後,我們來看看可能有助於提高應用程式效能的十個技巧。首先,我要講述可能會造成最大改觀的變更。
技巧1 — 傳回多個結果集
仔細檢視您的資料庫程式碼,看是否存在多次進入資料庫的請求路徑。每個這樣的往返都會降低應用程式可以提供的每秒請求數量。透過在一個資料庫請求中傳回多個結果集,可以節省與資料庫進行通訊所需的總時間長度。同時因為減少了資料庫伺服器管理請求的工作,也會使得系統伸縮性更強。
雖然可以使用動態SQL 傳回多個結果集,但我首選使用預存程序。關於業務邏輯是否應該駐留於預存程序的問題還存在一些爭議,但是我認為,如果存儲過程中的邏輯可以約束返回資料的話(縮小資料集的大小、縮短網路上所花費時間,不必篩選邏輯層的數據),則應贊成這樣做。
使用SqlCommand 實例及其ExecuteReader 方法填入強類型的業務類別時,可以透過呼叫NextResult 將結果集指標向前移動。圖1 顯示了使用類型類別填入幾個ArrayList 的範例會話。只從資料庫傳回您需要的資料將進一步減少伺服器上的記憶體分配。
Figure 1 Extracting Multiple Resultsets from a DataReader
// read the first resultset
reader = command.ExecuteReader();
// read the data from that resultset
while (reader.Read()) {
suppliers.Add(PopulateSupplierFromIDataReader( reader ));
}
// read the next resultset
reader.NextResult();
// read the data from that second resultset
while (reader.Read()) {
products.Add(PopulateProductFromIDataReader( reader ));
}
技巧2 — 分頁的資料存取
ASP.NET DataGrid 有一個很好的功能:資料分頁支援。在DataGrid 中啟用分頁時,一次會顯示固定數量的記錄。另外,在DataGrid 的底部也會顯示分頁UI,以便在記錄之間進行導覽。此分頁UI 可讓您在所顯示的資料之間向前和向後導航,並且一次顯示固定數量的記錄。
還有一個小小的波折。使用DataGrid 的分頁需要所有資料與網格進行綁定。例如,您的資料層需要傳回所有數據,那麼DataGrid 就會基於目前頁篩選顯示的所有記錄。如果透過DataGrid 進行分頁時傳回了100,000 個記錄,那麼針對每個請求會放棄99,975 個記錄(假設每頁大小為25 個記錄)。當記錄的數量不斷增加時,應用程式的效能就會受到影響,因為針對每個請求必須發送越來越多的資料。
要編寫效能更好的分頁程式碼,一個極佳的方式是使用預存程序。圖2 顯示了針對Northwind 資料庫中的Orders 資料表進行分頁的範例預存程序。簡而言之,您此時要做的只是傳遞頁索引和頁大小。然後就會計算合適的結果集,並將其傳回。
Figure 2 Paging Through the Orders Table
CREATE PROCEDURE northwind_OrdersPaged
(
@PageIndex int,
@PageSize int
)
AS
BEGIN
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @RowsToReturn int
-- First set the rowcount
SET @RowsToReturn = @PageSize * (@PageIndex + 1)
SET ROWCOUNT @RowsToReturn
-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
-- Create a temp table to store the select results
CREATE TABLE #PageIndex
(
IndexId int IDENTITY (1, 1) NOT NULL,
OrderID int
)
-- Insert into the temp table
INSERT INTO #PageIndex (OrderID)
SELECT
OrderID
FROM
Orders
ORDER BY
OrderID DESC
-- Return total count
SELECT COUNT(OrderID) FROM Orders
-- Return paged results
SELECT
O.*
FROM
Orders O,
#PageIndex PageIndex
WHERE
O.OrderID = PageIndex.OrderID AND
PageIndex.IndexID > @PageLowerBound AND
PageIndex.IndexID < @PageUpperBound
ORDER BY
PageIndex.IndexID
END
在社群伺服器中,我們編寫了一個分頁伺服器控件,以完成所有的資料分頁。您將會看到,我使用的就是技巧1 中討論的理念,從一個預存程序傳回兩個結果集:記錄的總數和請求的資料。
傳回記錄的總數可能會根據所執行查詢的不同而有所變更。例如,WHERE 子句可用於約束傳回的資料。為了計算在分頁UI 中顯示的總頁數,必須了解要傳回記錄的總數。例如,如果總共有1,000,000 筆記錄,並且要使用一個WHERE 子句將其篩選為1000 筆記錄,那麼分頁邏輯就需要了解記錄的總數才能正確呈現分頁UI。
技巧3 — 連接池
在Web 應用程式和SQL Server之間設定TCP 連線可能是一個非常消耗資源的操作。 Microsoft 的開發人員到目前為止已經能夠使用連線池有一段時間了,這使得他們能夠重複使用資料庫連線。他們不是針對每個請求都設定一個新的TCP 連接,而是只在連接池中沒有任何連接時才設定新連接。當連接關閉時,它會返回連接池,在其中它會保持與資料庫的連接,而不是完全破壞該TCP 連接。
當然,您需要小心是否會出現洩漏連接。當您完成使用連線時,請務必關閉這些連線。再重複一次:無論任何人對Microsoft?.NET Framework 中的垃圾回收有什麼評論,請務必在完成使用連線時針對該連線明確呼叫Close 或Dispose。不要相信公共語言運行庫(CLR) 會在預先確定的時間為您清除和關閉連線。儘管CLR 最終會破壞該類,並強制連接關閉,但是當針對物件的垃圾回收真正發生時,並不能保證。
要以最優化的方式使用連接池,需要遵守一些規則。首先開啟連接,執行操作,然後關閉該連接。如果您必須如此的話,可以針對每個請求多次打開和關閉連接(最好應用技巧1),但是不要一直將連接保持打開狀態並使用各種不同的方法對其進行進出傳遞。第二,使用相同的連接字串(如果使用整合式驗證的話,也要使用相同的執行緒標識)。如果不使用相同的連接字串,例如根據登入的使用者自訂連接字串,那麼您將無法獲得連接池提供的同一個最佳化值。如果您使用整合式身分驗證,同時也要模擬大量用戶,連線池的效率也會大大下降。嘗試追蹤與連接池相關的任何效能問題時,.NET CLR 資料效能計數器可能非常有用。
每當應用程式連接資源時,如在另一個進程中執行的資料庫,您都應該專注於連接該資源所花時間、發送或檢索資料所花時間,以及往返的數量,從而進行最佳化。優化應用程式中任何種類的進程跳躍都是獲得更佳效能的首要一點。
應用層包含了連接資料層、將資料轉換為有意義類別實例和業務流程的邏輯。例如社群伺服器,您要在其中填充Forums 或Threads集合,應用業務規則(如權限);最重要的是要在其中執行快取邏輯。
技巧4 — ASP.NET 快取API
在編寫應用程式程式碼行之前,一個首要完成的操作是設計應用程式層的結構,以便最大化利用ASP.NET 快取功能。
如果您的元件要在ASP.NET 應用程式中執行,則只需在該應用程式專案中包含一個System.Web.dll 參考。當您需要存取該快取時,請使用HttpRuntime.Cache 屬性(透過Page.Cache 和HttpContext.Cache 也可存取這個物件)。
對於快取數據,有幾個規則。首先,如果資料可能會多次使用時,則這是使用快取的一個很好的備選情況。第二,如果資料是通用的,而不是特定於某個特定的請求或使用者時,則也是使用快取的一個很好的備選情況。如果資料是特定於使用者或請求的,但是壽命較長的話,仍然可以對其進行緩存,但是這種情況可能並不經常使用。第三,一個經常被忽略的規則是,有時可能您緩存得太多。通常在一個x86 電腦上,為了減少記憶體不足錯誤出現的機會,您會想要使用不高於800MB 的專用位元組運行進程。因此快取應該有個限度。換句話說,您可能能夠重複使用某個計算結果,但是如果該計算採用10 個參數的話,您可能要嘗試快取10 個排列,這樣有可能給您帶來麻煩。一個要求ASP.NET 最常見的支援是由於過度快取引起的記憶體不足錯誤,尤其是對於大型資料集。
快取有幾個極佳的功能,您需要對它們有所了解。首先,快取會實現最近最少使用的演算法,使得ASP.NET 能夠在記憶體運作效率較低的情況下強制快取清除- 從快取自動刪除未使用過的項目。第二,快取支援可以強制失效的過期依賴項。這些依賴項包括時間、金鑰和檔案。時間常常會用到,但是對於ASP.NET 2.0,引入了一個功能更強的新失效類型:資料庫快取失效。它指的是當資料庫中的資料發生變化時自動刪除快取中的項目。有關資料庫快取失效的詳細信息,請參閱MSDN?Magazine 2004 年7 月的Dino Esposito Cutting Edge 專欄。若要了解快取的體系結構,請參考下圖。
技巧6 — 背景處理
通往程式碼的路徑應該盡可能快速,是嗎?可能有時您會覺得針對每個請求執行的或每n 個請求執行一次的任務所需資源非常多。發送電子郵件或分析和驗證傳入資料就是這樣的一些例子。
剖析ASP.NET Forums 1.0 並重新建置組成社群伺服器的內容時,我們發現新增張貼的程式碼路徑非常慢。每次新增張貼時,應用程式首先需要確保沒有重複的張貼,然後必須使用"壞詞"篩選器分析該張貼,分析張貼的字元圖釋,對張貼添加標記並進行索引,請求時將張貼添加到合適的隊列,驗證附件,最終張貼之後,立即向所有訂閱者發出電子郵件通知。很清楚,這涉及很多操作。
經研究發現,大多數時間都花在了索引邏輯和發送電子郵件上。對張貼進行索引是一個非常耗時的操作,人們發現內建的System.Web.Mail 功能要連接SMYP 伺服器,然後連續發送電子郵件。當某個特定張貼或主題領域的訂閱者數量增加時,執行AddPost 功能所需的時間也越來越長。
並不需要針對每個請求都進行電子郵件索引。理想情況下,我們想要將此操作進行批次處理,一次索引25 個張貼或每五分鐘發送一次所有電子郵件。我們決定使用先前用於對資料快取失效進行原型設計的程式碼,這個失效是用於最終進入Visual Studio? 2005 的內容的。
System.Threading 命名空間中的Timer 類別非常有用,但是在.NET Framework 中不是很有名,至少對於Web 開發人員來說是如此。創建之後,這個Timer 類別將以一個可配置的間隔針對ThreadPool 中的某個執行緒呼叫指定的回呼。這表示,您可以對程式碼進行設置,使其能夠在沒有對ASP.NET 應用程式進行傳入請求的情況下得以執行,這是後台處理的理想情況。您也可以在此後台進程中執行如索引或傳送電子郵件之類的操作。
但是,這項技術有幾個問題。如果應用程式網域卸載,則該計時器執行個體將停止觸發其事件。另外,因為CLR 對於每個進程的線程數量具有一個硬性標準,所以可能會出現這樣的情況:伺服器負載很重,其中計時器可能沒有可在其基礎上得以完成的線程,在某種程度上可能會造成延遲。 ASP.NET 透過在進程中保留一定數量的可用線程,並且僅使用總線程的一部分用於請求處理,試圖將上述情況發生的機會降到最低。但是,如果您具有很多非同步操作時,這可能就是一個問題了。
這裡沒有足夠的空間來放置該程式碼,但是您可以下載一個可以理解的範例,網址是www.rob-howard.net。請了解Blackbelt TechEd 2004 簡報中的幻燈片和簡報。
技巧7 — 頁輸出快取和代理伺服器
ASP.NET 是您的表示層(或應該是您的表示層);它由頁面、使用者控制項、伺服器控制項(HttpHandlers 和HttpModules)以及它們產生的內容組成。如果您具有一個ASP.NET 頁,它會產生輸出(HTML、XML、映像或任何其他資料),並且您針對每個請求執行此程式碼時,它都會產生相同的輸出,那麼您就擁有一個可用於頁輸出快取的絕佳備選內容。
將此行內容新增頁面的最上端<%@ Page OutputCache VaryByParams="none" Duration="60" %>
就可以有效地為此頁產生一次輸出,然後對它進行多次重複使用,時間最長為60 秒,此時該頁將重新執行,輸出也將再一次添加到ASP.NET 快取。透過使用一些低階程式化API 也可以完成此行為。對於輸出快取有幾個可配置的設置,如剛剛講到的VaryByParams 屬性。 VaryByParams 剛好被要求到,但也允許您指定HTTP GET 或HTTP POST 參數來更改快取項目。例如,只需設定VaryByParam="Report" 即可對default.aspx?Report=1 或default.aspx?Report=2 進行輸出快取。透過指定一個以分號分隔的列表,還可以指定其他參數。
很多人都不知道何時使用輸出快取,ASP.NET 頁還會產生一些位於快取伺服器下游的HTTP 標頭,如Microsoft Internet Security and Acceleration Server 或Akamai 使用的標頭。設定了HTTP 快取標頭之後,可以在這些網路資源上對文件進行緩存,用戶端請求也可在不必傳回原始伺服器的情況下得以滿足。
因此,使用頁輸出快取不會使得您的應用程式效率更高,但是它可能會減少伺服器上的負載,因為下游快取技術會快取文件。當然,這可能只是匿名內容;一旦它成為下游之後,您就再也不會看到這些請求,並且再也無法執行身份驗證以阻止對它的訪問了。
技巧8 — 運行IIS 6.0(只要用於核心快取)
如果您未執行IIS 6.0 (Windows Server? 2003),那麼您就錯過了Microsoft Web 伺服器中的一些很好的效能增強。在技巧7 中,我討論了輸出快取。在IIS 5.0 中,請求是透過IIS 然後進入ASP.NET 的。當涉及快取時,ASP.NET 中的HttpModule 會接收該請求,並傳回快取中的內容。
如果您正在使用IIS 6.0,您會發現一個很好的小功能,稱為核心緩存,它不需要對ASP.NET 進行任何程式碼更改。當請求由ASP.NET 進行輸出快取時,IIS 核心快取會接收快取資料的一個副本。當請求來自網路驅動程式時,核心層級的驅動程式(無上下文切換到使用者模式)就會接收該請求,如果經過了緩存,則會將快取的資料刷新到回應,然後完成執行。這就表示,當您將核心模式快取與IIS 和ASP.NET 輸出快取一起使用時,就會看到令人不敢相信的效能結果。在ASP.NET 的Visual Studio 2005 開發過程中,我一度是負責ASP.NET 效能的程式經理。開發人員完成具體工作,但是我要看到每天進行的所有報告。內核模式快取結果總是最有趣的。最常見的特徵是網路充滿了請求/回應,而IIS 運行時的CPU 使用率只有大約5%。這太令人震驚了!當然使用IIS 6.0 還有一些其他原因,但核心模式快取是其中最明顯的一個。
技巧9 — 使用Gzip 壓縮
雖然使用gzip 不一定是伺服器效能技巧(因為您可能會看到CPU 使用率的提高),但是使用gzip 壓縮可以減少伺服器發送的位元組數量。這就使人們覺得頁速度加快了,也減少了頻寬的用量。根據所發送資料、可以壓縮的程度以及客戶端瀏覽器是否支援(IIS只會向支援gzip 壓縮的客戶端發送經過gzip 壓縮的內容,如Internet Explorer 6.0 和Firefox),您的伺服器每秒可以服務於更多的請求。實際上,幾乎每當您減少所傳回的資料數量時,都會增加每秒請求數。
Gzip 壓縮已內建在IIS 6.0 中,其效能比IIS 5.0 中使用的gzip 壓縮要好的多,這是好消息。但不幸的是,當嘗試在IIS 6.0 中開啟gzip 壓縮時,您可能無法在IIS 的屬性對話中找到該設定。 IIS 小組在該伺服器中置入了卓越的gzip 功能,但忘了包含一個用於啟用該功能的管理UI。要啟用gzip 壓縮,您必須深入IIS 6.0 的XML 配置設定內部(這樣不會造成心臟虛弱)。順便提一句,這歸功於OrcsWeb 的Scott Forsyth,他幫助我提出了在OrcsWeb 上宿主的www.asp.net 伺服器的這個問題。
本文就不講述步驟了,請閱讀Brad Wilson 的文章,網址是IIS6 Compression。還有一篇關於為ASPX 啟用壓縮的知識庫文章,網址是Enable ASPX Compression in IIS。但您應該注意,由於一些實作細節,IIS 6.0 中不能同時存在動態壓縮和核心快取。
技巧10 — 伺服器控制項視圖狀態
視圖狀態是一個有趣的名稱,用來表示在所產生頁的隱藏輸出欄位中儲存一些狀態資料的ASP.NET。當該頁張貼回伺服器時,伺服器可以分析、驗證、並將此視圖狀態資料套用回該頁的控制項樹。視圖狀態是一個非常強大的功能,因為它允許狀態與客戶端一起保持,並且它不需要cookie 或伺服器記憶體即可保存此狀態。許多ASP.NET 伺服器控制項都使用視圖狀態來保持在與頁元素進行互動期間所建立的設置,例如儲存對資料進行分頁時顯示的目前頁。
然而使用視圖狀態也有一些缺點。首先,當服務或請求頁時,它都會增加頁的總負載。將張貼回伺服器的視圖狀態資料序列化或取消序列化時,也會發生額外的開銷。最後,視圖狀態會增加伺服器上的記憶體分配。
幾個伺服器控制項有著過度使用視圖狀態的趨勢,即使在不需要的情況下也要使用它,其中最著名的是DataGrid。 ViewState 屬性的預設行為是啟用,但是如果您不需要,則可以在控制項或頁面層級關閉。在控制項內,只需將EnableViewState 屬性設為false,或在頁中使用下列設定即可對其進行全域設定:
<%@ Page EnableViewState="false" %>
如果您不回發頁,或總是針對每個請求重新產生頁上的控件,則應在頁面層級停用視圖狀態。
我為您講述了一些我認為在編寫高效能ASP.NET 應用程式時有所幫助的技巧。正如我在本文前面部分提到的那樣,這是一個初步指南,並不是ASP.NET 效能的最後結果。 (有關改善ASP.NET 應用程式效能的信息,請參閱Improving ASP.NET Performance。)只有透過自己的親身體驗才能找出解決具體效能問題的最佳方法。但是,在您的旅程中,這些技巧應該會為您提供一些好的指南。在軟體開發中,幾乎沒有絕對的東西;每個應用程式都是唯一的。