.Net5 HttpClient 并发 - 性能

问题描述

使用 IHttpClientFactory 创建了一个 HttpClient 并向 WebApi 并行发送 1000 个 GET 调用,并观察到每个请求大约有 3-5 分钟的延迟.. 完成后再次并行发送 1000 个 GET 请求,这次没有延迟。

现在我将并行请求增加到 2000,对于第一批,每个请求延迟大约为 9-11 分钟。对于第二个 2000 个并行请求,每个请求的延迟约为 5 分钟(在 1000 个请求的情况下没有延迟。)

var client = _clientFactory.CreateClient();
            client.BaseAddress = new Uri("http://localhost:5000");
            client.Timeout = TimeSpan.FromMinutes(20);



            List<Task> _task = new List<Task>();
            for (int i = 1; i <= 4000; i++)
            {
                _task.Add(ExecuteRequest(client,i));
                if (i % 2000 == 0)
                {
                    await Task.WhenAll(_task);
                    _task.Clear();
                }
            }

private async Task ExecuteRequest(HttpClient client,int requestId)
    {

        var result = await client.GetAsync($"Performance/{requestId}");

        var response = await result.Content.ReadAsstringAsync();

        var data = JsonConvert.DeserializeObject<Response>(response);

    }

试图理解,

  • HttpClient 无延迟地支持多少并行请求。
  • 如何针对 2000 个或更多并行请求提高 HttpClient 的性能..

解决方法

HttpClient 无延迟支持多少个并行请求。

在现代 .NET Core 平台上,您仅受可用内存的限制。默认情况下没有内置的限制。

如何针对 2000 个或更多并行请求提高 HttpClient 的性能。

听起来您的服务器正在限制您。如果您想测试可扩展性更强的服务器,请尝试在您的服务器启动时运行:

var desiredThreads = 2000;
ThreadPool.GetMaxThreads(out _,out var maxIoThreads);
ThreadPool.SetMaxThreads(desiredThreads,maxIoThreads);
ThreadPool.GetMinThreads(out _,out var minIoThreads);
ThreadPool.SetMinThreads(desiredThreads,minIoThreads);
,

您正在做的是导致“冷”(刚刚更新或空连接池)HttpClient 的最坏情况性能。

当您发出新请求时,它会在连接池中查找打开的连接。当它没有找到时,它会尝试打开一个新连接。通过向冷客户端突然爆发,大多数对 SendAsync 的调用最终都会尝试打开新连接。

这是一个问题,因为需要新连接的请求将需要多次往返服务器,而现有连接上的请求只需要一次往返。如果您使用 HTTPS,情况会更糟。在这种情况下,您严重依赖网络延迟。

如果您只是进行基准测试,那么您需要对稳态性能进行基准测试,而不是热身性能。 Benchmark.NET 或多或少应该为您做这件事。

当您的请求完成得相当快时,将初始并发限制为总请求的较小百分比,然后从那里慢慢增加连接池大小可能会快得多。这允许后续请求重新使用连接。您可能会尝试像下面这样,它只会允许(粗略的行为,而不是保证)一次打开 10 个新连接:

var sem = new SemaphoreSlim(10);
var client = new HttpClient();

async Task<HttpResponseMessage> MakeRequestAsync(HttpRequestMessage req)
{
   Task t = sem.WaitAsync();
   bool openNew = t.IsCompleted;
   await t;

   try
   {
      return await client.SendAsync(req);
   }
   finally
   {
      sem.Release(openNew ? 2 : 1);
   }
}