【问题标题】:Requests are queuing in Azure AppService though it has enough threads in threadpool请求在 Azure AppService 中排队,尽管它在线程池中有足够的线程
【发布时间】:2017-09-13 07:06:32
【问题描述】:

我使用 asp.net webapi 编写了一个 api,并将其部署在 azure 中作为 Appservice。我的控制器的名称是 TestController,我的操作方法如下所示。

    [Route("Test/Get")]
    public string Get()
    {
        Thread.Sleep(10000);
        return "value";
    }

因此对于每个请求,它应该等待 10 秒才能返回字符串“值”。我还编写了另一个端点来查看线程池中用于执行请求的线程数。该操作类似于以下内容。

    [Route("Test/ThreadInfo")]
    public ThreadPoolInfo Get()
    {
        int availableWorker, availableIO;
        int maxWorker, maxIO;

        ThreadPool.GetAvailableThreads(out availableWorker, out availableIO);
        ThreadPool.GetMaxThreads(out maxWorker, out maxIO);

        return new ThreadPoolInfo
        {
            AvailableWorkerThreads = availableWorker,
            MaxWorkerThreads = maxWorker,
            OccupiedThreads = maxWorker - availableWorker
        };
    }

现在,当我们同时对 Test/Get 端点进行 29 次获取调用时,几乎需要 11 秒才能使所有请求成功。因此服务器在 11 个线程中同时执行所有请求。要查看线程状态,在调用 Test/Get 后立即调用 Test/ThreadInfo 会立即返回(无需等待) { “AvailableWorkerThreads”:8161, “MaxWorkerThreads”:8191, “占用线程”:30 }

似乎有 29 个线程正在执行 Test/Get 请求,1 个线程正在执行 Test/ThreadInfo 请求。

当我对 Test/Get 进行 60 次调用时,几乎需要 36 秒才能获得成功。调用 Test/ThreadInfo(需要一些时间)返回 { “AvailableWorkerThreads”:8161, “MaxWorkerThreads”:8191, “占用线程”:30 }

如果我们增加请求数,OccupiedThreads 的值会增加。就像 1000 个请求一样,它需要 2 分 22 秒,OccupiedThreads 的值为 129。

虽然 WorkerThread 中有很多线程可用,但似乎请求并在 30 个并发调用后排队。它逐渐增加了并发执行的线程,但这还不够(129 个请求用于 1000 个请求)。

由于我们的服务有很多 IO 调用(其中一些是外部 api 调用,一些是数据库查询),因此延迟也很高。由于我们使用所有 IO 调用异步方式,因此服务器可以同时处理大量请求,但是当处理器进行实际工作时我们需要更多的并发性。我们在一个实例中使用 S2 服务计划。增加实例会增加并发,但我们需要单个实例的更多并发。

在阅读了一些关于 IIS 的博客和文档后,我们看到有一个设置 minFreeThreads。如果线程池中的可用线程数低于此设置的值,则 IIS 开始排队请求。 appservice中有这样的东西吗?是否真的可以从 azure 应用服务获得更多并发,或者我们在那里缺少一些配置?

【问题讨论】:

  • AFAIK,线程池会将创建的线程数限制为每 500 毫秒一个。您可以扩大您的价格层以获得更高的 CPU、内存等。另外,您可以尝试关注Optimizing IIS Performance 以改进并发请求。
  • 因为我们已经在 Azure 中部署了应用程序,所以看不到任何进行 IIS 级别优化的选项。在 S1 中,我在并发方面得到了相同的行为,因此扩大价格层可能没有帮助。

标签: c# azure iis asp.net-web-api2 threadpool


【解决方案1】:

