2014-02-14 90 views
18

我们最近开发了基于SOA的网站,但这个网站最后不得不可怕的负载和性能问题时,它负载下去。我张贴有关这个问题在这里一个问题:异步并等待:它们不好吗?

ASP.NET website becomes unresponsive under load

该网站由被4个节点的集群,这是另一个4-托管的网站上托管的API(WEB API)网站节点集群并调用API。两者都是使用ASP.NET MVC 5开发的,所有操作/方法都基于异步等待方法。

在一些监控工具(如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(); 
+2

有关应用程序如何使用async/await的任何示例?当与IO绑定操作(如数据库调用和文件操作)一起使用时,通常应该提高可伸缩性,而不是降低它。如果它产生了不必要的线程,那么每个方法都可以标记为异步,这可能是一个红旗,可能是你的问题的原因。 –

+1

也许,你的代码使用'Task.Run'封装同步API,而不是使用自然的异步API? http://stackoverflow.com/q/21690385/1768303。通常,在服务器上使用'Task.Run'是个不错的主意。 – Noseratio

+0

是MVC 5吗?或者你的标签说MVC 3? –

回答

34

这种方法有什么愚蠢的东西吗?请注意,当我们将 所有方法转换为非异步方法时,我们获得了更好的性能。

我至少可以看到这两者是错在这里:

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); 
     } 
    } 

首先,你传递给ContinueWith拉姆达阻止:

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的池线程。这有效地消除了自然异步API的优势,并降低了您的Web应用程序的可伸缩性。在你的代码中注意这样的其他地方。

其次,ASP.NET请求由安装了特殊同步上下文的服务器线程处理,AspNetSynchronizationContext。如果继续使用await,则继续回调将发布到同一个同步上下文,编译器生成的代码将处理此问题。 OTOH,当你使用ContinueWith时,这不会自动发生。

因此,你需要明确地提供正确的任务调度,去除阻塞.Result(这将返回一个任务)和Unwrap嵌套任务:

return 
    await 
     httpClient.PostAsJsonAsync(action, parameters, ctk).ContinueWith(
      x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }), 
      ctk, 
      TaskContinuationOptions.None, 
      TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); 

这就是说,你真的不需要这样的加入ContinueWith复杂性在这里:

var x = await httpClient.PostAsJsonAsync(action, parameters, ctk); 
return await x.Content.ReadAsAsync<T>(new[] { formatter }); 

由Stephen Toub下面的文章是高度相关:

"Async Performance: Understanding the Costs of Async and Await"

如果我要调用一个异步方法在同步方面,其中使用的await 是不可能的,什么是做的最好的方法是什么?

你几乎永远不应该需要混合awaitContinueWith,你应该坚持await。基本上,如果你使用async,它必须是异步"all the way"

对于服务器侧ASP.NET MVC /网络API的执行环境,它只是意味着控制器方法应该是async并返回一个或TaskTask<>,检查this。 ASP.NET跟踪给定HTTP请求的未完成任务。在所有任务完成之前,请求没有完成。

如果你真的需要调用从ASP.NET同步方法的async方法,你可以使用AsyncManagerthis注册一个未决任务。对于传统的ASP.NET,您可以使用PageAsyncTask

在最坏的情况下,您会拨打task.Wait()并阻止,因为否则您的任务可能会继续超出该特定HTTP请求的范围。

对于客户端UI应用程序,从同步方法中调用async方法可能会出现一些不同的情况。例如,您可以使用ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext())并从action(如this)发起完成事件。

+1

那里好抓。我怀疑ContinueWith,但我无法明确指出问题所在。我似乎很清楚有些东西阻止线程返回到线程池。我喜欢你完全摆脱ContinueWith的解决方案。简单得多。 –

+0

@ErikTheViking,也许有一些这样的片段。或者就像在紧密的循环中调用它。我不确定单个阻止呼叫可能会为16个用户造成问题。 – Noseratio

+1

@Noseratio你是一个天才!我做了这个改变,我的负载测试结果大大提高了!一个问题:如果我必须在同步上下文中调用异步方法,那么在何处使用await是不可能的,那么执行此操作的最佳方式是什么? – Aref

3

异步和等待不应该创建大量线程的,特别不只有16个用户。事实上,它应该可以帮助你更好地利用线程。 MVC中异步和等待的目的是当它忙于处理IO绑定任务时,实际上放弃线程池线程。这表明你在某个地方做了一些愚蠢的事情,比如产生线程,然后无限期地等待。

不过,900线程并不是很多,如果他们使用100%cpu,那么他们不会等待......他们正在咀嚼什么。这是你应该关注的东西。你说过你已经使用过NewRelic这样的工具,他们指出哪些是CPU使用率的来源?什么方法?

如果我是你,我会首先证明,仅仅使用异步和等待不是你的问题的原因。只需创建一个模拟该行为的简单网站,然后在其上运行相同的测试。

其次,获取您的应用程序的副本,并开始剥离东西,然后运行测试。看看你是否可以追查问题的确切位置。

+0

我怎么找到那个愚蠢的东西?我们已经对几乎整个网站和API进行了简介,并修复了每个性能瓶颈。 – Aref

+0

@Aref:你应该寻找任何使用'Task.Run','Task.Factory.StartNew'或'Task.Start';这些不应该在ASP.NET中使用。此外,任何使用“并行”或并行LINQ。 –

+0

@Aref - 我给了你一些你可以使用的技巧,比如制作一个副本并剥离东西,直到问题消失。 –

3

有很多东西要讨论。

首先,当应用程序几乎没有业务逻辑时,async/await可以自然地帮助您。我的意思是async/await的意思是在睡眠模式下没有多线程等待某些东西,大部分是IO,例如数据库查询(和读取)。如果您的应用程序使用cpu 100%进行巨大的业务逻辑,则异步/等待不会对您有所帮助。

900线程的问题在于它们效率低下 - 如果它们同时运行。重点在于拥有如此数量的“业务”线程会更好,因为服务器具有核心/处理器。原因是线程上下文切换,锁争用等。有很多像LMAX distruptor模式或Redis这样的系统在一个线程中处理数据(或者每个核心中有一个线程)。这更好,因为你不必处理锁定。

如何达到所述的方法?查看干扰程序,对传入的请求进行排队并逐个处理它们,而不是并行处理。

相反的方法,当几乎没有业务逻辑时,许多线程只是等待IO是将异步/等待放入工作的好地方。

它是如何工作的:有一个线程从网络读取字节 - 大多只有一个。一旦某些请求到达,该线程将读取数据。处理请求的工作线程池也很有限。异步的一点是,一旦一个处理线程正在等待某些事情,主要是io,db,该线程将以轮询返回,并可用于其他请求。一旦IO响应准备就绪,池中的某个线程将用于完成处理。这就是如何在一秒钟内使用少量线程来处理千次请求的方式。

我建议你应该画一些图片你的网站是如何工作的,每个线程做了什么以及它如何同时工作。请注意,有必要确定吞吐量或延迟对您是否重要。