2

今天,我想在 MVC3 Web 角色的 AsyncController 中模拟等待一个长时间运行的阻塞进程(5 到 30 秒)。然而,首先,我只是从 1 秒开始,让事情顺利进行。是的,这样做的智慧值得怀疑,因为阻塞操作目前不能在 I/O 完成端口上异步运行到外部服务,但我想看看这种特殊情况下的性能限制是多少。

在我的网络角色中,我部署了 6 个小型实例。唯一的控制器是 AsyncController,有两个简单的方法来模拟 1000 毫秒的阻塞操作。

MVC3 Web 角色控制器就是这样的:

public class MessageController : AsyncController
{
    public void ProcessMessageAsync(string id)
    {
        AsyncManager.OutstandingOperations.Increment();
        Task.Factory.StartNew(() => DoSlowWork());
    }

    public ActionResult ProcessMessageCompleted()
    {
        return View("Message");
    }

    private void DoSlowWork()
    {
        Thread.Sleep(1000);
        AsyncManager.OutstandingOperations.Decrement();
    }
}

接下来,我从 Amazon EC2 向 Web 角色施加压力。使用 12 台服务器,我缓慢增加负载,接近 550 个请求/秒。任何超出此范围的尝试都会遇到明显的线程饥饿和后续错误。我假设我们达到了 CLR 线程限制,我理解为每个 CPU 100 个线程。计算 AsyncController 的一些开销,对于 1000 毫秒的阻塞操作,每台服务器平均每秒 550/6 = 92 个请求似乎符合这个结论。

这是真的吗?我见过其他人说类似的话,他们在这种类型的负载下每个实例每秒达到 60 到 80 个请求。该系统上的负载将主要由运行时间较长的操作组成,因此当 5000 毫秒的任务上线时,每秒 1000 毫秒的 92 个请求将大大减少。

如果没有通过多个单独的 Web 角色前端路由阻塞 I/O 的请求以将此负载分散到更多内核,有没有办法在 1000 毫秒阻塞时间内获得高于每秒 90 个左右请求的明显限制?我在这里犯了某种明显的错误吗?

4

1 回答 1

4

很抱歉,我不得不说这个购买你被所有博客误导,声称通过简单地使用Task.Factory.StartNew是解决你所有问题的方法,好吧,事实并非如此。

使用 Task.Factory.StartNew 进行负载测试

看看我对您的代码进行的以下负载测试(我将睡眠时间更改为 10 秒而不是 1 秒,以使其更糟)。该测试模拟了 200 个固定用户执行总共 2500 个请求。看看有多少由于线程饥饿而失败的请求:

在此处输入图像描述

如您所见,即使您将 AsyncController 与 Task 一起使用,线程饥饿仍然会发生。可能是因为长时间运行的过程造成的吗?

使用 TaskCreationOptions.LongRunning 进行负载测试

您知道您可以指定任务是否长时间运行吗?看看这个问题:我不使用 TaskCreationOptions.LongRunning 时的奇怪行为

当您不使用 LongRunning 标志时,任务被安排在线程池线程上,而不是它自己的(专用)线程上。这可能是您的行为改变的原因 -当您在没有设置 LongRunning 标志的情况下运行时,您可能会由于进程中的其他线程而导致线程池不足。

让我们看看如果我们更改 1 行代码会发生什么:

    public void ProcessMessageAsync(string id)
    {
        Task.Factory.StartNew(DoSlowWork, TaskCreationOptions.LongRunning);
        AsyncManager.OutstandingOperations.Increment();
    }

看看负载测试,有什么不同!

在此处输入图像描述

刚刚发生了什么?

如您所见,LongRunning 选项似乎有很大的不同。让我们添加一些日志来看看内部发生了什么:

    public void ProcessMessageAsync(string id)
    {
        Trace.WriteLine(String.Format("Before async call - ThreadID: {0} | IsBackground: {1} | IsThreadPoolThread: {2} | Priority: {3} | ThreadState: {4}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground,
            Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.Priority, Thread.CurrentThread.ThreadState));
        Task.Factory.StartNew(DoSlowWork, TaskCreationOptions.LongRunning);
        AsyncManager.OutstandingOperations.Increment();
    }

    ...

    private void DoSlowWork()
    {
        Trace.WriteLine(String.Format("In async call - ThreadID: {0} | IsBackground: {1} | IsThreadPoolThread: {2} | Priority: {3} | ThreadState: {4}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground,
               Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.Priority, Thread.CurrentThread.ThreadState)); 
        Thread.Sleep(10000);
        AsyncManager.OutstandingOperations.Decrement();
    }

没有 LongRunning:

Before async call - ThreadID: 11 | IsBackground: True | IsThreadPoolThread: True | Priority: Normal | ThreadState: Background
Async call - ThreadID: 11 | IsBackground: True | IsThreadPoolThread: True | Priority: Normal | ThreadState: Background

使用 LongRunning:

Before async call - ThreadID: 48 | IsBackground: True | IsThreadPoolThread: True | Priority: Normal | ThreadState: Background
Async call - ThreadID: 48 | IsBackground: True | IsThreadPoolThread: False | Priority: Normal | ThreadState: Background

如您所见,如果没有 LongRunning,您实际上是在使用线程池中的线程,从而导致饥饿。虽然 LongRunning 选项在这种情况下效果很好,但您应该始终评估您是否真的需要它。

注意:由于您使用的是 Windows Azure,因此您需要考虑到负载均衡器会在几分钟不活动后超时。

于 2012-09-23T08:17:06.297 回答