ASP.NET website becomes unresponsive under load
该站点由API(WEB API)站点组成,该站点位于4节点群集和托管在另一个4节点群集上的网站上,并对API进行调用。两者都是使用ASP.NET MVC 5开发的,所有操作/方法均基于async-await方法。
在一些监控工具(如NewRelic)上运行该站点后,调查了多个转储文件并对工作进程进行了分析,结果发现,在非常轻的负载下(例如16个并发用户),我们最终拥有大约900个线程,它们使用了100%的cpu并填写了IIS线程队列!
尽管我们设法通过引入堆高速缓存和性能修正来将站点部署到生产环境中,但我们团队中的许多开发人员都认为我们必须删除所有异步方法,并将API和网站都隐藏到正常的Web API和Action方法中只需返回一个Action结果。
我个人对方法感到不高兴,因为我的直觉是我们没有正确使用异步方法,否则这意味着微软推出了基本上是相当破坏性和不可用的功能!
你知道任何清除它的参考,应该/可以使用异步方法在哪里和如何使用?我们应该如何使用它们来避免这样的戏剧?例如根据我在MSDN上阅读的内容,我相信API层应该是异步的,但是网站可能是一个正常的不同步ASP.NET MVC网站。
更新:
这是使用与API进行通信的异步方法。
public static async Task<T> GetApiResponse<T>(object parameters,string action,CancellationToken ctk) { using (var httpClient = new HttpClient()) { httpClient.BaseAddress = new Uri(BaseApiAddress); var formatter = new JsonMediaTypeFormatter(); return await httpClient.PostAsJsonAsync(action,parameters,ctk) .ContinueWith(x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result,ctk); } }
这种方法有什么傻的吗?请注意,当我们将所有方法转换为非异步方法时,我们得到了更好的性能。
这是一个示例使用(我已经剪切了与验证,记录等相关的代码的其他位。这段代码是MVC操作方法的主体)。
在我们的服务包装:
public async static Task<IList<DownloadType>> GetSupportedContentTypes() { string userAgent = Request.UserAgent; var parameters = new { Util.AppKey,Util.StoreId,QueryParameters = new { UserAgent = userAgent } }; var taskResponse = await Util.GetApiResponse<ApiResponse<SearchResponse<ProductItem>>>( parameters,"api/Content/ContentTypeSummary",default(CancellationToken)); return task.Data.Groups.Select(x => x.DownloadType()).ToList(); }
而在行动中:
public async Task<ActionResult> DownloadTypes() { IList<DownloadType> supportedTypes = await ContentService.GetSupportedContentTypes();
解决方法
Is there anything silly with this method? Note that when we converted
all method to non-async methods we got a heaps better performance.
我可以看到至少有两件事情出现在这里:
public static async Task<T> GetApiResponse<T>(object parameters,ctk) .ContinueWith(x => x.Result.Content .ReadAsAsync<T>(new[] { formatter }).Result,ctk); } }
首先,你传递给ContinueWith的lambda是阻止:
x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result
这相当于:
x => { var task = x.Result.Content.ReadAsAsync<T>(new[] { formatter }); task.Wait(); return task.Result; };
因此,您正在阻止一个在其上执行lambda的池线程。这有效地消除了自然异步ReadAsAsync API的优势,并降低了Web应用程序的可扩展性。在您的代码中注意其他类似的地方。
其次,ASP.NET请求由安装了特殊同步上下文AspNetSynchronizationContext的服务器线程处理。当使用等待继续时,继续回调将被发布到相同的同步上下文中,编译器生成的代码将会处理这一点。 OTOH,当您使用ContinueWith时,这不会自动发生。
因此,您需要显式提供正确的任务调度程序,删除阻止的.Result(这将返回一个任务)并解开嵌套任务:
return await httpClient.PostAsJsonAsync(action,ctk).ContinueWith( x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }),ctk,TaskContinuationOptions.None,TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
也就是说,你真的不需要这样的附加复杂性ContinueWith这里:
var x = await httpClient.PostAsJsonAsync(action,ctk); return await x.Content.ReadAsAsync<T>(new[] { formatter });
以下文章由Stephen Toub高度相关:
“Async Performance: Understanding the Costs of Async and Await”。
If I have to call an async method in a sync context,where using await
is not possible,what is the best way of doing it?
你几乎不需要混合等待和继续,你应该坚持等待。基本上,如果你使用异步,它必须是async “all the way”。
对于服务器端ASP.NET MVC / Web API执行环境,它只是意味着控制器方法应该是异步的,并返回一个任务或任务,请检查this. ASP.NET跟踪给定HTTP请求的待处理任务。在所有任务完成之前,请求尚未完成。
如果您真的需要从ASP.NET中的同步方法调用异步方法,则可以使用像this这样的AsyncManager来注册待处理的任务。对于经典的ASP.NET,您可以使用PageAsyncTask
。
在最坏的情况下,您可以调用task.Wait()并阻止,否则您的任务可能会继续超出该特定HTTP请求的边界。
对于客户端UI应用程序,从同步方法调用异步方法可能会有一些不同的情况。例如,您可以使用ContinueWith(action,TaskScheduler.FromCurrentSynchronizationContext())并从动作触发完成事件(如this)。