最近,我们干了一件“惊天动地”的事——对改了十年、代码混乱无比、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?
Awaiting Socket Operations