【问题标题】:async and await: are they bad?async 和 await:它们不好吗?
【发布时间】:2014-03-13 08:00:25
【问题描述】:

我们最近开发了一个基于 SOA 的站点,但是该站点在负载过重时最终会出现严重的负载和性能问题。我在这里发布了一个与此问题相关的问题:

ASP.NET website becomes unresponsive under load

该站点由托管在 4 节点集群上的 API(WEB API)站点和托管在另一个 4 节点集群上并调用 API 的网站组成。两者都是使用 ASP.NET MVC 5 开发的,所有操作/方法都基于 async-await 方法。

在 NewRelic 等一些监控工具下运行该站点,调查几个转储文件并分析工作进程后,事实证明,在非常轻的负载(例如 16 个并发用户)下,我们最终拥有大约 900 个线程,其中使用了 100 个% 的 CPU 并填满了 IIS 线程队列!

尽管我们通过引入大量缓存和性能修正来设法将站点部署到生产环境,但我们团队中的许多开发人员认为我们必须删除所有异步方法并将 API 和网站转换为普通 Web API 和简单地返回一个动作结果的动作方法。

我个人对这种方法不满意,因为我的直觉是我们没有正确使用异步方法,否则这意味着微软引入了一个基本上具有破坏性且无法使用的功能!

您是否知道任何参考资料清楚说明应该/可以在何处以及如何使用异步方法?我们应该如何使用它们来避免这样的戏剧?例如根据我在 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();

【问题讨论】:

  • 应用程序如何使用 async/await 的任何示例?当与 IO 绑定操作(如数据库调用和文件操作)一起使用时,它通常应该增加可伸缩性,而不是降低它。如果它产生了不必要的线程,以便每个方法都可以标记为异步,那可能是一个危险信号,并且可能是您的问题的原因。
  • 也许,您的代码使用Task.Run 包装了同步API,而不是使用自然的异步API? stackoverflow.com/q/21690385/1768303。一般来说,在服务器上使用Task.Run 是个坏主意。
  • 是 MVC 5 吗?还是您的标签所说的 MVC 3?
  • @ErikTheViking 抱歉我修改了标签。
  • @AnthonyChu,WEB 和 API(消费者和服务)中的几乎所有方法和操作方法都是异步的!今天我也创建了一个完全同步的版本,并将它们置于负载测试之下。令人惊讶的是,如果我直接点击 API,API 的异步版本比同步版本更快。但是当我使用 Apache 基准测试工具访问网站时,API 的异步版本慢了三倍!您认为仅将与 DB 或 ElasticSearch 通信的方法转换为异步方法更好吗?正如 Noseratio 所说,使用自然异步 API?

标签: performance asp.net-mvc-4 async-await


【解决方案1】:

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

我可以看到这里至少有两个问题:

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 的 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 的服务器线程处理。当您使用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 不可能,最好的方法是什么?

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

对于服务器端 ASP.NET MVC / Web API 执行环境,它只是意味着控制器方法应该是async 并返回一个Task 或Task&lt;&gt;,检查this。 ASP.NET 跟踪给定 HTTP 请求的挂起任务。在完成所有任务之前,请求不会完成。

如果您真的需要从 ASP.NET 中的同步方法调用 async 方法,您可以使用 AsyncManager 和 this 一样来注册挂起的任务。对于经典的 ASP.NET,您可以使用PageAsyncTask。

在最坏的情况下,您会调用 task.Wait() 并阻止,否则您的任务可能会在该特定 HTTP 请求的边界之外继续。

对于客户端 UI 应用,从同步方法调用 async 方法可能会有一些不同的场景。例如,您可以使用ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext()) 并从action 触发完成事件(如this)。

【讨论】:

  • 很好。我对 ContinueWith 持怀疑态度,但我无法具体指出问题所在。我似乎很清楚,有什么东西阻止了线程返回到线程池。我喜欢你完全摆脱 ContinueWith 的解决方案。简单得多。
  • @ErikTheViking,也许有一些这样的片段。或者像在一个紧密的循环中调用它。我不确定单个阻塞调用是否会导致 16 个用户出现问题。
  • @Noseratio 你是个天才!我做了那个改变,我的负载测试结果大大提高了!一个问题:如果我必须在无法使用 await 的同步上下文中调用异步方法,那么最好的方法是什么?
  • @Aref,很高兴它有帮助。我已更新答案以解决您关于从同步方法调用 async 方法的问题。
【解决方案2】:

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

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

如果我是你,我会首先证明仅仅使用 async 和 await 并不是你的问题的原因。只需创建一个模仿行为的简单站点,然后在其上运行相同的测试。

其次,复制您的应用,然后开始剥离内容,然后针对它运行测试。看看你能不能找到问题的确切位置。

【讨论】:

  • 我怎样才能找到那个愚蠢的东西?我们已经分析了几乎整个网站和 API,并修复了每个性能瓶颈。
  • @Aref:您应该寻找Task.Run、Task.Factory.StartNew 或Task.Start 的任何用途;这些不应该在 ASP.NET 中使用。此外,Parallel 或并行 LINQ 的任何使用。
  • @Aref - 我给了你一些你可以使用的技巧,比如制作一个副本并删除一些东西直到问题消失。
  • @ErikTheViking,谢谢埃里克。事实上,我们的 API 基于第三方 RESTful 服务。 API 中有一个方法可以对该第三方服务进行所有 http 调用。我们使用 HttpClient.PostJasonAsync 等以异步方式构建了该 API。由于该核心方法是异步的,因此我们的整个 API 和 WEB(与 API 通信的方法相同)从头到脚都是异步的!我希望我们可以使通信异步的方法并使项目的其余部分保持非异步。有可能吗?
  • @Aref - 单个 Web 请求会进行多少个 Restful 服务调用?
【解决方案3】:

有很多东西要讨论。

首先,当您的应用程序几乎没有业务逻辑时,async/await 可以很自然地为您提供帮助。我的意思是异步/等待的重点是不要有很多线程处于睡眠模式等待某些东西,主要是一些 IO,例如数据库查询(和获取)。如果您的应用程序 100% 使用 cpu 执行庞大的业务逻辑,那么 async/await 对您没有帮助。

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

如何达到描述的方法?查看中断器,将传入请求排队并一一处理,而不是并行处理。

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

它的主要工作原理:有一个线程从网络读取字节 - 大部分只有一个。一旦某个请求到达,该线程就会读取数据。处理请求的工作线程池也有限。异步的要点是,一旦一个处理线程正在等待某些东西,主要是 io、db,该线程就会在 poll 中返回,并且可以用于另一个请求。一旦 IO 响应准备好,就会使用池中的一些线程来完成处理。这就是您可以使用少量线程在一秒钟内处理数千个请求的方式。

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

【讨论】:

    猜你喜欢
    • 2018-02-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-12
    • 2018-04-13
    • 2014-05-02
    • 1970-01-01
    • 2015-05-04
    • 1970-01-01
    相关资源
    最近更新 更多