最近,我们干了一件“惊天动地”的事——对改了十年、代码混乱无比、WebForms与MVC混血、ADO.NET与Entity Framework混合的博客程序,用.NET 4.5的async/await特性进行了异步化改造。主要的异步化改造已于昨天完成,并在昨天晚上发布了异步化改造后的博客程序。
触动我们进行这次异步化改造的是ASP.NET官网上一篇文章()中的一段话:
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 TaskSiteHome(int? pageIndex){ //...}
2. Entity Framework的异步化
也很轻松,查询时只需使用异步LINQ:
public async Task 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" %>
异步时代:
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);如果有,该线程会将当前请求放回队列中,然后抽身去处理其它请求。当异步任务完成时,该请求会被线程池中的某个线程捡起,直到执行完成。(参考自)。
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博客中的博文,修改了EnyimMemcached,实现了Memcached客户端的异步化,修改后的代码已发布至github()。
public async Task> GetAsync (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)的使用。
【参考资料】