作者:Rob Howard 譯:寒帶魚
這篇文章討論了:
·一般ASP.NET效能的秘密
·能提高ASP.NET表現的有用的技巧和竅門
·在ASP.NET中使用資料庫的建議
·ASP.NET中的快取和後台處理
使用ASP.NET編寫一個網頁應用程式是難以置信的簡單的。太簡單了,以至於許多開發者都不花時間來建立他們的應用程式來達到很好的表現。在這篇文章裡,我將為編寫高效能的網路應用程式推薦10個技巧。我不會講我的論述局限於ASP.NET應用程序,因為ASP.NET應用程式只是Web應用程式的子集而已。這篇文章不會是針對優化Web應用程式的效能的權威性指導——一本完整的書可以輕鬆的做到這一點。相反,我們應該把這篇文章當成一個好的起點。
在成為工作狂以前,我會常去攀岩。在做任何攀岩活動之前,我更願意看看旅遊指南裡面的路線,再讀讀那些曾經到過峰頂的人做的推薦。但是,不管旅行指南寫的有多好,在嘗試一個有挑戰性的目標之前,您都需要有實際的攀岩經驗。與之相似,當您面臨修復效能問題或執行高吞吐量網站的問題時,您只能學習如何編寫高效能Web 應用程式。
我的個人經驗來自在微軟的ASP.NET團隊中擔任過一名基礎程式經理的經歷,維護和管理www.asp.net ,還有幫助構架Community Server,它是幾個著名的ASP.NET應用程式(ASP.NET Forums,.Text,和連接到一個平台的nGallery)的下一個版本。我相信這些曾經幫助過我技巧中的一些也會對您有用的。
您應該考慮把您的應用程式分開為幾個邏輯層次。您可能已經聽過3層(或n層)體系結構。這些通常都是規定的結構模式,它們將業務和(或)硬體從物理上進行了功能劃分。如果系統需要更大的規模,更多的硬體可以輕鬆的加進來。然而,那會產生一個與業務和機器跳躍相關聯的性能下降,因此我們應該避免它。所以只要有可能,盡量在同一個應用程式中執行ASP.NET頁面和頁面的相關元件。
因為程式碼的分離和層次之間的邊界,使用Web服務或遠端處理會降低效能20%甚至更多。
資料層有點與眾不同,因為通常情況下,最好具有專用於資料庫的硬體。然而,然而進程跳躍到資料庫的成本依然很高,因此在資料層的效能是您優化程式碼時應該首先考慮的。
在投入修復您的應用程式的效能問題之前,請確保您要先分析您的應用程式來發現問題的根源所在。關鍵效能計數器(例如那個指示在執行垃圾收集過程中花費的時間百分比的計數器)在找出應用程式在哪裡花費了主要的時間時也是非常有用的。雖然那些花時間的地方經常是不那麼直觀的。
在這篇文章中我討論了兩種改進性能的方法:大塊的優化,例如使用ASP.NET緩存,還有小塊的優化,它們經常重複出現。這些小塊的最佳化有時是最有趣的。您對程式碼的一個小的修改會被呼叫成千上萬次。對大塊的優化,您可能會發現整個的性能有了一個大的飛躍。對小塊的最佳化,您可能會縮減了對一個給定請求的幾微秒的時間,但是如果把每天的所有的請求累積起來,性能就會得到一個意想不到的改進。
資料層中的效能
當您要開始優化一個應用程式的效能的時候,有一個決定性的測試您可以優先考慮使用:程式碼是否要存取資料庫?如果是,多長時間造訪一次?注意這個測試也可以應用到那些使用Web服務或遠端控制的程式碼中,但是我不會在這篇文章中涉及那些內容。
如果在您的程式碼中的某個程式碼路徑中要求一個資料庫請求,而您發現其他地方您想要優先優化,例如字串操作,那麼請停下來然後先執行關鍵性的測試。除非您有一個性能實在糟糕的問題要處理,否則您的時間會得到更好的利用,如果您把時間花在優化數據庫連接的時間,返回的數據量,還有您作的往返數據庫的操作中。
現在我已經總體介紹了相關的信息,以下讓我們來看看10條幫您的應用程式表現更好的技巧。我會從那些對改善性能效果最明顯的地方開始說。
技巧1——傳回多個結果集
查看您的資料庫程式碼,看看您是否有存取資料庫多於一次的請求路徑(request paths)。每個這樣的往返都回降低您的應用程式每秒可以服務的請求的數量。透過在一次資料庫請求中傳回多個結果集,您可以減少資料庫通訊消耗的總時間。在您減少了資料庫伺服器管理的請求之後,您也會使您的系統更具可升級性。
一般您可以使用動態SQL語句來傳回多個結果集,我比較喜歡用預存程序。是否應該把業務邏輯放在儲存過程中是有爭議的,但我認為如果一個儲存過程中的邏輯可以限制返回的資料(減少資料集的大小,花在網路連接上的時間,並且不需要過濾邏輯層的資料),那它就是好東西。
使用一個SqlCommand實例和它的ExecuteReader方法來產生強類型的業務類,您可以透過呼叫NextResult讓結果集指標向前移動。圖1展示了一個使用定義的類別產生幾個ArrayList的範例會話。只從資料庫傳回您需要的資料會顯著減少您伺服器上的記憶體申請。
1// read the first resultset
2reader = command.ExecuteReader();
3
4// read the data from that resultset
5while (reader.Read()) {
6 suppliers.Add(PopulateSupplierFromIDataReader( reader ));
7}
8
9// read the next resultset
10reader.NextResult();
11
12// read the data from that second resultset
13while (reader.Read()) {
14 products.Add(PopulateProductFromIDataReader( reader ));
15}
16
17
技巧2——分頁資料訪問
ASP.NET的DataGrid提供了一個非常棒的能力:對資料分頁的支援。當在DataGrid中設定了分頁,那麼將一次顯示一個特定數目的結果。此外,用來在結果之間導覽的分頁UI也會在DataGrid的底部顯示出來。分頁UI可讓您在顯示的資料之間向前導航或向後導航,每頁顯示特定數目的結果。
但是有一個小問題。使用DataGrid分頁時需要所有的資料都綁定到表格。例如,您的資料層會需要傳回所有數據,然後DataGrid要根據目前頁填入所有要顯示的記錄。如果當您在使用DataGrid分頁時傳回了100,000筆記錄,每次請求都會丟棄99,975筆記錄(假設每頁的容量是25筆記錄)。當記錄的數量不斷增長時,應用程式的效能會受到很大的影響,因為每次請求都必須傳回越來越多的資料。
一個寫出更好的分頁程式碼的辦法是使用預存程序。圖2顯示了一個範例預存程序,它為Nothwind資料庫中的Orders資料表分頁。總的來說,在這裡所有您需要做的就是傳入頁的索引和頁的容量。資料庫會計算出適當的結果集然後傳回它們。
1CREATE PROCEDURE northwind_OrdersPaged
2(
3 @PageIndex int,
4 @PageSize int
5)
6AS
7BEGIN
8DECLARE @PageLowerBound int
9DECLARE @PageUpperBound int
10DECLARE @RowsToReturn int
11
12-- First set the rowcount
13SET @RowsToReturn = @PageSize * (@PageIndex + 1)
14SET ROWCOUNT @RowsToReturn
15
16-- Set the page bounds
17SET @PageLowerBound = @PageSize * @PageIndex
18SET @PageUpperBound = @PageLowerBound + @PageSize + 1
19
20-- Create a temp table to store the select results
21CREATE TABLE #PageIndex
22(
23 IndexId int IDENTITY (1, 1) NOT NULL,
24 OrderID int
25)
26
27-- Insert into the temp table
28INSERT INTO #PageIndex (OrderID)
29SELECT
30 OrderID
31FROM
32 Orders
33ORDER BY
34 OrderID DESC
35
36-- Return total count
37SELECT COUNT(OrderID) FROM Orders
38
39-- Return paged results
40SELECT
41 O.*
42FROM
43 Orders O,
44 #PageIndex PageIndex
45WHERE
46 O.OrderID = PageIndex.OrderID AND
47 PageIndex.IndexID > @PageLowerBound AND
48 PageIndex.IndexID < @PageUpperBound
49ORDER BY
50 PageIndex.IndexID
51
52END
53
54
在社區服務期間中,我們寫了一個分頁服務端控制項來做這些資料分頁。您會發現我在使用技巧1中討論過的思想,從一個預存程序傳回兩個結果集:紀錄總數和請求的資料。
傳回的記錄總數可以根據執行的請求而有所不同。例如,一個WHERE分句可以用來約束傳回的資料。我們必須知道要傳回的記錄總數,以計算要在分頁UI中顯示的總的頁數。例如,如果有1,000,000筆總的記錄數,而一個WHERE分句用來把這些記錄過濾為1,000筆記錄,分頁邏輯需要知道總的記錄數來恰當的提交分頁UI。
技巧3——連結池
在您的網路應用程式和SQL Server之間建立TCP連線會是一個昂貴的操作。 Microsoft的開發者已經利用連線池有一段時間了,這允許他們重複使用與資料庫的連線。與其為每個請求建立一個新的TCP連接,不如只有在連接池中沒有一個可用的連接的時候才建立一個新的連接。當連接關閉後,它會回到連接池中——它還保持與資料庫的連接,而不是完全銷毀那個TCP連接。
當然您需要小心洩漏的連接。總是關閉您的連線在您使用完它們時。我重複一遍:不管誰說了關於Microsoft .NET框架的垃圾回收機制的什麼話,當您使用完時,您務必總是對您的連接明確調用Close或Dispose方法。不要相信通用語言運行時(CLR)會在一個預定的時間為您清理和關閉您的連線。 CLR最終會銷毀類別並且強迫連線關閉,但您不能保證何時在物件上的垃圾回收機制會真正執行。
要使用連接池達到最佳效果,您需要遵循幾個規則。第一,打開一個連接,完成工作,然後關閉連接。如果您必須(最好應用技巧1)為每個請求打開和關閉幾次連接也是可以的,這比一直開著連接然後把它傳遞給幾個不同的方法要好得多。第二,使用同一個連接字串(如果您正在使用整合身分認證,當然還需要有相同的執行緒標識)。如果您不使用同一個連接字串,例如基於登入的使用者的不同自訂連接字串,您就無法得到連接池提供的相同的最優值。而且如果您在模仿大量的使用者時使用了整合式身份驗證,您的連線池的效率也會降低很多。在嘗試追蹤任何與連接池相關的效能問題時,.NET CLR資料效能計數器會很有用的。
不論何時您的應用程式連接一個資源,例如一個資料庫,或在另一個進程中運行,您都應該透過將注意力集中到連接到資源所花費的時間上,發送和接受資料花費的時間,還有往返與資料庫的次數來進行最佳化。優化您的應用程式中的任何類型的進程跳轉(process hop)都是開始達到更好效能的第一步。
應用層包含連接到您的資料層的邏輯,並且將資料轉換為有意義的類別實例和邏輯過程。例如,在社群伺服器中,這裡是您產生一個論壇或執行緒集合,並且應用業務規則例如許可的地方;更重要的是這裡是執行緩衝邏輯的地方。
技巧4——ASP.NET緩衝API
在您開始編寫應用程式的第一行程式碼之前要考慮的第一件事情是,架構應用層來最大化並且利用ASP.NET的快取特性。
如果您的元件運行在一個ASP.NET應用程式之中,您只需要在您的應用程式專案中簡單的參考System.Web.dll就可以了。當您需要存取快取時,使用HttpRuntime.Cache屬性(這個物件也可以透過Page.Cache和HttpContext.Cache來存取)。
使用快取資料有幾個原則。第一,如果資料可以多次使用,那麼快取它就是一個好的選擇。第二,如果資料是通用的而不是給特定的請求或使用者使用的,那麼快取它就是一個非常好的選擇。如果資料是使用者或請求特定的,但是他的生存期是很長的,那麼它也可以被緩存,但是可能不會經常使用到。第三,一個經常被忽略的原則是,有時您可以快取的太多了。通常在一台x86電腦上,為了減少發生記憶體不足(out-of-memory)錯誤的可能性,您會想要執行一個使用不超過800MB私有位元組的進程。因此,緩存應該受到限制。換句話說,您可能需要重新使用一次計算的結果,但是如果那個計算需要十個參數,您可能需要嘗試快取10個排列,而這可能會給您帶來麻煩。由於過度快取造成的記憶體不足錯誤是ASP.NET中最常見的,特別是對於大資料集的情況。
快取有幾個極佳的功能,您需要對它們有所了解。首先,快取會實現最近最少使用的演算法,使得ASP.NET 能夠在記憶體運行效率較低的情況下強制快取清除——從快取自動刪除未使用過的項目。第二,快取支援可以強制失效的過期依賴項。這些依賴項包括時間、鍵和檔案。時間常常會用到,但是對於ASP.NET 2.0,引入了一個功能更強的新失效類型:資料庫快取失效。它指的是當資料庫中的資料發生變化時自動刪除快取中的項目。有關資料庫快取失效的詳細信息,請參閱MSDN Magazine 2004 年7 月的Dino Esposito Cutting Edge 專欄。若要了解快取的體系結構,請參考圖3。
技巧5 — 每請求緩存
在本文前面部分,我提到了經常遍歷程式碼路徑的一些小改善可以獲得較大的整體效能效益。對於這些小改善,其中有一個絕對是我的最愛,我將其稱之為「每個請求快取」。
快取API 的設計目的是為了將資料快取較長的一段時間,或快取至滿足某些條件時,但每請求快取則意味著只將資料快取為該請求的持續時間。對於每個請求,要經常存取某個特定的程式碼路徑,但是資料卻只需提取、套用、修改或更新一次。這聽起來有些理論化,那我們來舉一個具體的例子。
在社群伺服器的論壇應用程式中,頁面上使用的每個伺服器控制項都需要個人化的資料來決定使用什麼外觀、使用什麼樣式表,以及其他個人化資料。這些資料中有些可以長期緩存,但是有些資料只針對每個請求提取一次,然後在執行該請求期間對其重用多次,如要用於控制項的外觀。
為了達到每個請求緩存,請使用ASP.NET HttpContext。對於每個請求,都會建立一個HttpContext 實例,在該請求期間從HttpContext.Current 屬性的任何位置都可存取該實例。此HttpContext 類別具有一個特殊的Items 集合屬性;新增至此Items 集合的物件和資料只在該請求持續期間內進行快取。就像您可以使用快取來儲存經常存取的資料一樣,您也可以使用HttpContext.Items 來儲存只基於每個請求使用的資料。它背後的邏輯非常簡單:資料在它不存在的時候加入HttpContext.Items 集合,在後來的查找中,只是傳回HttpContext.Items 中的資料。
技巧6 — 背景處理
通往程式碼的路徑應該盡可能快速,是嗎?可能有時您會發現您正在執行的針對每個請求執行的或每n 個請求執行一次的任務所需資源非常多。發送電子郵件或分析和驗證傳入資料就是這樣的一些例子。
剖析ASP.NET Forums 1.0 並重新建置組成社群伺服器的內容時,我們發現發布新貼文的程式碼路徑非常慢。每次發布新帖子的時候,應用程式首先需要確保沒有重複的帖子,然後必須使用“壞詞”篩選器分析該帖子,分析帖子的字符圖釋,對帖子添加標記並進行索引,請求時將帖子新增至適當的佇列,驗證附件,最終在貼文發布之後,立即向所有訂閱者發出電子郵件通知。很清楚,這涉及很多操作。
經研究發現,大多數時間都花在了索引邏輯和發送電子郵件上。對貼文進行索引是一個非常耗時的操作,人們發現內建的System.Web.Mail 功能要連接SMTP 伺服器,然後連續發送電子郵件。當某個特定貼文或主題領域的訂閱者數量增加時,執行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 和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。)只有透過自己的親身體驗才能找出解決具體效能問題的最佳方法。但是,在您的旅程中,這些技巧應該會為您提供一些好的指南。在軟體開發中,幾乎沒有絕對的東西;每個應用程式都是唯一的。
請參閱提要列「Common Performance Myths」。
Rob Howard 是Telligent Systems 的創辦人,專門從事高效能Web 應用程式、知識庫管理和協作系統方面的工作。 Rob 以前受僱於Microsoft,他在那裡幫助設計了ASP.NET 1.0、1.1 和2.0 的基礎結構。若要聯絡Rob,請造訪[email protected] 。
原文連結: http://msdn.microsoft.com/msdnmag/issues/05/01/ASPNETPerformance/default.aspx