由于 HttpWebRequest,.NET Core 服务在负载下停止

.NET Core service stalling under load due to HttpWebRequest

我在 Windows 服务器上有一个 ASP.NET (.NET Framework 4.8) Web 服务 运行,它使用 HttpWebRequest 发出大量传出 HTTP 请求(同步).它可以毫不费力地处理数千个并发请求。

最近,我使用更新的 HttpWebRequest(同步)将 service/middleware 迁移到 Ubuntu 服务器上的 ASP.NET 核心(运行时 3.1)运行 .

现在这项服务在只有几百个并发请求的负载测试下停滞不前。系统journal/logs 表示健康检查(心跳)在几分钟后无法到达该服务。它开始时很好,但几分钟后它变慢并最终停止(没有响应但它不会崩溃 dotnet),然后在 5-10 分钟后再次开始工作而无需任何干预,并且每隔几次重复相同的行为分钟。

我不确定这是由于端口耗尽还是死锁造成的。如果我通过跳过所有 HttpWebRequest 调用来加载测试服务,那么它工作正常,所以我怀疑它必须与 HttpWebRequest 做一些事情导致在流量压力下出现问题。

查看 .NET Core 代码库,似乎 HttpWebRequest(同步)为每个请求创建一个新的 HttpClient(在我的例子中,由于参数原因,客户端未被缓存),并执行HttpClient synchronously 赞:

public override WebResponse GetResponse()
{
    :
    :
    return SendRequest(async: false).GetAwaiter().GetResult();
    :
    :
}

private async Task<WebResponse> SendRequest(bool async)
{
    :
    :
    _sendRequestTask = async ?
        client.SendAsync(...) :
        Task.FromResult(client.Send(...));

    HttpResponseMessage responseMessage = await _sendRequestTask.ConfigureAwait(false);
    :
    :
}

微软官方suggestion是使用IHttpClientFactorySocketsHttpHandler以获得更好的性能。我可以让我们的服务使用单例 SocketsHttpHandler 和新的 HttpClient 的每个传出请求(使用共享处理程序)来更好地重用和关闭套接字,但我主要关心的是(下面):

该服务基于同步代码,因此我将不得不同步使用异步 HttpClient,可能使用与上面官方 .NET Core 代码相同的 method.GetAwaiter().GetResult() 技术。虽然单例 SocketsHttpHandler 可能有助于避免端口耗尽,但由于像本机 HttpWebRequest?

这样的死锁,并发同步执行是否仍会导致停顿问题

此外,是否有一种方法(.NET Core 的另一个同步 HTTP 客户端,设置 'Connection: close' header 等)可以同步地平滑地发出大量并发 HTTP 请求,而不会出现端口耗尽或死锁,就像之前在 .NET Framework 4.8 中使用 HttpWebRequest 一样顺利?

澄清一下,所有WebRequest相关的objects在代码中都是closed/disposed,ServicePointManager.DefaultConnectionLimit设置为int.MaxValuenginx(dotnet 的代理)已 tunedsysctl 也已调整。

I'm not sure if this is due to port exhaustion or a deadlock.

对我来说听起来更像 thread pool exhaustion

The service is based on synchronous code, so I'll have to use asynchronous HttpClient synchronously

为什么?

线程池耗尽的最佳解决方案是将阻塞代码重写为异步。 ASP.NET pre-Core 中的某些地方需要同步代码(例如,MVC 操作过滤器和子操作),但 ASP.NET Core 是完全异步的,包括中间件管道。

如果出于某种原因绝对不能使代码正确异步,唯一的其他解决方法是在启动时增加线程池中的最小线程数。