使用 ASP.NET 編寫 Web 應(yīng)用程序的簡(jiǎn)單程度令人不敢相信。正因?yàn)槿绱撕?jiǎn)單,所以很多開(kāi)發(fā)人員就不會(huì)花時(shí)間來(lái)設(shè)計(jì)其應(yīng)用程序的結(jié)構(gòu),以獲得更好的性能了。在本文中,我將講述 10 個(gè)用于編寫高性能 Web 應(yīng)用程序的技巧。但是我并不會(huì)將這些建議僅局限于 ASP.NET 應(yīng)用程序,因?yàn)檫@些應(yīng)用程序只是 Web 應(yīng)用程序的一部分。本文不作為對(duì) Web 應(yīng)用程序進(jìn)行性能調(diào)整的權(quán)威性指南 — 一整本書恐怕都無(wú)法輕松講清楚這個(gè)問(wèn)題。請(qǐng)將本文視作一個(gè)很好的起點(diǎn)。
成為工作狂之前,我原來(lái)喜歡攀巖。在進(jìn)行任何大型攀巖活動(dòng)之前,我都會(huì)首先仔細(xì)查看指南中的路線,閱讀以前游客提出的建議。但是,無(wú)論指南怎么好,您都需要真正的攀巖體驗(yàn),然后才能嘗試一個(gè)特別具有挑戰(zhàn)性的攀登。與之相似,當(dāng)您面臨修復(fù)性能問(wèn)題或者運(yùn)行一個(gè)高吞吐量站點(diǎn)的問(wèn)題時(shí),您只能學(xué)習(xí)如何編寫高性能 Web 應(yīng)用程序。
我的個(gè)人體驗(yàn)來(lái)自在 Microsoft 的 ASP.NET 部門作為基礎(chǔ)架構(gòu)程序經(jīng)理的經(jīng)驗(yàn),在此期間我運(yùn)行和管理 www.ASP.NET,幫助設(shè)計(jì)社區(qū)服務(wù)器的結(jié)構(gòu),社區(qū)服務(wù)器是幾個(gè)著名 ASP.NET 應(yīng)用程序(組合到一個(gè)平臺(tái)的 ASP.NET Forums、.Text 和 nGallery)。我確信有些曾經(jīng)幫助過(guò)我的技巧對(duì)您肯定也會(huì)有所幫助。
您應(yīng)該考慮將應(yīng)用程序分為幾個(gè)邏輯層。您可能聽(tīng)說(shuō)過(guò) 3 層(或者 n 層)物理體系結(jié)構(gòu)一詞。這些通常都是規(guī)定好的體系結(jié)構(gòu)方式,將功能在進(jìn)程和/或硬件之間進(jìn)行了物理分離。當(dāng)系統(tǒng)需要擴(kuò)大時(shí),可以很輕松地添加更多的硬件。但是會(huì)出現(xiàn)一個(gè)與進(jìn)程和機(jī)器跳躍相關(guān)的性能下降,因此應(yīng)該避免。所以,如果可能的話,請(qǐng)盡量在同一個(gè)應(yīng)用程序中一起運(yùn)行 ASP.NET 頁(yè)及其相關(guān)組件。
因?yàn)榇a分離以及層之間的邊界,所以使用 Web 服務(wù)或遠(yuǎn)程處理將會(huì)使得性能下降 20% 甚至更多。
數(shù)據(jù)層有點(diǎn)與眾不同,因?yàn)橥ǔG闆r下,最好具有專用于數(shù)據(jù)庫(kù)的硬件。然而進(jìn)程跳躍到數(shù)據(jù)庫(kù)的成本依然很高,因此數(shù)據(jù)層的性能是您在優(yōu)化代碼時(shí)首先要考慮的問(wèn)題。
在深入應(yīng)用程序的性能修復(fù)問(wèn)題之前,請(qǐng)首先確保對(duì)應(yīng)用程序進(jìn)行剖析,以便找出具體的問(wèn)題所在。主要性能計(jì)數(shù)器(如表示執(zhí)行垃圾回收所需時(shí)間百分比的計(jì)數(shù)器)對(duì)于找出應(yīng)用程序在哪些位置花費(fèi)了其主要時(shí)間也非常有用。然而花費(fèi)時(shí)間的位置通常非常不直觀。
本文講述了兩種類型的性能改善:大型優(yōu)化(如使用 ASP.NET 緩存),和進(jìn)行自身重復(fù)的小型優(yōu)化。這些小型優(yōu)化有時(shí)特別有意思。您對(duì)代碼進(jìn)行一點(diǎn)小小的更改,就會(huì)獲得很多很多時(shí)間。使用大型優(yōu)化,您可能會(huì)看到整體性能的較大飛躍。而使用小型優(yōu)化時(shí),對(duì)于某個(gè)特定請(qǐng)求可能只會(huì)節(jié)省幾毫秒的時(shí)間,但是每天所有請(qǐng)求加起來(lái),則可能會(huì)產(chǎn)生巨大的改善。
數(shù)據(jù)層性能
談到應(yīng)用程序的性能調(diào)整,有一個(gè)試紙性的測(cè)試可用來(lái)對(duì)工作進(jìn)行優(yōu)先級(jí)劃分:代碼是否訪問(wèn)數(shù)據(jù)庫(kù)?如果是,頻率是怎樣的?請(qǐng)注意,這一相同測(cè)試也可應(yīng)用于使用 Web 服務(wù)或遠(yuǎn)程處理的代碼,但是本文對(duì)這些內(nèi)容未做講述。
如果某個(gè)特定的代碼路徑中必需進(jìn)行數(shù)據(jù)庫(kù)請(qǐng)求,并且您認(rèn)為要首先優(yōu)化其他領(lǐng)域(如字符串操作),則請(qǐng)停止,然后執(zhí)行這個(gè)試紙性測(cè)試。如果您的性能問(wèn)題不是非常嚴(yán)重的話,最好花一些時(shí)間來(lái)優(yōu)化一下與數(shù)據(jù)庫(kù)、返回的數(shù)據(jù)量、進(jìn)出數(shù)據(jù)庫(kù)的往返頻率相關(guān)的花費(fèi)時(shí)間。
了解這些常規(guī)信息之后,我們來(lái)看一下可能會(huì)有助于提高應(yīng)用程序性能的十個(gè)技巧。首先,我要講述可能會(huì)引起最大改觀的更改。
技巧 1 — 返回多個(gè)結(jié)果集
仔細(xì)查看您的數(shù)據(jù)庫(kù)代碼,看是否存在多次進(jìn)入數(shù)據(jù)庫(kù)的請(qǐng)求路徑。每個(gè)這樣的往返都會(huì)降低應(yīng)用程序可以提供的每秒請(qǐng)求數(shù)量。通過(guò)在一個(gè)數(shù)據(jù)庫(kù)請(qǐng)求中返回多個(gè)結(jié)果集,可以節(jié)省與數(shù)據(jù)庫(kù)進(jìn)行通信所需的總時(shí)間長(zhǎng)度。同時(shí)因?yàn)闇p少了數(shù)據(jù)庫(kù)服務(wù)器管理請(qǐng)求的工作,還會(huì)使得系統(tǒng)伸縮性更強(qiáng)。
雖然可以使用動(dòng)態(tài) SQL 返回多個(gè)結(jié)果集,但是我首選使用存儲(chǔ)過(guò)程。關(guān)于業(yè)務(wù)邏輯是否應(yīng)該駐留于存儲(chǔ)過(guò)程的問(wèn)題還存在一些爭(zhēng)議,但是我認(rèn)為,如果存儲(chǔ)過(guò)程中的邏輯可以約束返回?cái)?shù)據(jù)的話(縮小數(shù)據(jù)集的大小、縮短網(wǎng)絡(luò)上所花費(fèi)時(shí)間,不必篩選邏輯層的數(shù)據(jù)),則應(yīng)贊成這樣做。
使用 SqlCommand 實(shí)例及其 ExecuteReader 方法填充強(qiáng)類型的業(yè)務(wù)類時(shí),可以通過(guò)調(diào)用 NextResult 將結(jié)果集指針向前移動(dòng)。圖 1 顯示了使用類型類填充幾個(gè) ArrayList 的示例會(huì)話。只從數(shù)據(jù)庫(kù)返回您需要的數(shù)據(jù)將進(jìn)一步減少服務(wù)器上的內(nèi)存分配。
技巧 2 — 分頁(yè)的數(shù)據(jù)訪問(wèn)
ASP.NET DataGrid 具有一個(gè)很好的功能:數(shù)據(jù)分頁(yè)支持。在 DataGrid 中啟用分頁(yè)時(shí),一次會(huì)顯示固定數(shù)量的記錄。另外,在 DataGrid 的底部還會(huì)顯示分頁(yè) UI,以便在記錄之間進(jìn)行導(dǎo)航。該分頁(yè) UI 使您能夠在所顯示的數(shù)據(jù)之間向前和向后導(dǎo)航,并且一次顯示固定數(shù)量的記錄。
還有一個(gè)小小的波折。使用 DataGrid 的分頁(yè)需要所有數(shù)據(jù)均與網(wǎng)格進(jìn)行綁定。例如,您的數(shù)據(jù)層需要返回所有數(shù)據(jù),那么 DataGrid 就會(huì)基于當(dāng)前頁(yè)篩選顯示的所有記錄。如果通過(guò) DataGrid 進(jìn)行分頁(yè)時(shí)返回了 100,000 個(gè)記錄,那么針對(duì)每個(gè)請(qǐng)求會(huì)放棄 99,975 個(gè)記錄(假設(shè)每頁(yè)大小為 25 個(gè)記錄)。當(dāng)記錄的數(shù)量不斷增加時(shí),應(yīng)用程序的性能就會(huì)受到影響,因?yàn)獒槍?duì)每個(gè)請(qǐng)求必須發(fā)送越來(lái)越多的數(shù)據(jù)。
要編寫性能更好的分頁(yè)代碼,一個(gè)極佳的方式是使用存儲(chǔ)過(guò)程。圖 2 顯示了針對(duì) Northwind 數(shù)據(jù)庫(kù)中的 Orders 表進(jìn)行分頁(yè)的一個(gè)示例存儲(chǔ)過(guò)程。簡(jiǎn)而言之,您此時(shí)要做的只是傳遞頁(yè)索引和頁(yè)大小。然后就會(huì)計(jì)算合適的結(jié)果集,并將其返回。
在社區(qū)服務(wù)器中,我們編寫了一個(gè)分頁(yè)服務(wù)器控件,以完成所有的數(shù)據(jù)分頁(yè)。您將會(huì)看到,我使用的就是技巧 1 中討論的理念,從一個(gè)存儲(chǔ)過(guò)程返回兩個(gè)結(jié)果集:記錄的總數(shù)和請(qǐng)求的數(shù)據(jù)。
返回記錄的總數(shù)可能會(huì)根據(jù)所執(zhí)行查詢的不同而有所變化。例如,WHERE 子句可用來(lái)約束返回的數(shù)據(jù)。為了計(jì)算在分頁(yè) UI 中顯示的總頁(yè)數(shù),必須了解要返回記錄的總數(shù)。例如,如果總共有 1,000,000 條記錄,并且要使用一個(gè) WHERE 子句將其篩選為 1000 條記錄,那么分頁(yè)邏輯就需要了解記錄的總數(shù)才能正確呈現(xiàn)分頁(yè) UI。
技巧 3 — 連接池
在 Web 應(yīng)用程序和 SQL Server? 之間設(shè)置 TCP 連接可能是一個(gè)非常消耗資源的操作。Microsoft 的開(kāi)發(fā)人員到目前為止能夠使用連接池已經(jīng)有一段時(shí)間了,這使得他們能夠重用數(shù)據(jù)庫(kù)連接。他們不是針對(duì)每個(gè)請(qǐng)求都設(shè)置一個(gè)新的 TCP 連接,而是只在連接池中沒(méi)有任何連接時(shí)才設(shè)置新連接。當(dāng)連接關(guān)閉時(shí),它會(huì)返回連接池,在其中它會(huì)保持與數(shù)據(jù)庫(kù)的連接,而不是完全破壞該 TCP 連接。
當(dāng)然,您需要小心是否會(huì)出現(xiàn)泄漏連接。當(dāng)您完成使用連接時(shí),請(qǐng)一定要關(guān)閉這些連接。再重復(fù)一遍:無(wú)論任何人對(duì) Microsoft?.NET Framework 中的垃圾回收有什么評(píng)論,請(qǐng)一定要在完成使用連接時(shí)針對(duì)該連接顯式調(diào)用 Close 或 Dispose。不要相信公共語(yǔ)言運(yùn)行庫(kù) (CLR) 會(huì)在預(yù)先確定的時(shí)間為您清除和關(guān)閉連接。盡管 CLR 最終會(huì)破壞該類,并強(qiáng)制連接關(guān)閉,但是當(dāng)針對(duì)對(duì)象的垃圾回收真正發(fā)生時(shí),并不能保證。
要以最優(yōu)化的方式使用連接池,需要遵守一些規(guī)則。首先打開(kāi)連接,執(zhí)行操作,然后關(guān)閉該連接。如果您必須如此的話,可以針對(duì)每個(gè)請(qǐng)求多次打開(kāi)和關(guān)閉連接(最好應(yīng)用技巧 1),但是不要一直將連接保持打開(kāi)狀態(tài)并使用各種不同的方法對(duì)其進(jìn)行進(jìn)出傳遞。第二,使用相同的連接字符串(如果使用集成身份驗(yàn)證的話,還要使用相同的線程標(biāo)識(shí))。如果不使用相同的連接字符串,例如根據(jù)登錄的用戶自定義連接字符串,那么您將無(wú)法得到連接池提供的同一個(gè)優(yōu)化值。如果您使用集成身份驗(yàn)證,同時(shí)還要模擬大量用戶,連接池的效率也會(huì)大大下降。嘗試跟蹤與連接池相關(guān)的任何性能問(wèn)題時(shí),.NET CLR 數(shù)據(jù)性能計(jì)數(shù)器可能非常有用。
每當(dāng)應(yīng)用程序連接資源時(shí),如在另一個(gè)進(jìn)程中運(yùn)行的數(shù)據(jù)庫(kù),您都應(yīng)該重點(diǎn)考慮連接該資源所花時(shí)間、發(fā)送或檢索數(shù)據(jù)所花時(shí)間,以及往返的數(shù)量,從而進(jìn)行優(yōu)化。優(yōu)化應(yīng)用程序中任何種類的進(jìn)程跳躍都是獲得更佳性能的首要一點(diǎn)。
應(yīng)用層包含了連接數(shù)據(jù)層、將數(shù)據(jù)轉(zhuǎn)換為有意義類實(shí)例和業(yè)務(wù)流程的邏輯。例如社區(qū)服務(wù)器,您要在其中填充Forums 或 Threads集合,應(yīng)用業(yè)務(wù)規(guī)則(如權(quán)限);最重要的是要在其中執(zhí)行緩存邏輯。
技巧 4 — ASP.NET 緩存 API
編寫應(yīng)用程序代碼行之前,一個(gè)首要完成的操作是設(shè)計(jì)應(yīng)用層的結(jié)構(gòu),以便最大化利用 ASP.NET 緩存功能。
如果您的組件要在 ASP.NET 應(yīng)用程序中運(yùn)行,則只需在該應(yīng)用程序項(xiàng)目中包括一個(gè) System.Web.dll 引用。當(dāng)您需要訪問(wèn)該緩存時(shí),請(qǐng)使用 HttpRuntime.Cache 屬性(通過(guò) Page.Cache 和 HttpContext.Cache 也可訪問(wèn)這個(gè)對(duì)象)。
對(duì)于緩存數(shù)據(jù),有幾個(gè)規(guī)則。首先,如果數(shù)據(jù)可能會(huì)多次使用時(shí),則這是使用緩存的一個(gè)很好的備選情況。第二,如果數(shù)據(jù)是通用的,而不特定于某個(gè)具體的請(qǐng)求或用戶時(shí),則也是使用緩存的一個(gè)很好的備選情況。如果數(shù)據(jù)是特定于用戶或請(qǐng)求的,但是壽命較長(zhǎng)的話,仍然可以對(duì)其進(jìn)行緩存,但是這種情況可能并不經(jīng)常使用。第三,一個(gè)經(jīng)常被忽略的規(guī)則是,有時(shí)可能您緩存得太多。通常在一個(gè) x86 計(jì)算機(jī)上,為了減少內(nèi)存不足錯(cuò)誤出現(xiàn)的機(jī)會(huì),您會(huì)想使用不高于 800MB 的專用字節(jié)運(yùn)行進(jìn)程。因此緩存應(yīng)該有個(gè)限度。換句話說(shuō),您可能能夠重用某個(gè)計(jì)算結(jié)果,但是如果該計(jì)算采用 10 個(gè)參數(shù)的話,您可能要嘗試緩存 10 個(gè)排列,這樣有可能給您帶來(lái)麻煩。一個(gè)要求 ASP.NET 的最常見(jiàn)支持是由于過(guò)度緩存引起的內(nèi)存不足錯(cuò)誤,尤其是對(duì)于大型數(shù)據(jù)集。
緩存有幾個(gè)極佳的功能,您需要對(duì)它們有所了解。首先,緩存會(huì)實(shí)現(xiàn)最近最少使用的算法,使得 ASP.NET 能夠在內(nèi)存運(yùn)行效率較低的情況下強(qiáng)制緩存清除 - 從緩存自動(dòng)刪除未使用過(guò)的項(xiàng)目。第二,緩存支持可以強(qiáng)制失效的過(guò)期依賴項(xiàng)。這些依賴項(xiàng)包括時(shí)間、密鑰和文件。時(shí)間經(jīng)常會(huì)用到,但是對(duì)于 ASP.NET 2.0,引入了一個(gè)功能更強(qiáng)的新失效類型:數(shù)據(jù)庫(kù)緩存失效。它指的是當(dāng)數(shù)據(jù)庫(kù)中的數(shù)據(jù)發(fā)生變化時(shí)自動(dòng)刪除緩存中的項(xiàng)。有關(guān)數(shù)據(jù)庫(kù)緩存失效的詳細(xì)信息,請(qǐng)參閱 MSDN?Magazine 2004 年 7 月的 Dino Esposito Cutting Edge 專欄。要了解緩存的體系結(jié)構(gòu),請(qǐng)參閱圖 3。
技巧 5 — 每請(qǐng)求緩存
在本文前面部分,我提到了經(jīng)常遍歷代碼路徑的一些小改善可能會(huì)導(dǎo)致較大的整體性能收益。對(duì)于這些小改善,其中有一個(gè)絕對(duì)是我的最愛(ài),我將其稱之為“每請(qǐng)求緩存”。
緩存 API 的設(shè)計(jì)目的是為了將數(shù)據(jù)緩存較長(zhǎng)的一段時(shí)間,或者緩存至滿足某些條件時(shí),但每請(qǐng)求緩存則意味著只將數(shù)據(jù)緩存為該請(qǐng)求的持續(xù)時(shí)間。對(duì)于每個(gè)請(qǐng)求,要經(jīng)常訪問(wèn)某個(gè)特定的代碼路徑,但是數(shù)據(jù)卻只需提取、應(yīng)用、修改或更新一次。這聽(tīng)起來(lái)有些理論化,那么我們來(lái)舉一個(gè)具體的示例。
在社區(qū)服務(wù)器的論壇應(yīng)用程序中,頁(yè)面上使用的每個(gè)服務(wù)器控件都需要個(gè)性化的數(shù)據(jù)來(lái)確定使用什么外觀、使用什么樣式表,以及其他個(gè)性化數(shù)據(jù)。這些數(shù)據(jù)中有些可以長(zhǎng)期緩存,但是有些數(shù)據(jù)卻只針對(duì)每個(gè)請(qǐng)求提取一次,然后在執(zhí)行該請(qǐng)求期間對(duì)其重用多次,如要用于控件的外觀。
為了達(dá)到每請(qǐng)求緩存,請(qǐng)使用 ASP.NET HttpContext。對(duì)于每個(gè)請(qǐng)求,都會(huì)創(chuàng)建一個(gè) HttpContext 實(shí)例,在該請(qǐng)求期間從 HttpContext.Current 屬性的任何位置都可訪問(wèn)該實(shí)例。該 HttpContext 類具有一個(gè)特殊的 Items 集合屬性;添加到此 Items 集合的對(duì)象和數(shù)據(jù)只在該請(qǐng)求持續(xù)期間內(nèi)進(jìn)行緩存。正如您可以使用緩存來(lái)存儲(chǔ)經(jīng)常訪問(wèn)的數(shù)據(jù)一樣,您也可以使用 HttpContext.Items 來(lái)存儲(chǔ)只基于每個(gè)請(qǐng)求使用的數(shù)據(jù)。它背后的邏輯非常簡(jiǎn)單:數(shù)據(jù)在它不存在的時(shí)候添加到 HttpContext.Items 集合,在后來(lái)的查找中,只是返回 HttpContext.Items 中的數(shù)據(jù)。
技巧 6 — 后臺(tái)處理
通往代碼的路徑應(yīng)該盡可能快速,是嗎?可能有時(shí)您會(huì)覺(jué)得針對(duì)每個(gè)請(qǐng)求執(zhí)行的或者每 n 個(gè)請(qǐng)求執(zhí)行一次的任務(wù)所需資源非常多。發(fā)送電子郵件或者分析和驗(yàn)證傳入數(shù)據(jù)就是這樣的一些例子。
剖析 ASP.NET Forums 1.0 并重新構(gòu)建組成社區(qū)服務(wù)器的內(nèi)容時(shí),我們發(fā)現(xiàn)添加新張貼的代碼路徑非常慢。每次添加新張貼時(shí),應(yīng)用程序首先需要確保沒(méi)有重復(fù)的張貼,然后必須使用“壞詞”篩選器分析該張貼,分析張貼的字符圖釋,對(duì)張貼添加標(biāo)記并進(jìn)行索引,請(qǐng)求時(shí)將張貼添加到合適的隊(duì)列,驗(yàn)證附件,最終張貼之后,立即向所有訂閱者發(fā)出電子郵件通知。很清楚,這涉及很多操作。
經(jīng)研究發(fā)現(xiàn),大多數(shù)時(shí)間都花在了索引邏輯和發(fā)送電子郵件上。對(duì)張貼進(jìn)行索引是一個(gè)非常耗時(shí)的操作,人們發(fā)現(xiàn)內(nèi)置的 System.Web.Mail 功能要連接 SMYP 服務(wù)器,然后連續(xù)發(fā)送電子郵件。當(dāng)某個(gè)特定張貼或主題領(lǐng)域的訂閱者數(shù)量增加時(shí),執(zhí)行 AddPost 功能所需的時(shí)間也越來(lái)越長(zhǎng)。
并不需要針對(duì)每個(gè)請(qǐng)求都進(jìn)行電子郵件索引。理想情況下,我們想要將此操作進(jìn)行批處理,一次索引 25 個(gè)張貼或者每五分鐘發(fā)送一次所有電子郵件。我們決定使用以前用于對(duì)數(shù)據(jù)緩存失效進(jìn)行原型設(shè)計(jì)的代碼,這個(gè)失效是用于最終進(jìn)入 Visual Studio? 2005 的內(nèi)容的。
System.Threading 命名空間中的 Timer 類非常有用,但是在 .NET Framework 中不是很有名,至少對(duì)于 Web 開(kāi)發(fā)人員來(lái)說(shuō)是這樣。創(chuàng)建之后,這個(gè) Timer 類將以一個(gè)可配置的間隔針對(duì) ThreadPool 中的某個(gè)線程調(diào)用指定的回調(diào)。這就表示,您可以對(duì)代碼進(jìn)行設(shè)置,使其能夠在沒(méi)有對(duì) ASP.NET 應(yīng)用程序進(jìn)行傳入請(qǐng)求的情況下得以執(zhí)行,這是后臺(tái)處理的理想情況。您還可以在此后臺(tái)進(jìn)程中執(zhí)行如索引或發(fā)送電子郵件之類的操作。
但是,這一技術(shù)有幾個(gè)問(wèn)題。如果應(yīng)用程序域卸載,該計(jì)時(shí)器實(shí)例將停止觸發(fā)其事件。另外,因?yàn)?CLR 對(duì)于每個(gè)進(jìn)程的線程數(shù)量具有一個(gè)硬性標(biāo)準(zhǔn),所以可能會(huì)出現(xiàn)這樣的情形:服務(wù)器負(fù)載很重,其中計(jì)時(shí)器可能沒(méi)有可在其基礎(chǔ)上得以完成的線程,在某種程度上可能會(huì)造成延遲。ASP.NET 通過(guò)在進(jìn)程中保留一定數(shù)量的可用線程,并且僅使用總線程的一部分用于請(qǐng)求處理,試圖將上述情況發(fā)生的機(jī)會(huì)降到最低。但是,如果您具有很多異步操作時(shí),這可能就是一個(gè)問(wèn)題了。
這里沒(méi)有足夠的空間來(lái)放置該代碼,但是您可以下載一個(gè)可以看懂的示例,網(wǎng)址是 www.rob-howard.net。請(qǐng)了解一下 Blackbelt TechEd 2004 演示中的幻燈片和演示。
技巧 7 — 頁(yè)輸出緩存和代理服務(wù)器
ASP.NET 是您的表示層(或者說(shuō)應(yīng)該是您的表示層);它由頁(yè)、用戶控件、服務(wù)器控件(HttpHandlers 和 HttpModules)以及它們生成的內(nèi)容組成。如果您具有一個(gè) ASP.NET 頁(yè),它會(huì)生成輸出(HTML、XML、圖像或任何其他數(shù)據(jù)),并且您針對(duì)每個(gè)請(qǐng)求運(yùn)行此代碼時(shí),它都會(huì)生成相同的輸出,那么您就擁有一個(gè)可用于頁(yè)輸出緩存的絕佳備選內(nèi)容。
將此行內(nèi)容添加頁(yè)的最上端
<%@ Page OutputCache VaryByParams="none" Duration="60" %>
就可以高效地為此頁(yè)生成一次輸出,然后對(duì)它進(jìn)行多次重用,時(shí)間最長(zhǎng)為 60 秒,此時(shí)該頁(yè)將重新執(zhí)行,輸出也將再一次添加到 ASP.NET 緩存。通過(guò)使用一些低級(jí)程序化 API 也可以完成此行為。對(duì)于輸出緩存有幾個(gè)可配置的設(shè)置,如剛剛講到的 VaryByParams 屬性。VaryByParams 剛好被請(qǐng)求到,但還允許您指定 HTTP GET 或 HTTP POST 參數(shù)來(lái)更改緩存項(xiàng)。例如,只需設(shè)置 VaryByParam="Report" 即可對(duì) default.aspx?Report=1 或 default.aspx?Report=2 進(jìn)行輸出緩存。通過(guò)指定一個(gè)以分號(hào)分隔的列表,還可以指定其他參數(shù)。
很多人都不知道何時(shí)使用輸出緩存,ASP.NET 頁(yè)還會(huì)生成一些位于緩存服務(wù)器下游的 HTTP 標(biāo)頭,如 Microsoft Internet Security and Acceleration Server 或 Akamai 使用的標(biāo)頭。設(shè)置了 HTTP 緩存標(biāo)頭之后,可以在這些網(wǎng)絡(luò)資源上對(duì)文檔進(jìn)行緩存,客戶端請(qǐng)求也可在不必返回原始服務(wù)器的情況下得以滿足。
因此,使用頁(yè)輸出緩存不會(huì)使得您的應(yīng)用程序效率更高,但是它可能會(huì)減少服務(wù)器上的負(fù)載,因?yàn)橄掠尉彺婕夹g(shù)會(huì)緩存文檔。當(dāng)然,這可能只是匿名內(nèi)容;一旦它成為下游之后,您就再也不會(huì)看到這些請(qǐng)求,并且再也無(wú)法執(zhí)行身份驗(yàn)證以阻止對(duì)它的訪問(wèn)了。
技巧 8 — 運(yùn)行 IIS 6.0(只要用于內(nèi)核緩存)
如果您未運(yùn)行 IIS 6.0 (Windows Server? 2003),那么您就錯(cuò)過(guò)了 Microsoft Web 服務(wù)器中的一些很好的性能增強(qiáng)。在技巧 7 中,我討論了輸出緩存。在 IIS 5.0 中,請(qǐng)求是通過(guò) IIS 然后進(jìn)入 ASP.NET 的。涉及到緩存時(shí),ASP.NET 中的 HttpModule 會(huì)接收該請(qǐng)求,并返回緩存中的內(nèi)容。
如果您正在使用 IIS 6.0,就會(huì)發(fā)現(xiàn)一個(gè)很好的小功能,稱為內(nèi)核緩存,它不需要對(duì) ASP.NET 進(jìn)行任何代碼更改。當(dāng)請(qǐng)求由 ASP.NET 進(jìn)行輸出緩存時(shí),IIS 內(nèi)核緩存會(huì)接收緩存數(shù)據(jù)的一個(gè)副本。當(dāng)請(qǐng)求來(lái)自網(wǎng)絡(luò)驅(qū)動(dòng)程序時(shí),內(nèi)核級(jí)別的驅(qū)動(dòng)程序(無(wú)上下文切換到用戶模式)就會(huì)接收該請(qǐng)求,如果經(jīng)過(guò)了緩存,則會(huì)將緩存的數(shù)據(jù)刷新到響應(yīng),然后完成執(zhí)行。這就表示,當(dāng)您將內(nèi)核模式緩存與 IIS 和 ASP.NET 輸出緩存一起使用時(shí),就會(huì)看到令人不敢相信的性能結(jié)果。在 ASP.NET 的 Visual Studio 2005 開(kāi)發(fā)過(guò)程中,我一度是負(fù)責(zé) ASP.NET 性能的程序經(jīng)理。開(kāi)發(fā)人員完成具體工作,但是我要看到每天進(jìn)行的所有報(bào)告。內(nèi)核模式緩存結(jié)果總是最有意思的。最常見(jiàn)的特征是網(wǎng)絡(luò)充滿了請(qǐng)求/響應(yīng),而 IIS 運(yùn)行時(shí)的 CPU 使用率只有大約 5%。這太令人震驚了!當(dāng)然使用 IIS 6.0 還有一些其他原因,但是內(nèi)核模式緩存是其中最明顯的一個(gè)。
技巧 9 — 使用 Gzip 壓縮
雖然使用 gzip 并不一定是服務(wù)器性能技巧(因?yàn)槟赡軙?huì)看到 CPU 使用率的提高),但是使用 gzip 壓縮可以減少服務(wù)器發(fā)送的字節(jié)數(shù)量。這就使人們覺(jué)得頁(yè)速度加快了,并且還減少了帶寬的用量。根據(jù)所發(fā)送數(shù)據(jù)、可以壓縮的程度以及客戶端瀏覽器是否支持(IIS 只會(huì)向支持 gzip 壓縮的客戶端發(fā)送經(jīng)過(guò) gzip 壓縮的內(nèi)容,如 Internet Explorer 6.0 和 Firefox),您的服務(wù)器每秒可以服務(wù)于更多的請(qǐng)求。實(shí)際上,幾乎每當(dāng)您減少所返回?cái)?shù)據(jù)的數(shù)量時(shí),都會(huì)增加每秒請(qǐng)求數(shù)。
Gzip 壓縮已經(jīng)內(nèi)置到 IIS 6.0 中,并且其性能比 IIS 5.0 中使用的 gzip 壓縮要好的多,這是好消息。但不幸的是,當(dāng)嘗試在 IIS 6.0 中打開(kāi) gzip 壓縮時(shí),您可能無(wú)法在 IIS 的屬性對(duì)話中找到該設(shè)置。IIS 小組在該服務(wù)器中置入了卓越的 gzip 功能,但是忘了包括一個(gè)用于啟用該功能的管理 UI。要啟用 gzip 壓縮,您必須深入到 IIS 6.0 的 XML 配置設(shè)置內(nèi)部(這樣不會(huì)引起心臟虛弱)。順便提一句,這歸功于 OrcsWeb 的 Scott Forsyth,他幫助我提出了在 OrcsWeb 上宿主的 www.asp.net 服務(wù)器的這個(gè)問(wèn)題。
本文就不講述步驟了,請(qǐng)閱讀 Brad Wilson 的文章,網(wǎng)址是 IIS6 Compression。還有一篇有關(guān)為 ASPX 啟用壓縮的知識(shí)庫(kù)文章,網(wǎng)址是 Enable ASPX Compression in IIS。但是您應(yīng)該注意,由于一些實(shí)施細(xì)節(jié),IIS 6.0 中不能同時(shí)存在動(dòng)態(tài)壓縮和內(nèi)核緩存。
技巧 10 — 服務(wù)器控件視圖狀態(tài)
視圖狀態(tài)是一個(gè)有趣的名稱,用于表示在所生成頁(yè)的隱藏輸出字段中存儲(chǔ)一些狀態(tài)數(shù)據(jù)的 ASP.NET。當(dāng)該頁(yè)張貼回服務(wù)器時(shí),服務(wù)器可以分析、驗(yàn)證、并將此視圖狀態(tài)數(shù)據(jù)應(yīng)用回該頁(yè)的控件樹(shù)。視圖狀態(tài)是一個(gè)非常強(qiáng)大的功能,因?yàn)樗试S狀態(tài)與客戶端一起保持,并且它不需要 cookie 或服務(wù)器內(nèi)存即可保存此狀態(tài)。很多 ASP.NET 服務(wù)器控件都使用視圖狀態(tài)來(lái)保持在與頁(yè)元素進(jìn)行交互期間創(chuàng)建的設(shè)置,例如保存對(duì)數(shù)據(jù)進(jìn)行分頁(yè)時(shí)顯示的當(dāng)前頁(yè)。
然而使用視圖狀態(tài)也有一些缺點(diǎn)。首先,服務(wù)或請(qǐng)求頁(yè)時(shí),它都會(huì)增加頁(yè)的總負(fù)載。對(duì)張貼回服務(wù)器的視圖狀態(tài)數(shù)據(jù)進(jìn)行序列化或取消序列化時(shí),也會(huì)發(fā)生額外的開(kāi)銷。最后,視圖狀態(tài)會(huì)增加服務(wù)器上的內(nèi)存分配。
幾個(gè)服務(wù)器控件有著過(guò)度使用視圖狀態(tài)的趨勢(shì),即使在并不需要的情況下也要使用它,其中最著名的是 DataGrid。ViewState 屬性的默認(rèn)行為是啟用,但是如果您不需要,則可以在控件或頁(yè)級(jí)別關(guān)閉。在控件內(nèi),只需將 EnableViewState 屬性設(shè)置為 false,或者在頁(yè)中使用下列設(shè)置即可對(duì)其進(jìn)行全局設(shè)置:
<%@ Page EnableViewState="false" %>
如果您不回發(fā)頁(yè),或者總是針對(duì)每個(gè)請(qǐng)求重新生成頁(yè)上的控件,則應(yīng)該在頁(yè)級(jí)別禁用視圖狀態(tài)。
小結(jié)
我為您講述了一些我認(rèn)為在編寫高性能 ASP.NET 應(yīng)用程序時(shí)有所幫助的技巧。正如我在本文前面部分提到的那樣,這是一個(gè)初步指南,并不是 ASP.NET 性能的最后結(jié)果。(有關(guān)改善 ASP.NET 應(yīng)用程序性能的信息,請(qǐng)參閱 Improving ASP.NET Performance。)只有通過(guò)自己的親身體驗(yàn)才能找出解決具體性能問(wèn)題的最好方法。但是,在您的旅程中,這些技巧應(yīng)該會(huì)為您提供一些好的指南。在軟件開(kāi)發(fā)中,幾乎沒(méi)有絕對(duì)的東西;每個(gè)應(yīng)用程序都是唯一的。