最近,我們干了一件“驚天動地”的事——對改了十年、代碼混亂無比、WebForms與MVC混血、ADO.NET與Entity Framework混合的博客程序,用.NET 4.5的async/await特性進行了異步化改造。主要的異步化改造已於昨天完成,並在昨天晚上發布了異步化改造后的博客程序。
觸動我們進行這次異步化改造的是ASP.NET官網上一篇文章(Using Asynchronous Methods in ASP.NET 4.5)中的一段話:
A web application using synchronous methods to service high latency calls where the thread pool grows to the .NET 4.5 default maximum of 5, 000 threads would consume approximately 5 GB more memory than an application able the service the same requests using asynchronous methods and only 50 threads.
在高延遲操作場景下,同步方式需要5000個線程才能完成的工作,采用異步方式只需50個線程!以一敵百,如此的高效,怎能不讓人心動。
而itworld一篇文章中的一句話更是火上澆油,讓我們下定決心實現異步化。
I’ve seen load tests show 300% improvement in response times and concurrent connections boost almost 8x over the synchronous counterparts.
此次異步化改造一共有6個部分,其中三個部分的改造最輕松,它們是MVC,EF,WCF;而另外三個則最艱苦,它們是WebForms,ADO.NET,EnyimMemcached(memcached .NET客戶端)。
下面分別簡單介紹一下這6個部分的改造:
1. MVC的異步化改造
無比輕松,只要把ActionResult改為async Task<AstionResult>:
public async Task<ActionResult> SiteHome(int? pageIndex) { //... }
2. Entity Framework的異步化
也很輕松,查詢時只需使用異步LINQ:
public async Task<int> GetAsync() { return await Entities .Where(...) .Select(...) .CountAsync(); }
保存時只需SaveChangesAsync():
async Task IUnitOfWork.CommitAsync() { await base.SaveChangesAsync(); }
3. WCF客戶端的異步化
照樣輕松,只要選擇“Generate task-based operations”重新生成WCF客戶端代理:
4. WebForms的異步化
a) 所有實現異步的.aspx都要加上async="true"標記。
<%@ Page Async="true" Language="c#"%>
b) 原來獲取數據進行綁定的代碼要放在異步方法中,並通過Page.RegisterAsyncTask進行注冊。
protected override void OnLoad(EventArgs e) { base.OnLoad(e); this.Page.RegisterAsyncTask(new System.Web.UI.PageAsyncTask(GetPostsByMonth)); }
c) 原來靜態綁定的用戶控件不得不改為動態加載。
同步時代:
<%@ Register TagPrefix="uc1" TagName="EntryList" Src="EntryList.ascx" %> <uc1:EntryList id="Days" DescriptionOnly = "true" runat="server"></uc1:EntryList>
異步時代:
public class ArchiveMonth : UserControl { protected override void OnLoad(EventArgs e) { base.OnLoad(e); this.Page.RegisterAsyncTask(new System.Web.UI.PageAsyncTask(GetPostsByMonth)); } private async Task GetPostsByMonth() { var DaysControl = LoadControl("EntryList.ascx") as EntryList; if (DaysControl != null) { DaysControl.EntryListItems = await postSevice.GetEntriesByMonth(CurrentBlog, dt, PostType.BlogPost); DaysControl.DescriptionOnly = true; Controls.Add(DaysControl); } } }
d) 原來在OnPreRender中的處理代碼(依賴異步任務的處理結果)需要移至Render,因為ASP.NET是在OnPreRender階段檢查所有注冊的異步任務並進行異步執行。
【WebFoms中的異步原理】
如果在.aspx中設置了async="true",ASP.NET線程在處理針對這個頁面的請求時,會在PreRender階段查找是否有注冊的異步任務(async task);如果有,該線程會將當前請求放回隊列中,然后抽身去處理其它請求。當異步任務完成時,該請求會被線程池中的某個線程撿起,直到執行完成。(參考自Async Pages part 2: How to use asynchrony in your Pages)。
5. ADO.NET的異步化
所有進行異步化的數據庫操作都需要用類似下面的ADO.NET代碼進行改造
using(var conn = new SqlConnection(connectionString)) { using(var command = conn.CreateCommand()) { command.CommandType = CommandType.StoredProcedure; command.CommandText = "..."; command.Parameters.AddWithValue("...", ...); await conn.OpenAsync(); using (IDataReader reader = await command.ExecuteReaderAsync()) { //... } } }
6. EnyimMemcached的異步化
也就是Socket的異步化,參考msdn博客中的博文Awaiting Socket Operations,修改了EnyimMemcached,實現了Memcached客戶端的異步化,修改后的代碼已發布至github(https://github.com/cnblogs/EnyimMemcached)。
public async Task<IGetOperationResult<T>> GetAsync<T>(string key) { //... var commandResult = await node.ExecuteAsync(command); //... }
【發布后的不理想情況】
1. CPU出現抖動
異步化改造后的博客程序發布后,在阿里雲雲服務器上CPU出現抖動,后來發展為瘋狂抖動。
最后放棄使用異步化的EnyimMemcached,改回原來同步的EnyimMemcached,CPU抖動情況得到了改善(后來發現異步化后的EnyimMemcached存在內存泄漏問題)。
a) 訪問低峰時的CPU抖動情況
b)訪問高峰時的CPU抖動情況
2. w3wp進程消耗的線程與內存更多
這個地方的表現讓人大跌眼鏡,原以為線程與內存的消耗會明顯降低,實際卻不但不降反而上升。
【更新1】
我們在負載均衡中加了另外一台雲服務器,不理想情況竟然沒出現。
后來,我們將原先2台表現不理想的服務器中的w3wp進程重啟后,不理想情況也消失了。昨天我們發布時只是更新了dll,並沒有對w3wp進程進行回收。
【更新2】
重啟w3wp進程之后,還是會出現CPU抖動的情況,但目前觀測下來對響應速度未造成影響。我們猜測CPU抖動可能與並行處理有關。
【更新3】
解決進展:
1. 發現一個異步方法中調用了System.Web.HttpContext.Current,去掉了這個調用。
2. 增加ConfigureAwait(false)的使用。
【參考資料】
Best Practices in Asynchronous Programming
Using Asynchronous Methods in ASP.NET 4.5
Async Pages part 2: How to use asynchrony in your Pages
How to create Asynchronous device Page in ASP.NET 4.5
Why you should use async tasks in .NET 4.5 and Entity Framework 6?