數(shù)據(jù)層的性能
當(dāng)調(diào)整某個應(yīng)用程序的性能時,有一個簡單的試金石,你可以用它按先后次序:檢查代碼是否存取數(shù)據(jù)庫?如果是,多長時間存取一次?注意相同的測試也可以被應(yīng)用于使用 Web 服務(wù)或遠程調(diào)用的代碼,但我們本文中不涉及這方面內(nèi)容。
如果在特定的代碼流程中必須具有對數(shù)據(jù)庫的請求以及要考察其它方面,如:想對字符串處理進行優(yōu)先優(yōu)化,那么暫且把它放一放,先按照上面定好的優(yōu)先次序來做。除非你有異乎尋常的性能問題,否則你的時間應(yīng)該用在嘗試最優(yōu)化與數(shù)據(jù)庫的連接所花的時間,返回的數(shù)據(jù)量以及多長時間往返一次和數(shù)據(jù)庫的通訊上。
有了這些概括信息,下面就讓我們來看看能幫助你改善應(yīng)用程序性能的十個技巧。我將從能獲得最顯著效果的改變開始。
技巧 1 —— 返回多個結(jié)果集
復(fù)審你的數(shù)據(jù)庫代碼,看看是否有多于一次的對數(shù)據(jù)庫的訪問請求。這樣每次往返數(shù)據(jù)庫都降低你的應(yīng)用程序能處理的每秒請求數(shù)。通過在單個數(shù)據(jù)庫請求中返回多結(jié)果集,你能降低與數(shù)據(jù)庫通信的總體時間。同時你也將使系統(tǒng)更具伸縮性,因為你減少了數(shù)據(jù)庫服務(wù)器處理請求的負擔(dān)。
雖然你可以用動態(tài) SQL 返回多結(jié)果集,我更喜歡使用存儲過過程。是否將業(yè)務(wù)邏輯駐留在存儲過程當(dāng)中是個有待爭論的問題,但我認為,如果存儲過程中的邏輯能約束返回的數(shù)據(jù)(降低數(shù)據(jù)集的尺寸,在網(wǎng)絡(luò)上傳輸?shù)臅r間以及邏輯層不必過慮數(shù)據(jù)),這是一件好事情。
使用 SqlCommand 命令實例及其 ExecuteReader 方法來處理強類型的各個業(yè)務(wù)類,你通過調(diào)用 NextResult 可以向前移動結(jié)果集指針。Figure 1 示范了處理幾個帶類型的 ArrayLists 例子會話。從數(shù)據(jù)庫只返回你需要的數(shù)據(jù)還會降低服務(wù)器上內(nèi)存的分配。
技巧 2 —— 分頁數(shù)據(jù)存取
建立 Web 應(yīng)用程序與 SQL Server 之間的 TCP 連接是一項昂貴的操作。微軟的開發(fā)人員利用連接池技術(shù)已經(jīng)有好長一段時間了,這個技術(shù)使他們能重用到數(shù)據(jù)庫的連接。而不是每次請求都建立新的 TCP 連接,新連接僅在連接池中得不到連接時才建立。當(dāng)連接被關(guān)閉時,它被返回到連接池中,在那里它仍然保持與數(shù)據(jù)庫的連接,與完全斷開 TCP 連接相反。
當(dāng)然,你需要提防泄漏的連接。當(dāng)你處理完畢,一定要關(guān)閉連接。重申一次:不管人們怎么吹噓微軟 .NET 框架中的垃圾收集特性,每當(dāng)你處理完畢,一定要顯式地調(diào)用連接對象的 Close 或 Dispose 方法。不要指望公共語言運行時(CLR)來為你定時清除和關(guān)閉連接。CLR 最終將銷毀類并強行關(guān)閉連接,但你無法保證該對象的垃圾收集屆時會起作用。
為了充分用好連接池,有幾條規(guī)則必須了然于心。首先,打開連接,進行處理,然后關(guān)閉連接。寧愿每個請求的連接打開和關(guān)閉多次,也不要保持連接打開狀態(tài)以及在不同的方法間將它傳來傳去。其次,使用相同的連接串(如果你使用集成身份檢查,那么也要用相同的線程身份)。如果你不用相同的連接串,例如,根據(jù)登錄用戶來定制連接串,你將無法得到連接池所提供的相同的最優(yōu)化值。當(dāng)模擬大用戶量情形時,如果你使用集成身份檢查,那么你的連接池將效力大減。.NET CLR 數(shù)據(jù)性能計數(shù)器在試圖跟蹤任何與連接池有關(guān)的性能問題時是非常有用的。
不管什么時候,只要你的應(yīng)用程序連接到運行在其它進程中的資源,比如某個數(shù)據(jù)庫,你都應(yīng)該針對連接到資源所耗時間,發(fā)送和接收數(shù)據(jù)所耗時間以及往返次數(shù)進行優(yōu)化。為了實現(xiàn)較好的性能,應(yīng)該首當(dāng)其充優(yōu)化應(yīng)用程序中任何種類的忙碌進程。
應(yīng)用層包含到數(shù)據(jù)層的連接以及將數(shù)據(jù)轉(zhuǎn)換成有意義的類實例和業(yè)務(wù)處理的邏輯。以 Community Server 為例,你要在其中處理 Forums 和 Threads 集合;以及應(yīng)用許可這樣的業(yè)務(wù)規(guī)則;尤其重要的是緩沖(Caching)邏輯也實現(xiàn)其中。
技巧 4 —— ASP.NET Cache API
在本文前面,我曾提到對頻繁執(zhí)行的代碼塊所做的小小改動可能產(chǎn)生很大的,整體性能的提升。我把其中一個我特別中意的叫做預(yù)請求緩沖(per-request caching)。
由于 Cache API 被設(shè)計用來緩沖長期數(shù)據(jù)或直到某個條件被滿足,預(yù)請求緩沖意旨用于請求期間的緩沖該數(shù)據(jù)。特定的代碼流程被每次請求頻繁訪問但是數(shù)據(jù)只需要被拾取,應(yīng)用,修改或更新一次,這樣說太理論化,還是讓我們看一個具體的例子吧。
在 Community Server 的 Forums (論壇)應(yīng)用中,某個頁面上使用的每個服務(wù)器控件需要個性化數(shù)據(jù)以確定使用那個皮膚和式樣頁,以及其它的個性化數(shù)據(jù),其中有些數(shù)據(jù)可以被長時間緩沖,但有些數(shù)據(jù),比如用于控件的皮膚在單個請求中只被拾取一次并在該請求執(zhí)行期間被重用多次。
為了完成預(yù)請求緩沖,用 ASP.NET HttpContext。HttpContext 的實例是隨每個請求創(chuàng)建的,并可以通過 HttpContext.Current 屬性在那個請求執(zhí)行期間的任何地方存取它。HttpContext 類具有一個特別的 Items 集合屬性,被添加到該 Items 集合的對象和數(shù)據(jù)只是在該請求期間被緩存。就像你可以使用 Cache 來保存頻繁使用的數(shù)據(jù)一樣,你可以用 HttpContext.Items 來保存只在某個預(yù)請求中使用的數(shù)據(jù)。在此背景后的邏輯很簡單:當(dāng)數(shù)據(jù)不存在時被添加到 HttpContext.Items 集合,以及在隨后的并發(fā)查找中簡單地返回 HttpContext.Items 中發(fā)現(xiàn)的數(shù)據(jù)。
技巧 6——后臺處理
你的代碼流程應(yīng)該盡可能快,對吧?你自己可能多次發(fā)現(xiàn)要完成每個請求或每n個請求的任務(wù)代價很高。發(fā)出 e-mail 或解析并檢查輸入數(shù)據(jù)的有效性就是個例。
在重新生成 ASP.NET Forums 1.0 并把它整合到 Community Server 時,我們發(fā)現(xiàn)添加新貼的代碼流程非常慢。每次添加帖子,應(yīng)用程序首先要確保沒有重復(fù)貼,然后必須用“badword”過濾器解析該貼的表情圖像,記號并索引,如果必要還要將帖子添加到相應(yīng)的隊列中,對附件進行有效性檢查,最終完成發(fā)貼后,給預(yù)訂者發(fā)出 e-mail 通知。顯然,這里做的工作太多。
我們發(fā)現(xiàn)大多數(shù)時間都花在了索引邏輯和發(fā)送e-mail上。索引帖子是一個很耗時的操作,此外,內(nèi)建的 System.Web.Mail 功能要與 SMTP 服務(wù)器連接并順序發(fā)送郵件。當(dāng)特定帖子或主題預(yù)定者數(shù)量增加時,AddPost 函數(shù)的執(zhí)行時間會越來越長。
并不是每個請求都需要索引郵件,我們想最好是批量集中處理,并且一次只索引25個帖子或每隔五分鐘發(fā)送一次郵件。我們決定使用的代碼與我曾在原型數(shù)據(jù)庫緩沖失效中所使用的代碼相同,最終它也被納入 Visual Studio 2005。
名字空間 System.Threading 中的 Timer 類非常有用,但在.NET 框架中鮮為人知,至少對 Web 開發(fā)者來說是這樣。一旦創(chuàng)建,Timer 將以可定制的間隔針對線程池中的某個線程調(diào)用指定的回調(diào)函數(shù)。這意味著你不用輸入請求到 ASP.NET 應(yīng)用程序便能讓代碼實行,這是一種最合適后臺處理的情形。你也可以在這種后臺處理模式中進行例如索引或發(fā)送電子郵件這樣的工作。
盡管如此,這個技術(shù)存在幾個問題,如果你的應(yīng)用程序域關(guān)閉,該定時器實例將停止觸發(fā)其事件。另外,由于 CLR 有一個硬坎,即每個進程的線程數(shù)是固定的,你便可能陷入嚴重的服務(wù)器負荷當(dāng)中,此時可能就沒有線程來處理定時器,從而造成延時。為了讓發(fā)生這種情況的幾率最小化,ASP.NET 通過在進程中預(yù)留一定數(shù)量的空閑線程,并只使用部分線程來處理請求。然而,如果你有許多異步處理,這樣做會有問題。
由于篇幅所限,在此無法列出代碼,但你可以從 http://www.rob-howard.net/ 下載可消化的例子。其中有 Blackbelt TechEd 2004 展示的幻燈和 Demo。
技巧 7——頁面輸出緩存和代理服務(wù)器
ASP.NET 是你的表示層(或者說應(yīng)該是);它由頁面,用戶控件,服務(wù)器控件(HttpHandlers and HttpModules)以及它們生成的內(nèi)容組成。如果你有一個產(chǎn)生輸出的 ASP.NET 頁面,不管是輸出 HTML,XML,圖像還是任何其它數(shù)據(jù),而且每個請求你都運行這個代碼并產(chǎn)生相同的輸出,此時最好選擇使用頁面輸出緩存。
只要在頁面頂部添加這一行代碼即可:
<%@ Page OutputCache VaryByParams="none" Duration="60" %>
你可以為此頁面有效地產(chǎn)生一次輸出并可以在60秒內(nèi)多次重用它,一到這個時間點,該頁面將重新執(zhí)行并將再次將輸出添加到 ASP.NET Cache。這個行為還能用某些低級編程 APIs 來完成。輸出緩存有幾個可以配置的設(shè)置,比如:VaryByParams 屬性。VaryByParams 不是必須的,但允許你指定 HTTP GET 或 HTTP POST 參數(shù)來改變緩存入口。例如,default.aspx?Report=1 或 default.aspx?Report=2 可以簡單地設(shè)置 VaryByParam="Report" 來對輸出進行緩存。額外的參數(shù)被命名并用用分號分隔。
在使用輸出緩存機制時,許多人都不了解 ASP.NET 頁還產(chǎn)生一組下游緩存服務(wù)器 HTTP 頭,比如 Microsoft Internet Security and Acceleration Server 或 Akamai 使用的 HTTP 頭。當(dāng)設(shè)置 HTTP 緩存頭,文檔可以被緩存到這些網(wǎng)絡(luò)資源,從而響應(yīng)客戶端請求不必返回原服務(wù)器。
然而,使用頁面輸出緩存并不會使你的應(yīng)用程序更有效率,但它能通過下游緩存技術(shù)緩存文檔從而潛在地降低服務(wù)器的負載。當(dāng)然,這只能是異步內(nèi)容;一旦實施下游緩存,你將無法看到任何請求,也不能實現(xiàn)身份認證來防止對它的存取。
技巧 8——運行 IIS 6.0 (如果僅用于內(nèi)核緩存)
如果你不運行 IIS 6.O(Windows Server 2003),那么你將得不到微軟 Web 服務(wù)器中一些重大的性能改進。在技巧 7 中,我談到了輸出緩存。在 IIS 5.0 中,請求到達 IIS,然后到達 ASP.NET。當(dāng)使用緩存時,ASP.NET 中的 HttpModule 接受該請求,并從該緩存中返回內(nèi)容。
如果你用 IIS 6.0,有一些巧妙的特性叫內(nèi)核緩存,它不需要將任何代碼改成 ASP.NET。當(dāng) ASP.NET對請求進行緩存處理,IIS 內(nèi)核緩存便接收一份緩存數(shù)據(jù)的拷貝。當(dāng)請求來自網(wǎng)絡(luò)驅(qū)動器,內(nèi)核一級的驅(qū)動程序(沒有到用戶模式的上下文轉(zhuǎn)換)接收該請求,如果緩存,則直接用緩存數(shù)據(jù)響應(yīng)并完成執(zhí)行。這意味著當(dāng)你使用 IIS 內(nèi)核模式緩存和 ASP.NET 緩存時,你將看到無法置信的性能結(jié)果。在開發(fā) Visual Studio 2005 的 ASP.NET 期間,我是負責(zé) ASP.NET 性能的程序經(jīng)理。開發(fā)人員的工作做的真是棒極了,而我基本上每天都在看報告。內(nèi)核模式緩存結(jié)果總是最有趣的。典型的情況是請求/響應(yīng)往往使網(wǎng)絡(luò)飽和,但 IIS 的運行僅占 CPU 的百分之五。真令人驚異!當(dāng)然使用 IIS 6.O 有其它一些原因,但內(nèi)核模式緩存是顯而易見的理由。
技巧 9——使用 Gzip 壓縮
雖然使用 gzip 壓縮不是一個必須的服務(wù)器性能技巧(因為你可能看到 CUP 的使用率上升了),但它能降低服務(wù)器發(fā)送字節(jié)的數(shù)量。從而感覺頁面更快,而且減少帶寬的占用。其壓縮的效果好壞取決于所發(fā)送的數(shù)據(jù)以及客戶端瀏覽器是否支持這種壓縮(IIS 只會將數(shù)據(jù)發(fā)送到支持 gzip 的瀏覽器,比如:IE 6.0 和 Firefox),從而使服務(wù)器可以在每秒鐘里處理更多的請求。事實上,只要你降低返回數(shù)據(jù)的數(shù)量,便能提高每秒所處理的請求數(shù)。
有一個好消息是 gzip 壓縮是 IIS 6.0 的內(nèi)建特性,并且比它在 IIS 5.0 中使用的效果更好。但是,要想在 IIS 6.0 中啟用 gzip 壓縮可能沒那么方便,IIS 的屬性對話框里找不到設(shè)置它的地方。IIS 團隊將卓越的 gzip 壓縮能力內(nèi)建在服務(wù)器中,但忽視了建立一個啟用壓縮特性的管理用戶界面。要想啟用 gzip 壓縮機制,你必須深入到 IIS 的 XML 配置設(shè)置內(nèi)部(必須對之相當(dāng)熟悉才能配置)。順便提一下,在此感謝 OrcsWeb 的 Scott Forsyth 幫我解決了在 OrcsWeb 數(shù)個 http://www.asp.net/ 服務(wù)器上的這個問題。
與其在本文中包含整個過程,還不如閱讀 Brad Wilson 在 IIS6 Compression 上的文章。微軟知識庫也有一篇關(guān)于為ASPX啟用壓縮特性的文章:Enable ASPX Compression in IIS。但是,還必須注意一點,動態(tài)壓縮與內(nèi)核緩存由于某些實現(xiàn)細節(jié)的原因,其在 IIS 6.0 中是相互排斥的。
技巧 10——服務(wù)器控件的可視狀態(tài)
可視狀態(tài)(View State)對于 ASP.NET 來說是個奇特的名字,它在所產(chǎn)生的頁面中隱藏輸入域以存儲某些狀態(tài)數(shù)據(jù)。當(dāng)頁面被發(fā)回服務(wù)器,該服務(wù)器能解析,檢查其有效性并將這個狀態(tài)數(shù)據(jù)應(yīng)用到頁面的控件樹中??梢暊顟B(tài)是一種非常強大的能力,因為它允許狀態(tài)被客戶端持續(xù)化并且它不需要cookies 或 服務(wù)器內(nèi)存來存儲該狀態(tài)。許多 ASP.NET 服務(wù)器控件使用可視狀態(tài)來持續(xù)化與頁面元素交互期間所作的設(shè)置,例如,對數(shù)據(jù)進行分頁時保存當(dāng)前頁顯示頁。
然而,使用可視狀態(tài)有許多不利之處,首先,不論是在請求的時候還是提供服務(wù)的時候,它都增加造成整個頁面的負擔(dān)。當(dāng)序列化或反序列化被返回服務(wù)器的可視狀態(tài)數(shù)據(jù)時還產(chǎn)生一些附加的開銷。最終可視狀態(tài)會增加服務(wù)器的內(nèi)存分配。
最著名的服務(wù)器控件要數(shù) DataGrid 了,使用可視狀態(tài)有過之而無不及,即便是在不需要使用的時候也是如此。ViewState 屬性默認是啟用的,但如果你不需要它,可以在頁面控件級或頁面級關(guān)閉它。在某個控件中,只要將 EnableViewState 設(shè)置為 false,或者在頁面里使用如下全局設(shè)置:
<%@ Page EnableViewState="false" %>
如果在某頁面中不進行回發(fā),或每次請求頁面時總是重新產(chǎn)生控件,那么你應(yīng)該在頁面級禁用可視狀態(tài)。
結(jié)論
我已經(jīng)向你提供了一些我認為有用的編寫高性能 ASP.NET 應(yīng)用程序的技巧。正如我在本文開頭時所講的那樣,這是一些很初級的指南,而不是 ASP.NET 性能方面的最終定論。(更多有關(guān)改進 ASP.NET 應(yīng)用程序性能方面的信息請參見:Improving ASP.NET Performance.)只有通過自己的經(jīng)驗方能找到最佳途徑來解決具體的性能問題。不管怎樣,在你解決問題的過程中,這些技巧多少會對你有所裨益的。在軟件開發(fā)過程中,每一個應(yīng)用都有其獨特的一面,沒有什么東西是絕對的。
——常見的性能神話
最常見的神話之一是 C# 代碼比 Visual Basic 代碼快。這樣的說法是站不住腳的,雖然在 Visual Basic 中存在一些 C# 沒有的性能阻礙行為,比如顯式地聲明類型。但是如果遵循良好的編程實踐,沒有理由說明 Visual Basic 和 C# 代碼不能以幾乎同樣的性能執(zhí)行。簡單說來,相同的代碼產(chǎn)生相同的結(jié)果。
另一個神話是后臺代碼比內(nèi)聯(lián)代碼快,這是絕對不成立的。性能與你的 ASP.NET 應(yīng)用程序代碼在哪沒有什么關(guān)系,無論是后臺代碼文件還是內(nèi)聯(lián)在 ASP.NET 頁面。有時我更喜歡使用內(nèi)聯(lián)代碼,因為變更不會產(chǎn)生后臺代碼那樣的更新成本。例如,使用后臺代碼必須更新整個后臺 DLL,那時一個可能引起驚慌的主張。
第三個神話是組件比頁面要快。這在經(jīng)典的 ASP 中是存在的,因為編譯的 COM 服務(wù)器要比 VBScript 快得多。但是對于頁面和組件都是類的 ASP.NET 來說則不然。不論你的代碼是以后臺代碼形式內(nèi)聯(lián)在頁面,還是分離的組件,所產(chǎn)生的性能差別不大。只是這種組織形式能更好地從邏輯上對功能進行分組,在性能上沒有差別。
我想澄清的最后一個神話是用 Web 服務(wù)來實現(xiàn)兩個應(yīng)用程序之間各個功能。Web 服務(wù)應(yīng)該被用于連接異構(gòu)系統(tǒng)或提供系統(tǒng)功能及行為的遠程訪問。不應(yīng)該將它用于兩個相同系統(tǒng)的內(nèi)部連接。雖然使用起來很容易,但有很多其它更好的可選方法。最糟的事情莫過于將 Web 服務(wù)用于相同服務(wù)器上 ASP 和 ASP.NET 應(yīng)用程序之間的通訊,我已經(jīng)不厭其煩地對之進行了說明。