使用HttpClient的并发请求比预期的要长

时间:2017-08-16 15:22:04

标签: c# dotnet-httpclient

我有一个webservice,可以同时接收多个请求。对于每个请求,我需要调用另一个Web服务(身份验证)。问题是,如果多个(> 20)请求同时发生,响应时间突然变得更糟。

我做了一个样本来证明问题:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace CallTest
{
    public class Program
    {
        private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false });

        static void Main(string[] args)
        {
            ServicePointManager.DefaultConnectionLimit = 100;
            ServicePointManager.Expect100Continue = false;

            // warmup
            CallSomeWebsite().GetAwaiter().GetResult();
            CallSomeWebsite().GetAwaiter().GetResult();

            RunSequentiell().GetAwaiter().GetResult();

            RunParallel().GetAwaiter().GetResult();
        }

        private static async Task RunParallel()
        {
            var tasks = new List<Task>();
            for (var i = 0; i < 300; i++)
            {
                tasks.Add(CallSomeWebsite());
            }
            await Task.WhenAll(tasks);
        }

        private static async Task RunSequentiell()
        {
            var tasks = new List<Task>();
            for (var i = 0; i < 300; i++)
            {
                await CallSomeWebsite();
            }
        }

        private static async Task CallSomeWebsite()
        {
            var watch = Stopwatch.StartNew();
            using (var result = await _httpClient.GetAsync("http://example.com").ConfigureAwait(false))
            {
                // more work here, like checking success etc.
                Console.WriteLine(watch.ElapsedMilliseconds);
            }
        }
    }
}

顺序通话没问题。它们需要几毫秒才能完成,响应时间大致相同。

但是,并行请求开始越来越长,发送的请求越多。有时甚至需要几秒钟。我在.NET Framework 4.6.1和.NET Core 2.0上测试了它,结果相同。

甚至更奇怪:我使用WireShark跟踪HTTP请求,并且它们总是在同一时间内进行。 但示例程序报告的并行请求值远高于WireShark。

如何为并行请求获得相同的性能?这是一个线程池问题吗?

4 个答案:

答案 0 :(得分:1)

在问题的RunParallel()功能中,在程序运行的第一秒中为所有300个呼叫启动秒表,并在每个http请求完成时结束。

因此,这些时间无法与连续迭代进行比较。

对于较少数量的并行任务,例如50,如果你测量顺序和并行方法所花费的时间,你应该发现并行方法更快,因为它可以尽可能多地GetAsync任务。

也就是说,当运行300次迭代的代码时,我确实在调试器外部运行时发现了可重复的几秒钟停顿:

  

在调试器中调试构建:顺序27.6秒,并行0.6秒

     

调试版本,无需调试器:顺序26.8秒,并行3.2秒

[编辑]

有一个类似的情景描述in this question,它可能与你的问题无关。

这个问题越多,任务运行越多,并在以下情况下消失:

  • 交换GetAsync工作等效延迟
  • 针对本地服务器运行
  • 减慢创建/运行任务的速度减少并发任务

watch.ElapsedMilliseconds诊断停止所有连接,表明所有连接都受到限制的影响。

在主机或网络中似乎是某种(反syn-flood?)限制,一旦一定数量的套接字开始连接就会暂停数据包流。

答案 1 :(得分:1)

听起来无论出于什么原因,你都会在大约20个并发任务中达到收益递减点。所以,你最好的选择可能是扼杀你的并行性。 TPL Dataflow是实现这一目标的绝佳库。要按照您的模式,添加如下方法:

private static Task RunParallelThrottled()
{
    var throtter = new ActionBlock<int>(i => CallSomeWebsite(),
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20 });

    for (var i = 0; i < 300; i++)
    {
        throttler.Post(i);
    }
    throttler.Complete();
    return throttler.Completion;
}

您可能需要尝试MaxDegreeOfParallelism,直到找到最佳位置。请注意,这比批量20更有效。在该方案中,批次中的所有20个都需要在下一批次开始之前完成。使用TPL Dataflow,一旦完成,就允许另一个开始。

