如果你在構建一個面向公眾的web站點,那麼在專案結尾時你想要實現的就是web負載效能良好。這意味著,你要確保你的產品在高負載下(50個併發使用者或者每秒200個使用者等等)能夠執行,即使你認為此時不會有那麼大的負載。久而久之,你的web站點可能吸引越來越多的使用者,此時如果web的負載難以讓人忍受時,那麼自然而然網站開始走下坡路,意味著客戶流失以及名譽受損。
那麼可以採取哪些措施可以使得一個ASP.NET或者ASP.NET MVC應用更加高效能呢?
早期階段就要對應用進行負載測試
大多數開發者趨向在應用開發完成後,整合測試和迴歸測試通過後才進行負載測試。儘管在開發完成後執行一次負載測試好過不做,但是一旦完成了程式碼的編寫,修復效能問題就為時已晚了。這個問題最常見的例子就是當應用程式在負載測試時不能正確響應時,就會考慮向外擴充套件(增加更多的伺服器)。有時這是不可能的,因為程式碼不適合實現擴充套件伺服器。比如當物件是儲存在Session中並不可序列化時,這樣新增更多的web節點或者工作者程式就是不可能的。如果你在開發的早期階段就發現你的應用可能要部署到多臺伺服器上,那麼你應該在配置和伺服器的數量等方面都要讓測試環境和最終環境儘可能地接近,這樣你的程式碼會更容易適應。
使用高效能類庫
近來我在診斷web站點的效能問題時發現了程式碼中的一個熱點問題:來自第三方web服務的JSON資訊必須要被反序列化多次。那些Json資訊是由Newtonsoft.Json反序列化的,並且證明了Newtonsoft.Json在反序列化時不是最快的類庫,然後我們使用了一個更快的類庫(如ServiceStack)替代了Json.Net,並獲得了更好的結果。
如果我們在挑選了Json.Net作為序列化類庫的早期階段就完成了負載測試,那麼我們就會更早地發現效能問題,我們就不必再程式碼中做這麼多的修改了,也不必再次完整地重新測試了。
你的應用是CPU密集還是IO密集的?
在開始實現web應用以及設計專案時,你要首先考慮的一件事就是你的應用是CPU密集的還是IO密集的?這對於瞭解擴充套件應用的策略是很重要的。
比如,如果你的應用是CPU密集的,那麼你可能想使用同步模式,並行處理等等。然而,對於一個有很多IO受限的操作(例如和外部web服務或者網路資源如資料庫通訊)的產品,基於Task的非同步模式可能對於向外擴充套件更有幫助。此外,為了在未來的某天能夠建立Web Gardens和Web Farms,你可能想有一個集中式的快取系統,這樣就實現了跨越多個工作者程式或服務的負載。
使用基於Task的非同步模型,但要慎重
如果你的產品依賴許多IO受限的操作,或者包括了可能需要消耗寶貴的IIS執行緒等待一個操作完成的長時間執行的操作,你最好為ASP.NET MVC專案考慮使用基於Task的非同步模式。
網際網路上有很多關於非同步的ASP.NET MVC actions的手冊(比如這個),所以這篇部落格儘量避免介紹關於非同步的知識點。然而,必須指出ASP.NET (MVC)中傳統的同步action會造成IIS執行緒繁忙直到操作完成或請求處理完成。這意味著如果如果web應用在等待一個外部的資源(如一個web服務)響應,那麼該執行緒將是繁忙的。.Net執行緒池中可以用於處理請求的執行緒數量也是有限的,因此,儘可能快地釋放執行緒是很重要的。基於Task的非同步action或方法會在請求處理後釋放該執行緒,然後從執行緒池中獲取新的執行緒,並使用該新的執行緒返回該action的結果。這樣,多個請求可以由多個執行緒處理,這會為你的應用帶來更好的響應。
雖然TAP模式對於適當的應用來說很方便,但必須要慎重使用。當你使用TAP設計或者實現一個專案時,必須要注意幾個點,然而,開發者使用async和await關鍵字面臨的最大挑戰是知道在這個上下文中他們必須略有不同地處理執行緒。比如,可以建立一個返回Task(如Task《Product》,部落格園的markdown不支援單尖括號)的方法,通常你可以對那個Task呼叫Run()方法或者只呼叫task.Result來迫使執行該task,直到拿到結果。
分發快取和會話(session)狀態
開發者在單個開發機器上構建一個web應用是非常常見的,並且也會假設該產品執行在單個伺服器上,然而對於面向大眾的web通常不是這樣的。它們往往被部署到一個叫做負載均衡之後的多個伺服器上。儘管你仍然可以使用In-Proc(關於In-Proc和Out-Proc的知識點補充)的模式和粘性(sticky)Session將一個web應用部署到多個伺服器上(負載均衡器會將屬於相同session的所有請求定向到單個伺服器),你可能必須保留session資料和快取資料的多個副本。
比如,如果你將產品部署到由4臺伺服器組成的web farm上,並且你保持session資料為In-Proc,那時一個請求到達一個已經包含快取資料的機會是四分之一或25%,然而你如果在正確的地方使用了集中式快取機制,那麼為每個請求找到快取條目的機會就是100%。這對於嚴重依賴快取資料的web應用是至關重要的。
使用集中式快取機制(像App Fabric或Redis)的另一個好處是對實際的產品實現主動快取系統的能力。主動快取機制可以用於在客戶端請求一個條目之前就可以將最受歡迎的條目預載入到快取中。如果你使用實際的資料來源來管理快取同步時,那麼這會幫助你大幅地改善大資料驅動應用的效能。
建立Web Gardens
之前提到,在一個IO受限的web應用中包含了很多長時間執行的操作(如web服務的呼叫),此時你可能想盡可能地釋放主執行緒。預設情況下,每個web應用執行在一個主執行緒下,該主執行緒為保持web站點存活(alive)負責,但不幸的是,當它太忙的時候,你的站點就變得不能響應了。給應用新增更多的“主執行緒”是一種辦法,這是通過將更多的工作者程式新增到IIS下的Web站點來實現的。每個工作者程式都會包括一個單獨的主執行緒,因此,如果一個主執行緒繁忙,還有另外的主執行緒來處理到來的請求。
擁有多個工作者程式會將你的web站點變成一個Web Garden,這要求你將Session和應用資料持久化到Out-Proc中(例如一個state server或者Sql Server)。
巧妙地使用快取和懶載入
無需強調,如果你將經常訪問的一點資料快取到記憶體中,那麼這會減少資料庫和web服務的呼叫。正如之前所說,這對於IO受限的應用特別有幫助,當網站處於低負載時可能會造成不幸。
提高站點響應的另一種方式是使用懶載入。懶載入意味著應用裡不會有確定的資料片,但是它知道資料在哪裡。比如,如果web頁面上有一個下拉選單,這意味著要顯示一個產品列表,一旦頁面載入完畢,不必從資料庫中載入所有的產品。你可以在頁面中新增一個jQuery函式,該函式可以在第一次下拉時填充下拉選單。你也可以在程式碼中的許多地方使用相同的技術,比如當使用Linq查詢和CLR集合時。
不要在MVC檢視中放C#程式碼
ASP.NET MVC檢視會在執行時編譯而不是在編譯時,因此,如果在檢視中包含了太多的C#程式碼,那麼你的程式碼不會編譯後放到dll檔案中。這樣做不僅有害於軟體的可測試性,還會使web應用更慢,因為每個頁面都有花更長的時間獲得顯示(因為它們必須被編譯)。將程式碼新增到檢視中的其他缺陷在於它們不能非同步執行,這樣,如果你決定基於TAP來構建應用時,就不能充分利用檢視中的非同步方法和action了。
比如,如果你的程式碼中有這麼一個方法:
1 2 3 4 5 6 7 8 |
public async Task GetName(int code) { var result = … return await result; } |
該方法可以在一個非同步的ASP.NET MVC action的上下文中非同步執行,比如:
1 2 3 4 |
public Task Index( CancellationToken ctx ) { var name = await GetName( 100 ); } |
但是如果在一個檢視中呼叫這方法,因為該檢視不是非同步的,所以必須以一種執行緒阻塞(thread-blocking)的方式執行,如:
var name = GetName(100).Result;
.Result
會阻塞執行的執行緒,直到GetName()
處理完成了請求,這樣該應用的執行會停止一會兒,然而當使用await關鍵字呼叫該程式碼時,該執行緒不會被阻塞。
適當時使用Fire & Forget
注:Fire & Forget,字面意思是發射,然後忘記,不去理會了。網路釋義為射後不理。
如果兩個或多個操作沒有生成單個事務,那麼你很可能不必按順序執行它們。比如,如果一個使用者在你的站點註冊時建立了一個賬戶,一旦他們註冊了,你就把他們的詳細資訊儲存到資料庫,然後給他們傳送一封郵件,你不必等待郵件傳送出去才結束這個操作。
在這種情況下,最好的方法很可能就是開啟一個新的執行緒,讓它給使用者傳送郵件,然後返回到主執行緒。這就叫做Fire& Forget,它可以提高應用的響應能力。
為x64 CPU建立
32-bit的應用侷限於較低的記憶體量和可以訪問更少的CPU功能/指令。要克服這些限制,如果你的伺服器是64-bit的,那麼要確保你的應用執行在64-bit模式下(通過確保在IIS的32-bit模式下執行網站的選項是不是開啟)。然後為x64 CPU編譯並生成程式碼而不是Any CPU。
x64有用的一個例子是提高資料驅動應用的響應能力和效能,有一個好的快取機制是必須的。In-proc是消耗記憶體的,因為一切都儲存在網站的應用程式池的記憶體邊界。對於x86程式來說,可以分配的記憶體量限制到4GB,這樣,如果載入到快取中,這個限制很快就被打破。如果相同的站點是為x64 CPU顯式生成的,那麼記憶體限制就不需考慮了,這樣更多的條目可以加到快取中,然後和資料庫的通訊就少了,這會帶來更好的效能。
使用伺服器上的監視和診斷工具
可能存在你肉眼看不到的更多的效能問題,因為它們不會出現在錯誤日誌中。當應用程式已經部署到基本沒有機會除錯的伺服器上時,識別效能問題是更嚇人的事情。
要找出緩慢的處理,執行緒阻塞,懸掛,錯誤等等,強烈建議在伺服器上安裝一個監視和診斷工具,讓它們持續地跟蹤和監視你的應用程式。我個人使用的是NewRelic檢查我的線上網站的健康。
分析執行中的應用
一旦完成了網站開發,部署到IIS,然後附加一個分析器(如Visual Studio Profiler),然後抓取應用的多個部分的快照。比如抓取購買或者使用者註冊等操作的快照,然後檢查病檢視是否存在任何造成緩慢或阻塞的程式碼。在早期階段找到那些熱點可能會節省你大量的時間,榮譽和金錢。