1

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

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

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

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

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

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

假设这是原始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;
}

这解决了这个问题。

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

谢谢

4

5 回答 5

6

SubmitSearch在方法中包装执行是否会破坏AsyncManager.Sync使用此模型的目的?换句话说,当我们在AsyncManager.Sync方法中时,我们是否回到了 IIS 线程,这让我们回到了原点?

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

IIS 线程、由 启动的线程ThreadPool.QueueUserWorkItemTask线程都取自同一个线程池。

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

我很久以前在我的回答中写过这个:在高流量场景中使用 ASP.NET 中的 ThreadPool.QueueUserWorkItem。任务和等待者非常好,但它们不会改变 .NET 线程池的基本动态。

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

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

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

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

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

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

于 2013-11-09T03:33:16.100 回答
2

我认为@Aaronaught 迄今为止给出了最好的答案:您需要真正的异步处理才能扩展(即Begin/ End,而不仅仅是使用线程池线程),并且异步代码没有快捷方式或快速修复 - 这需要至少重新设计该部分。

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

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

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

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

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

于 2013-11-09T04:49:13.903 回答
0

有许多原因会导致服务器变慢。如果只讲线程,每个线程最少消耗 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);
}

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

希望能帮助到你。

于 2013-11-09T02:39:57.190 回答
0

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

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

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

于 2013-11-09T11:31:39.757 回答
0

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

于 2013-11-07T05:51:22.917 回答