答案 2 :(得分:1)

.NET Core 2.1已修复此问题。我认为问题是HttpClient使用的基础Windows WinHTTP处理程序。

在.NET Core 2.1中,他们重写了HttpClientHandler(请参见https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performance-improvements-in-net-core-2-1/#user-content-networking):

  

在.NET Core 2.1中,HttpClientHandler具有一个新的默认实现,完全从头开始在其他System.Net库的C#顶部完全由C#实现。 System.Net.Sockets,System.Net.Security等。它不仅解决了上述行为问题,而且还显着提高了性能(该实现也公开为SocketsHttpHandler公开,可以直接使用,而不必通过HttpClientHandler来使用。以配置特定于SocketsHttpHandler的属性。

事实证明,这消除了问题中提到的瓶颈。

在.NET Core 2.0上,我得到以下数字(以毫秒为单位):

Fetching URL 500 times...
Sequentiell   Total: 4209, Max:  35, Min: 6, Avg:  8.418
Parallel      Total:  822, Max: 338, Min: 7, Avg: 69.126

但是在.NET Core 2.1上,单个并行HTTP请求似乎已经改善了很多:

Fetching URL 500 times...
Sequentiell   Total: 4020, Max:  40, Min: 6, Avg:  8.040
Parallel      Total:  795, Max:  76, Min: 5, Avg:  7.972

答案 3 :(得分:0)

您遇到问题的原因是.NET未按照等待的顺序恢复Tasks,等待Task仅在调用函数无法恢复执行时才会恢复,Task不适用于Parallel执行。

如果您进行了一些修改,以便将i传递给CallSomeWebsite函数并在将所有任务添加到列表后调用Console.WriteLine("All loaded");,您将得到类似的内容:( RequestNumber:Time)

All loaded
0: 164
199: 236
299: 312
12: 813
1: 837
9: 870
15: 888
17: 905
5: 912
10: 952
13: 952
16: 961
18: 976
19: 993
3: 1061
2: 1061

在任何时间打印到屏幕之前,您是否注意到每个Task的创建方式?创建Task的整个循环在等待网络调用之后的任何Task恢复执行之前完成。

另外,请看请求1在请求1之前是如何完成的? .NET将按其认为最佳的顺序恢复Task(这可以保证更复杂,但我不确定.NET如何决定继续Task

我认为您可能会感到困惑的一件事是AsynchronousParallel。它们不相同,Task用于Asynchronous执行。这意味着所有这些任务都在同一个线程上运行(可能。.NET可以根据需要为任务启动新线程),因此它们不会在Parallel中运行。如果它们是真正的Parallel,它们都将在不同的线程中运行,并且每次执行的执行时间都不会增加。

更新的功能:

    private static async Task RunParallel()
    {
        var tasks = new List<Task>();
        for (var i = 0; i < 300; i++)
        {
            tasks.Add(CallSomeWebsite(i));
        }
        Console.WriteLine("All loaded");
        await Task.WhenAll(tasks);
    }

    private static async Task CallSomeWebsite(int i)
    {
        var watch = Stopwatch.StartNew();
        using (var result = await _httpClient.GetAsync("https://www.google.com").ConfigureAwait(false))
        {
            // more work here, like checking success etc.
            Console.WriteLine($"{i}: {watch.ElapsedMilliseconds}");
        }
    }

至于Asynchronous执行Synchronous执行时打印时间较长的原因,您当前的跟踪时间方法没有考虑执行暂停和执行之间花费的时间。延续。这就是为什么所有报告执行时间都超过完成的请求集的原因。如果您想要准确的时间,您需要找到一种方法来减去await发生和执行继续之间所花费的时间。问题不在于需要更长的时间,而是您的报告方法不准确。如果您将所有Synchronous次来电的时间相加,那么它实际上远远超过Asynchronous来电的最长时间:

Sync: 27965
Max Async: 2341