终于得到了我的问题的答案。问题是 ASP.NET 线程池维护了一个线程池,这些线程已经产生了线程初始化成本并且易于重用。 .NET 线程池也是自我调整的。它监视 CPU 和其他资源利用率,并根据需要添加新线程或修剪线程池大小。当有很多请求并且池中没有足够的线程可用时,线程池开始在池中添加新线程,在此之前它运行自己的算法来查看系统的内存和cpu使用状态,这需要很长时间时间,这就是为什么我们看到池中工作线程缓慢增加并获得大量请求排队的原因。但幸运的是,有一个选项可以在线程池切换到添加新线程的算法之前设置工作线程的数量。代码如下所示。

    public string Settings()
    {
        int minWorker, minIOC;
        ThreadPool.GetMinThreads(out minWorker, out minIOC);

        if (ThreadPool.SetMinThreads(300, minIOC)){ 
            return "The minimum number of threads was set successfully.";
        }
        else
        {
            return "The minimum number of threads was not changed.";
        }

    }

这里 ThreadPool.SetMinThreads(300, minIOC) 是设置线程池在切换到添加或删除线程的算法之前将创建的最小线程的值。我已将此方法添加为我的 webapi 控制器的一个操作,然后在运行此操作后,当我向 Test/Get 端点发出 300 个并发请求时发出请求,所有这些都在 11 秒内运行并完成,并且没有请求排队。

【讨论】:

    【解决方案2】:

    根据我的理解,您需要通过ThreadPool.GetMinThreads 检查MinWorkerThreads,它可以检索 ThreadPool 在预期新请求时保持的空闲线程数。我建议您在执行操作 (Tetst/Get) 后返回当前线程池信息,而不是向 Test/ThreadInfo 发出新请求。根据您的代码,我在我的 Web 应用上使用 B1 定价层对其进行了测试,如下所示:

    1000 个并发请求,每个动作休眠 15 秒,总耗时 3 分 20 秒。

    线程池创建和销毁工作线程以优化吞吐量,如果当前线程可以处理请求,则不会创建更多线程。一旦这些线程完成了它们的活动,它们就会被返回到线程池中。

    然后,我使用Asynchronous programming,并将动作更改如下:

    public async Task<ActionResult> DoJob(int id)
    {
        await Task.Delay(TimeSpan.FromSeconds(15));
        return ThreadInfo();
    }
    

    结果:

    近 3000 个并发请求:

    一般来说,AvailableWorkerThreads只是表示可以启动的额外工作线程的数量,而不是为你创建的工作线程。我建议您使用异步编程并将您的实际工作部署在 azure web 应用程序上,然后检查实际性能并找到相关方法是否存在瓶颈。

    【讨论】:

    • "AvailableWorkerThreads 仅表示可以启动的额外工作线程数,而不是为您创建的工作线程。"是的,确实可以创建更多的工作线程,实际上服务器会这样做,因为对于大量的请求,它会逐渐增加它,但这非常慢(每秒 2 个)。我们在代码的所有部分都使用异步。我选择了那个例子来看看当 cpu 绑定工作同时发生时会发生什么。这是一个常见的场景,比如 1000 个 I/O 请求成功并恢复执行。
    • await Task.Delay(TimeSpan.FromSeconds(15)); Task.Delay 不会阻塞线程,而是立即释放线程并threadpool 获取该线程以服务来自队列的另一个请求。这就是为什么你会得到更好的结果。所以这并没有给出当会有很多请求与 cpu 绑定的工作发生时的情况。
    • 异步代码一次增加可能的请求执行次数,但这些请求是 IO 绑定的。但是对于 cpu 限制,我们需要更多可用的工作线程。
    • 我会使用任务并行库在我的操作中执行异步任务以处理 CPU 密集型任务。
    • 所以你会像return Task.Run(() =&gt; { return RunCpuBoundOperationAndReturnData(); }); 这样编写你的代码,这会将线程释放到池中,但运行 cpu 绑定的工作将从线程池中获取一个线程。所以不会有任何好处,但会有线程切换开销。所以尽量避免这种情况。不要将 TPL 用于 CPU 绑定工作。
    猜你喜欢
    • 2012-04-09
    • 1970-01-01
    • 2021-06-03
    • 2011-08-19
    • 2013-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多