【问题标题】:Using AsyncController to help increase concurrency on a legacy ASP.NET MVC 3 project使用 AsyncController 帮助提高旧 ASP.NET MVC 3 项目的并发性
【发布时间】:2013-10-20 01:22:44
【问题描述】:

我们现在有一个网站正在与并发用户作斗争。

这是项目的非常高级的背景:

  • 旧版 ASP.NET MVC 3 项目 (.NET 4)
  • 无法对核心代码进行任何重大重写
  • 执行时间最长的主入口点是Search 控制器上的SubmitSearch 操作。平均响应时间为 5-10 秒。

因此,正如第二点所述,我们不想在这个项目上花费太多时间重写大段。但是,我们想尝试增加并发用户。我们不打算更改任何其他内容或提高性能,因为这需要更多的工作。

我们看到的是,随着更多人点击SubmitSearch,网站的速度总体上会变慢。这很可能是由于所有 IIS 线程都被锁定在执行搜索。

我们正在寻求实现AsyncController 并使SubmitSearch 操作在普通CLR 线程上执行。以下是我们想要实现它的方式:

假设这是原始的SubmitSearch 方法:

/// <summary>
/// Submits a search for execution.
/// </summary>
/// <param name="searchData">The search data</param>
/// <returns></returns>
public virtual ActionResult SubmitSearch(SearchFormModel searchData)
{
    //our search code
}

我们希望转换为AsyncController 的最快方法就是这样做:

/// <summary>
/// Submits a search for execution.
/// </summary>
/// <param name="searchData">The search data</param>
/// <returns></returns>
protected virtual ActionResult SubmitSearch(SearchFormModel searchData)
{
    //our search code
}

/// <summary>
/// Asynchronous Search entry point
/// </summary>
/// <param name="searchData"></param>
public void SubmitSearchAsync(SearchFormModel searchData)
{
    AsyncManager.OutstandingOperations.Increment();
    System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        ActionResult result = SubmitSearch(searchData);
        AsyncManager.Parameters["result"] = result;
        AsyncManager.OutstandingOperations.Decrement();
    });

    return;
}

/// <summary>
/// Called when the asynchronous search has completed
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
public ActionResult SubmitSearchCompleted(ActionResult result)
{
    //Just return the action result
    return result;
}

当然这不起作用,因为在整个代码中,我们都在引用 HttpContext.Current,我们知道在这种方法中最终会是 null

所以我们当时希望通过SubmitSearchAsync 做到这一点:

/// <summary>
/// Asynchronous Search entry point
/// </summary>
/// <param name="searchData"></param>
public void SubmitSearchAsync(SearchFormModel searchData)
{
    AsyncManager.OutstandingOperations.Increment();
    System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        ActionResult result = null;
        AsyncManager.Sync(() =>
        {
            result = SubmitSearch(searchData);
        });

        AsyncManager.Parameters["result"] = result;
        AsyncManager.OutstandingOperations.Decrement();
    });

    return;
}

这解决了问题。

所以这是我的担忧:
AsyncManager.Sync 方法中包装SubmitSearch 的执行是否会破坏使用此模型的目的?换句话说,当我们在 AsyncManager.Sync 方法中时,我们是否回到了 IIS 线程,这让我们回到了第一阶段?

谢谢

