从 HttpClient SendAsync 请求获取响应时出现无法解释的超时和延迟

Posted

技术标签:

【中文标题】从 HttpClient SendAsync 请求获取响应时出现无法解释的超时和延迟【英文标题】:Unexplained timeouts and delays on getting the response from HttpClient SendAsync requests 【发布时间】:2021-07-05 12:31:47 【问题描述】:

我们有一个混合使用异步和同步代码的 .NET 4.7.2(我理解这是一个禁忌)。我们在 Windows 服务上使用 NancyFX。该服务获取休息调用并进行休息调用。线程池看起来很健康(整个进程只使用 70 个线程)。出于某种原因,某些 http 响应会延迟 10 秒,有时会延迟 100 秒,从而导致任务取消。

这是代码的结构

public async Task<Guid> SomeFunction()

   ...
   var response = await _httpClient.SendAsync(request, cancellationToken);
   ...

SomeFunction().Result

首先,我确信由于某种原因,响应在网络上的某个地方被延迟了。但是我们已经排除了多种方法,最重要的是通过 perfview 查看 ETW 跟踪并看到数据包几乎立即返回(使用 Microsoft-Windows-NDIS-PacketCapture/PacketFragment)

其次,我确信这与异步方法上的 .Result 代码引起的线程池问题有关。但是,进程上的线程再次保持稳定在 70 个线程。通过 perfview 我可以看到饥饿并没有发生(使用 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThreadAdjustment/Adjustment)

我也想过我可能会遇到等待/异步和 .Result 的死锁情况,但死锁意味着请求永远不会完成,而不是它会延迟 10 秒。

我还仔细检查了我们只使用了一个 httpclient 实例,而且确实如此。

还能是什么?

此时我们正在删除 .Result 并将其替换为适当的 async/await。但我没有证据表明这会解决问题,因为我没有看到任何死锁或线程耗尽的证据。

这是性能分析

我们也在考虑我们正在以某种方式耗尽 http 连接的建议。我认为不是这种情况的一个原因是,根据 perfview,请求被发送出去并且数据包返回,但响应并没有构成 c# 堆栈。但是,这些性能计数器可能表明正在排队。

更新 我们已经用这个增加了http连接,好像已经生效了。

<connectionManagement>
  <add address="*" maxconnection="1024"/>
</connectionManagement>

上面显示的排队完全没有了。但是这些请求没有完成的问题仍然存在

【问题讨论】:

您正在创建多少个 HttpClient 实例?也许您的问题与打开 API 的最大连接数有关。看这里:aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong 只使用一个实例——并且没有得到任何套接字异常来表明我们正在耗尽套接字 您能否尝试在应用程序的开头添加此语句:ThreadPool.SetMinThreads(200, 200);,看看是否有什么不同?不建议将此作为修复方法,而是作为解决问题的一种方法。 @TheodorZoulias 我刚试过,这似乎可能减少了问题,但没有消除它。我需要等到明天早上才能将苹果与苹果进行比较,但问题仍然存在。如果这能解决问题,我会感到惊讶,因为在此更改之前,根据任务管理器的线程数徘徊在 71(有足够的增长空间)。现在线程悬停在 140,这也很奇怪,因为我要求它从 200 开始。 仅供参考 SetMinThreads API 没有立即创建指定数量的线程的效果。它只是确保通过创建一个新线程立即满足所有对后台工作的请求,直到达到此阈值。之后,ThreadPool 切换到保守算法,等待 500 毫秒让当前正在运行的作业完成,然后将新线程注入池中。 【参考方案1】:

还能是什么?

你已经检查过我的第一个猜测,那就是线程池饥饿。

还有另一种可能性,具体取决于 API 调用的完成方式。如果对同一主机有许多同时请求,则 .NET 网络堆栈可能会限制您。非 ASP.NET 应用程序对同一主机的 2 个同时请求默认限制。在这种情况下,您有一个服务器应用程序,但没有一个 ASP.NET 应用程序,因此默认情况下您会启用相当严格的限制。

建议:把这段代码放在你的启动中:

ServicePointManager.DefaultConnectionLimit = int.MaxValue;

请注意,默认情况下 .NET Core 不会限制客户端 HTTP 请求,因此这只是模拟现代 .NET 平台上的默认行为。

【讨论】:

我会试试这个。你知道在尝试之前是否有办法证明这一点?有任何线索表明这实际上是正在发生的事情吗?我可以转储 ServicePointManager.DefaultConnectionLimit 如果它是 2 或其他什么,那么我知道吗? @Mark:我不知道是否有简单的方法可以证明这一点。某处可能有诊断日志,但这是旧的 .NET Framework 代码,所以谁知道呢。 我试过这个 这似乎减少了我在某些性能计数器中看到的排队(请参阅更新关于主要问题)但没有消除问题。正在启动通信并且始终发送请求(远程服务器接收并响应),但进程无法正确接收。 通常,处理响应的延迟是线程池耗尽的指标。【参考方案2】:

感谢所有提供帮助的人。最后,没有什么是确凿的证据。我无法证明线程饥饿或 http 连接饥饿。我们最终清理了完成的 async/await 代码以删除 .Result 之类的内容并添加了一些缓存,问题就消失了。

我的最终猜测是,当您混合并匹配 async/await 和 .Result 并且发出大量请求时,在 .net 4.7.2 中会出现一些边缘情况阻塞。

【讨论】:

以上是关于从 HttpClient SendAsync 请求获取响应时出现无法解释的超时和延迟的主要内容,如果未能解决你的问题,请参考以下文章

C# HttpClient.SendAsync 在测试某些 URL 时抛出“发送请求时发生错误”异常

.NET 3.1 中来自 HTTPClient 的 SendAsync 中的对象循环 Json

HttpClient.SendAsync 不发送请求正文

HttpClient SendAsync 阻塞主线程

在 HttpClient.SendAsync() 之后无法访问已释放的对象

HttpClient.SendAsync() 未将 JSON 作为字符串发送