圖 5 消除不必要的會話狀態數據庫訪問
那麼您應該怎麼辦呢?很簡單:禁用不使用會話狀態的頁中的會話狀態。這樣做總是一個好辦法,但是當會話狀態存儲在數據庫中時,該方法尤其重要。圖 5 顯示如何禁用會話狀態。如果頁面根本不使用會話狀態,請在其 Page 指令中包含 EnableSessionState="false",如下所示:
<%@ Page EnableSessionState="false" ... %>
該指令阻止會話狀態管理器在每個請求中讀取和寫入會話狀態數據庫。如果頁面從會話狀態中讀取數據,但卻不寫入數據(即,不修改用戶會話的內容),則將 EnableSessionState 設置為 ReadOnly,如下所示:
<%@ Page EnableSessionState="ReadOnly" ... %>
最後,如果頁面需要對會話狀態進行讀/寫訪問,則省略 EnableSessionState 屬性或將其設置為 true:
<%@ Page EnableSessionState="true" ... %>
通過以這種方式控制會話狀態,可以確保 ASP.Net 只在真正需要時才訪問會話狀態數據庫。消除不必要的數據庫訪問是構建高性能應用程序的第一步。
順便說一下,EnableSessionState 屬性是公開的。該屬性自 ASP.Net 1.0 以來就已經進行了說明,但是我至今仍很少見到開發人員利用該屬性。也許是因為它對於內存中的默認會話狀態模型並不十分重要。但是它對於 SQL Server 模型卻很重要。
返回頁首
未緩存的角色
以下語句經常出現於 ASP.NET 2.0 應用程序的 web.config 文件以及介紹 ASP.Net 2.0 角色管理器的示例中:
<roleManager enabled="true" />
但正如以上所示,該語句確實會對性能產生明顯的負面影響。您知道為什麼嗎?
默認情況下,ASP.Net 2.0 角色管理器不會緩存角色數據。相反,它會在每次需要確定用戶屬於哪個角色(如果有)時參考角色數據存儲。這意味著一旦用戶經過了身份驗證,任何利用角色數據的頁(例如,使用啟用了安全裁減設置的網站圖的頁,以及使用 web.config 中基於角色的 URL 指令進行訪問受到限制的頁)將導致角色管理器查詢角色數據存儲。如果角色存儲在數據庫中,那麼對於每個請求需要訪問多個數據庫的情況,您可以輕松地免除訪問多個數據庫。解決方案是配置角色管理器以在 CookIE 中緩存角色數據:
<roleManager enabled="true" cacheRolesInCookIE="true" />
您可以使用其他<roleManager> 屬性控制角色 Cookie 的特征 — 例如,CookIE 應保持有效的期限(以及角色管理器因此返回角色數據庫的頻率)。角色 CookIE 默認情況下是經過簽名和加密的,因此安全風險雖然不為零,但也有所緩解。
返回頁首
配置文件屬性序列化
ASP.NET 2.0 配置文件服務為保持每個用戶的狀態(例如個性化首選項和語言首選項)的問題提供了一個現成的解決方案。要使用配置文件服務,您可以定義一個 XML 配置文件,其中包含要保留的代表單個用戶的屬性。然後,ASP.Net 編譯一個包含相同屬性的類,並通過添加到頁的配置文件屬性提供對類實例的強類型訪問。
配置文件靈活性很強,它甚至允許將自定義數據類型用作配置文件屬性。但是,其中卻存在一個問題,我親眼看到該問題導致開發人員出差錯。圖 6 包含一個名為 Posts 的簡單類,以及將 Posts 用作配置文件屬性的配置文件定義。但是,該類和該配置文件在運行時會產生意外的行為。您能找出其中的原因嗎?
問題在於 Posts 包含一個名為 _count 的私有字段,該字段必須進行序列化和反序列化,才能完全凍結和重新凍結類實例。但是 _count 卻沒有經過序列化和反序列化,因為它是私有的,而且默認情況下 ASP.Net 配置文件管理器使用 XML 序列化對自定義類型進行序列化和反序列化。XML 序列化程序將忽略非公共成員。
因此,會對 Posts 的實例進行序列化和反序列化,但是每次反序列化類實例時,_count 都會重設為 0。
一種解決方案是使 _count 成為公共字段而非私有字段。另一種解決方案是使用公共讀/寫屬性封裝 _count。最佳解決方案是將 Posts 標記為可序列化(使用 SerializableAttribute),並將配置文件管理器配置為使用 .Net Framework 二進制序列化程序對類實例進行序列化和反序列化。該解決方案能夠保持類本身的設計。與 XML 序列化程序不同的是,二進制序列化程序序列化字段,而不管是否可以訪問。圖 7 顯示 Posts 類的修復版本並突出顯示了更改的附帶配置文件定義。
您應該牢記的一點是,如果您使用自定義數據類型作為配置文件屬性,並且該數據類型具有必須序列化才能完全序列化類型實例的非公共數據成員,則在屬性聲明中使用 serializeAs="Binary" 屬性並確保類型本身是可序列化的。否則,將無法進行完整的序列化,並且您還將浪費時間來嘗試確定配置文件無法工作的原因。
返回頁首
線程池飽和
在執行數據庫查詢並等待 15 秒或更長時間來獲得返回的查詢結果時,我經常對看到的實際的 ASP.NET 頁數感到非常驚訝。(我也等待了 15 分鐘才看到查詢結果!)有時,延遲是由於返回的數據量很大而導致的不可避免的無奈結果;而有時,延遲則是由於數據庫的設計不佳導致的。但不管是什麼原因,長時間的數據庫查詢或任何類型的長時間 I/O 操作在 ASP.Net 應用程序中都會導致吞吐量的下降。
關於這個問題我以前已經詳細地描述過,所以在此就不再作過多的說明了。我只說一點就夠了,ASP.NET 依賴於有限的線程池處理請求,如果所有線程都被占用來等待數據庫查詢、Web 服務調用或其他 I/O 操作完成,則在某個操作完成並且釋放出一個線程之前,其他請求都必須排隊等待。當請求排隊時,性能會急劇下降。如果隊列已滿,則 ASP.Net 會使隨後的請求失敗並出現 HTTP 503 錯誤。這種情況不是我們希望在 Web 生產服務器的生產應用程序上所樂見的。
解決方案非異步頁面莫屬,這是 ASP.NET 2.0 中最佳卻鮮為人知的功能之一。對異步頁面的請求從一個線程上開始,但是當它開始一個 I/O 操作時,它將返回該線程以及 ASP.NET 的 IAsyncResult 接口。操作完成後,請求通過 IAsyncResult 通知 ASP.NET,ASP.Net 從池中提取另一個線程並完成對請求的處理。值得注意的是,當 I/O 操作發生時,沒有占用線程池線程。這樣可以通過阻止其他頁面(不執行較長的 I/O 操作的頁面)的請求在隊列中等待,從而顯著地提高吞吐量。
您可以在 MSDN®Magazine 的 2005 年 10 月刊中閱讀有關異步頁面的所有信息。I/O 綁定而不是計算機綁定且需要很長時間執行的任何頁面很有可能成為異步頁面。
當我將關於異步頁面的信息告知開發人員時,他們經常回答“那真是太棒了,但是我的應用程序中並不需要它們。”對此我回答說:“你們的任何頁面需要查詢數據庫嗎?它們調用 Web 服務嗎?您是否已經檢查 ASP.Net 性能計數器中關於排隊請求和平均等待時間的統計信息?即使您的應用程序至今運行正常,但是隨著您的客戶規模的增長,應用程序的負載可能會增加。”
實際上,絕大多數實際的 ASP.Net 應用程序都需要異步頁面。請切記這一點!
返回頁首
模擬和 ACL 授權
以下是一個簡單的配置指令,但是每當在 web.config 中看到它時都讓我眼前一亮:
<identity impersonate="true" />
此指令在 ASP.NET 應用程序中啟用客戶端模擬。它將代表客戶端的訪問令牌附加到處理請求的線程,以便操作系統執行的安全性檢查針對的是客戶端身份而不是輔助進程身份。ASP.Net 應用程序很少需要模擬;我的經驗告訴我,開發人員通常都是由於錯誤的原因而啟用模擬的。以下是原因所在。
開發人員經常在 ASP.NET 應用程序中啟用模擬,以便可以使用文件系統權限來限制對頁面的訪問。如果 Bob 沒有查看 Salaries.aspx 的權限,則開發人員將會啟用模擬,以便可以通過將訪問控制列表 (ACL) 設置為拒絕 Bob 的讀取權限,阻止 Bob 查看 SalarIEs.aspx。但是存在以下隱患:對於 ACL 授權來說,模擬是不必要的。在 ASP.NET 應用程序中啟用 Windows 身份驗證時,ASP.Net 會自動為請求的每個 .ASPx 頁面檢查 ACL 並拒絕沒有讀取文件權限的調用者的請求。即使禁用了模擬,它仍會這樣操作。
有的時候需要證明模擬的合理性。但是您通常可以用良好的設計來避免它。例如,假定 Salaries.aspx 在數據庫中查詢只有管理人員才能知道的工資信息。通過模擬,您可以使用數據庫權限拒絕非管理人員查詢工資數據的能力。或者您可以不考慮模擬,並且通過為 SalarIEs.ASPx 設置 ACL 以使非管理人員不具有讀取權限,從而限制對工資數據的訪問。後一種方法提供的性能更佳,因為它完全避免了模擬。它也消除了不必要的數據庫訪問。為什麼查詢數據庫僅由於安全原因被拒絕?
順便說一下,我曾經幫助對一個傳統的 ASP 應用程序進行故障排除,該應用程序由於內存占用不受限制而定期重新啟動。一個沒有經驗的開發人員將目標 SELECT 語句轉換成了 SELECT *,而沒有考慮要查詢的表包含圖像,這些圖像很大而且數目很多。問題由於未檢測到內存洩漏而惡化。(我的托管代碼領域!)多年來運行正常的應用程序開始突然停止工作,因為以前返回一兩千字節數據的 SELECT 語句現在卻返回了幾兆字節。如果再加上不充分的版本控制,開發團隊的生活將不得不“亢奮起來”— 這裡所謂的“亢奮”,就如同當您在晚上要睡覺時,還不得不看著您的孩子玩令人厭煩的足球游戲一樣。
理論上,傳統的內存洩漏不會發生在完全由托管代碼組成的 ASP.NET 應用程序中。但是內存使用量不足會通過強制垃圾收集更頻繁地發生而影響性能。即使是在 ASP.Net 應用程序中,也要警惕 SELECT *!
返回頁首
不要完全信賴它 — 請設置數據庫的配置文件!
作為一名顧問,我經常被詢問為何應用程序沒有按預期執行。最近,有人詢問我的團隊為何 ASP.Net 應用程序只完成請求文檔所需吞吐量(每秒的請求數)的大約 1/100。我們以前所發現的問題是我們在不能正常運行的 Web 應用程序中發現的問題特有的 — 和我們所有人應該認真對待的教訓。
我們運行 SQL Server Profiler 並監視此應用程序和後端的數據庫之間的交互情況。在一個更極端的案例中,僅僅只是一個按鈕單擊,就導致數據庫發生了 1,500 多個錯誤。您不能那樣構建高性能的應用程序。良好的體系結構總是從良好的數據庫設計開始。不管您的代碼的效率有多高,如果它被編寫不佳的數據庫所拖累,就會不起作用。
糟糕的數據訪問體系結構通常源於下面的一個或多個方面:
• 拙劣的數據庫設計(通常由開發人員設計,而不是數據庫管理員)。
• DataSets 和 DataAdapters 的使用 — 尤其是 DataAdapter.Update,它適用於 Windows 窗體應用程序和其他胖客戶端,但是對於 Web 應用程序來說通常不理想。
• 具有拙劣編制計算程序、以及執行相對簡單的操作需消耗很多 CPU 周期的設計糟糕的數據訪問層 (DAL)。
必須先確定問題才能對其進行處理。確定數據訪問問題的方式是運行 SQL Server Profiler 或等效的工具以查看後台正在執行的操作。檢查應用程序和數據庫之間的通信之後,性能調整才完成。嘗試一下 — 您可能會對您的發現大吃一驚。
返回頁首
結論
現在您已經了解在生成 ASP.NET 生產應用程序過程中可能遇到的一些問題及其解決方案了。下一步是仔細查看您自己的代碼並嘗試避免我在此概述的一些問題。ASP.Net 可能降低了 Web 開發人員的門檻,但是您的應用程序完全有理由靈活、穩定和高效。請認真考慮,避免出現新手易犯的錯誤。
圖 8 提供了一個簡短檢查列表,您可以使用它來避免本文中描述的缺陷。您可以創建一個類似的安全缺陷檢查列表。例如:
• 您是否已經對包含敏感數據的配置節進行加密?
• 您是否正在檢查並驗證在數據庫操作中使用的輸入,是否使用了 Html編碼輸入作為輸出?
• 您的虛擬目錄中是否包含具有不受保護的擴展名的文件?
如果您重視網站、承載網站的服務器以及它們所依賴的後端資源的完整性,則這些問題非常重要。
Jeff Prosise 是對 MSDN Magazine 貢獻很大的編輯以及多本書籍的作者,這些書籍中包括 Programming Microsoft .Net (Microsoft Press, 2002)。他也是軟件咨詢和教育公司 Wintellect 的共同創始人。
摘自 MSDN Magazine 的 2006 年 7 月刊。
Trackback: http://tb.blog.csdn.Net/TrackBack.ASPx?PostId=1487825