【问题讨论】:

    标签: asp.net-mvc multithreading asynccontroller


    【解决方案1】:

    AsyncManager.Sync 方法中包装SubmitSearch 的执行是否违背了使用此模型的目的?换句话说,当我们在 AsyncManager.Sync 方法中时,我们是否回到了 IIS 线程,这让我们回到了第一阶段?

    或多或少,是的。但不幸的是,在您的情况下,使用 Task.Factory.StartNew also 违背了使用异步控制器的目的。使用您尝试使用的方法,您无法取胜。

    IIS 线程、ThreadPool.QueueUserWorkItem 启动的线程和Task 线程都取自同一个线程池。

    为了从异步控制器中获得任何好处,您需要真正的异步方法。换句话说,像Stream.ReadAsyncWebRequest.GetResponseAsync 这样的方法。这些特殊命名的方法使用 I/O 完成端口而不是普通线程,后者使用硬件中断并在不同的线程池上操作。

    我很久以前在这里的回答中写过这个:Using ThreadPool.QueueUserWorkItem in ASP.NET in a high traffic scenario。任务和等待者非常好,但它们不会改变 .NET 线程池的基本动态。

    需要注意的是,有一个选项TaskCreationOptions.LongRunning,您可以在创建Task 时指定它,它实质上是通知框架任务将进行大量等待,理论上是TPL将尝试避免在线程池中调度它。实际上,这在高流量网站上可能不太实用,因为:

    1. 框架实际上并没有保证它不会使用线程池。这是一个实现细节,选项只是您提供的提示

    2. 即使它确实避开了池,它仍然需要使用一个线程,这本质上就像使用new Thread - 如果不是字面上那么至少是有效的。这意味着繁重的上下文切换,这绝对会降低性能,这也是线程池存在的主要原因。

    “搜索”命令显然意味着某种 I/O,这意味着可能您可以在某处使用真正的异步方法,即使它是老式的 BeginXyz/@987654334 @。这里没有捷径,没有快速修复;您必须重新设计您的代码以实现真正的异步。

    .NET 框架无法检查您的 Task 内部发生的情况,并神奇地将其转换为中断。它根本无法使用 I/O 完成端口,除非您直接引用知道它们的特定方法。

    您要处理的下一个 Web 或中间件应用程序,请尝试提前考虑这一点,并避免像瘟疫一样的同步 I/O。

    【讨论】:

    • 感谢背景和信息。基本证实了我的猜想。看来我们现在只能忍气吞声了。
    【解决方案2】:

    我认为@Aaronaught 给出了迄今为止最好的答案:您需要true 异步处理以进行扩展(即Begin/End,而不仅仅是使用线程池线程),并且异步代码没有捷径或快速修复 - 至少需要重新架构那部分。

    这让我想到了你问题的这一部分:

    我们不想在这个项目上花费太多时间重写大段。

    最划算的可能是购买更多内存并将其保存在服务器中。 (您应该首先使用分析器检查以确保它内存问题 - 内存通常是 ASP.NET 的限制因素,但最好先检查)。

    尽管我们开发人员喜欢解决问题,但事实上我们可能会耗费大量时间,例如,将同步代码更改为异步代码。仅供参考,.NET 4.5 中基于任务的异步模式 (async/await) 将允许您更轻松地将同步代码更改为异步非常

    所以现在我建议购买几个 RAM 芯片并记下在您更改为 .NET 4.5 后(更容易)升级到 async

    【讨论】:

    • Awaiters 确实使以同步风格编写异步代码变得非常容易,但这并不一定可以轻松转换整个应用程序。问题是您不能只使应用程序的一小部分异步;它基本上必须是端到端异步的:异步控制器与异步服务交谈与异步存储库交谈与异步文件 I/O 或数据库驱动程序等。这绝对比以前更容易,但我没有高希望有一个明显严重依赖HttpContext.Current的应用程序。
    • 将同步转换为 TAP 并不是简单,但它比将同步转换为任何其他异步模式要容易得多。
    • (顺便说一下,当 搜索 很慢时,这通常意味着 I/O 而不是内存问题;如果搜索基于某种数据库,则扩大到更多的服务器或索引可能是唯一的基础架构解决方案。)但是很好的答案仍然是,许多开发人员忽略了基础架构解决方案,它可能比数百个开发小时便宜得多。
    【解决方案3】:

    我会首先查看服务器本身的性能,然后考虑使用 Visual Studio 中的分析工具来准确确定瓶颈所在和位置。考虑查看迷你分析器的讨论,可以在这里找到http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx。大体同意楼上关于线程消耗的评论。

    【讨论】:

      【解决方案4】:

      有许多原因会导致服务器变慢。如果只讲线程,每个线程最少消耗 1/4 内存,也就是说从线程池中抽取的线程越多,消耗的内存就越多。这可能是导致服务器速度变慢的问题之一。

      如果服务器的响应时间超过 10 秒,请考虑使用异步。就像在您的代码中一样,使 SubmitSearchAsync 函数异步,它将避免阻塞线程,并将线程释放回线程池。但是,就像您提供的代码一样,当从 SubmitSearchAsync 操作收到请求时,会从线程池中抽取一个线程来执行其主体。

      SubmitSearch 是一个同步动作,它一直等到执行完成,然后阻塞线程直到执行完成。 换句话说,你释放了一个线程,但你也阻塞了另一个线程。如果您需要从异步线程同步代码,请使用 AsyncManager.Sync 方法。但在您的情况下, AsyncManager.Sync 可能没有多大帮助。我建议两种可能的解决方案:

      1) 手动生成线程:

      public virtual ActionResult SubmitSearch(SearchFormModel searchData){
          new Thread(() => { 
              //your search code
          }).Start();
      }
      

      在这种情况下,您的搜索代码可能需要更长的时间,但搜索的执行将在不属于池的线程上完成。

      2) 使用 Parallelism 异步更改 SubmitSearch 函数:

      protected virtual async Task<ActionResult> SubmitSearch(SearchFormModel searchData){
         // Make your search code using Parallel task like below.
        var task1 = DoingTask1Async(searchData);
        var task2 = DoingTask2Async(searchData)  
        await Task.WhenAll(task1,task2);
      }
      

      除了上述建议,考虑使用取消令牌,它会进一步减少线程使用。

      希望对你有帮助。

      【讨论】:

        【解决方案5】:

        我们看到的是,随着越来越多的人点击 SubmitSearch,网站的速度总体上会变慢。这很可能是由于所有 IIS 线程在执行搜索时都被锁定。

        如果是线程被锁定,那么它不会变慢,但可能会返回 http 错误。请问有多少并行命中会导致减速? .Net4 中的线程池相当大。此外,如果您的搜索需要 10 秒,这意味着您的数据库正在执行繁重的工作。我想看看 DB 性能:如果您网站的其他部分也依赖于 DB,那么几个并行的 DB 密集型搜索会降低您的应用程序的速度。

        如果由于某种原因您不能/不想执行数据库,那么这里有一个简单的测试:将数据库搜索调用更改为休眠调用 X 秒(在这种情况下约为 10)。然后运行您的并行请求并查看站点响应性是否下降。您的请求线程号是相同的,所以如果这是原因,那么它应该具有相同的效果。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-04-11
          • 2011-04-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-04-22
          • 1970-01-01
          相关资源
          最近更新 更多