【问题标题】:Azure Web Role Stress Test - 1000ms blocking operation in AsyncControllerAzure Web 角色压力测试 - AsyncController 中的 1000 毫秒阻塞操作
【发布时间】:2012-09-23 04:44:13
【问题描述】:

今天,我想在 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 个左右请求的明显限制阻塞时间?我在这里犯了什么明显的错误吗?

【问题讨论】:

  • 你是说阻塞过程的持续时间与当前负载无关?因为如果不是,那很可能会比线程数更早成为您的瓶颈。
  • 阻塞进程平均为 5 到 30 秒。有些会超过负载均衡器超时值(似乎是 4 分钟)。这将会非常好玩。看起来我需要几百台服务器才能获得任何类型的吞吐量。
  • 真正的操作是否真的需要在它运行的全部时间内消耗一个线程?通常,您会尝试“一直向下”异步,这样您就不会在操作期间使用/浪费线程。如果操作在运行时不需要消耗 webapp 的线程,则使用 Task.Delay 将是更好的选择
  • 在大多数情况下,实际操作将在 ServiceBus 命名空间中的 TopicClient 或 SubscriptionClient 上使用 BeginReceive()。那应该释放线程。我主要对这些具有阻塞负载的 Azure Web 角色实例的容量感兴趣。当我们有这种负载时,看起来我们可能需要分散到一个“阵列”的网络角色来让更多的核心参与到行动中。在这些情况下,这只是谁被阻止的问题,因为有人会承担这种负载。我们更希望前端 API 有大量的可用容量,而后端的蜜蜂可以等待。

标签: asp.net-mvc iis-7 azure task-parallel-library azure-web-roles


【解决方案1】:

很抱歉,我不得不说这个购买你被所有声称只需使用Task.Factory.StartNew 就能解决你所有问题的博客误导,好吧,事实并非如此。

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

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

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

使用 TaskCreationOptions.LongRunning 进行负载测试

您知道您可以指定任务是否长时间运行吗?看看这个问题:Strange Behavior When I Don't Use 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 选项在这种情况下效果很好,但如果你真的需要它,你 should always evaluate

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

【讨论】:

  • 这让我笑了!我正在考虑尝试 LongRunning 选项,但各种博客对该解决方案嗤之以鼻,作为某种边缘案例的最后手段。我的错误:我也应该对此进行测试,而不是掩饰它。非常感谢!我会试试这个。不知道这样会不会绕过线程池的正常线程注入速度障碍?什么时候系统会有这么多线程,以至于上下文切换变得过于昂贵?
  • 另外,是的,Azure 负载均衡器的这个限制并不酷。我想没有人可以实施网络通话时间超过 4 分钟的 B2B 解决方案?我知道我可以返回客户端可以定期尝试检索的响应的 URL,或者我可以在他们的平台上调用回调 API 来发出响应就绪的信号,但这些是旧客户端,它们可以调用 GET/PUT /发布/删除。其余部分超出范围。这4分钟的事情不好。你知道有什么方法可以让连接保持更长时间吗?
  • 这可以说是天翻地覆。我们在 6 个实例中的基线为 550/秒。有了这个,它在出现问题之前达到了 1400/秒。我将阻塞负载持续时间减少到 500 毫秒,并且在问题出现之前它能够容纳高达 2100/秒。这要好得多。一旦我们在控制器中进行异步 I/O,我希望这会更高。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-17
  • 1970-01-01
  • 2011-06-07
  • 1970-01-01
  • 2015-10-29
  • 1970-01-01
  • 2015-01-16
相关资源
最近更新 更多