Runtime: 单例 HttpClient 不尊重 DNS 更改

创建于 2016-08-29  ·  77评论  ·  资料来源: dotnet/runtime

如以下帖子所述: http://byterot.blogspot.co.uk/2016/07/singleton-httpclient-dns.html ,一旦您开始保留共享的HttpClient实例以提高性能,您'将遇到一个问题,即客户端在故障转移情况下不会尊重 DNS 记录更新。

潜在的问题是ConnectionLeaseTimeout的默认值设置为-1 ,无限。 它只会在处理客户端时关闭,这是非常低效的。

修复方法是更新服务点上的值,如下所示:

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1 minute

不幸的是,今天没有办法用 .NET Core 做到这一点。

应该将 ServicePointManager 引入 .NET Core,或者应该以其他方式启用类似的等效功能。

area-System.Net.Http enhancement

最有用的评论

这绝对是个问题。 @darrelmiller提出的解决方案似乎表明对用例缺乏了解。

我们使用 Azure 流量管理器来评估实例的运行状况。 它们在具有短 TTL 的 DNS 之上工作。 主机不会神奇地知道它不健康并开始发出 C onnection:close标头。 流量管理器只是检测到不健康状态并相应地调整 DNS 解析。 按照设计,这对客户端和主机是完全透明的......就目前而言,当先前解析的主机变得不健康时,流量管理器(在不知不觉中)解析 DNS 的静态 HttpClient 应用程序将永远不会收到新主机。

从 netcore 1.1 开始,我目前正在寻找解决这个确切问题的方法。 我没有看到一个,无论如何。

所有77条评论

我不认为这是同一个属性。 那是连接超时,而不是 ConnectionLeaseTimeout。

https://msdn.microsoft.com/en-us/library/system.net.servicepoint.connectionleasetimeout.aspx 中所述,在某些情况下应定期删除它。 但是没有明显的方法可以改变 .NET Core 上的行为。

@onovotny是的,这似乎是一件奇怪的事情,它似乎做了一个明确的连接:在特定时间范围后

@darrelmiller所以这超出了我的范围,不确定......但如果有解决原始问题的书面解决方法,那将是一个巨大的好处。

@onovotny ,.NET Core 中没有ServicePointManager类。 这听起来像是 .NET Framework(桌面)问题。 请在 UserVoice 或 Connect 中提出问题。

@onovotny据我了解,最初的问题是,在服务器端,有人想要更新 DNS 条目,以便将来对主机 A 的所有请求都转到新的 IP 地址。 在我看来,最好的解决方案是当 DNS 条目发生变化时,应该告诉主机 A 上的应用程序在所有未来的响应中向客户端返回一个 C onnection:close标头。 这将迫使所有客户端断开连接并建立一个新连接。 解决方案是在客户端上没有超时值,因此客户端在“租用”超时到期时发送 C onnection:close

这与 Stackoverflow单元测试 DNS 故障转移中的问题非常吻合

这绝对是个问题。 @darrelmiller提出的解决方案似乎表明对用例缺乏了解。

我们使用 Azure 流量管理器来评估实例的运行状况。 它们在具有短 TTL 的 DNS 之上工作。 主机不会神奇地知道它不健康并开始发出 C onnection:close标头。 流量管理器只是检测到不健康状态并相应地调整 DNS 解析。 按照设计,这对客户端和主机是完全透明的......就目前而言,当先前解析的主机变得不健康时,流量管理器(在不知不觉中)解析 DNS 的静态 HttpClient 应用程序将永远不会收到新主机。

从 netcore 1.1 开始,我目前正在寻找解决这个确切问题的方法。 我没有看到一个,无论如何。

@kudoz83你是对的,主机不会神奇地知道流量管理器何时调整了 DNS 解析,因此我说

当该 DNS 条目更改时,应告知主机 A 上的应用程序

如果您不认为通知源服务器它应该返回 c onnection:close标头是切实可行的,那么您只剩下几个选择:

  • 定期关闭来自客户端或中间件的连接,并支付重新打开这些连接的冗余成本
  • 让客户端轮询服务以了解何时应该重置连接。
  • 使用一个足够聪明的中间件,可以知道 DNS 何时切换。

关于我不了解用例。 最初的问题是基于一篇博客文章,其中用例与生产槽和暂存槽之间的切换有关,与运行状况监控无关。 尽管这两种情况都可以从接收 DNS/插槽切换通知中受益。

AFAIK没有办法通知失败的主机它失败了? 通常这意味着它处于某种错误状态。 原始帖子正在谈论故障转移方案。

Azure 流量管理器在这里解释https://docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-monitoring

它在 DNS 级别运行,并且对客户端和主机是透明的 - 设计使然。 Azure 实例通过流量管理器相互通信以进行 DNS 解析,因此无法为 HttpClient 的 DNS 刷新设置 TTL。

HttpClient 目前的运作方式似乎与微软自己的 Azure 服务产品不一致——这就是重点。 更不用说,任何类似的服务(例如 Amazon Route 53)的工作方式完全相同,并且具有完全相同的短 DNS TTL 依赖关系。

显然我找到了这个线程,因为我受到了这个问题的影响。 我只是想澄清一下,除了为了确保 DNS 故障转移成功而任意重新创建 HttpClient(以性能和复杂性为代价)之外,目前似乎没有理想的解决方法。

AFAIK没有办法通知失败的主机它失败了?

如果“失败”的主机无法执行任何代码,那么我们就不需要 HTTP 状态代码 503,因为源服务器永远无法返回它。 在某些情况下,故障主机可能无法处理通知,但在这些情况下,它可能也不会长时间保持活动的 TCP/IP 连接打开。

当阿里最初在 Twitter 上提出这个话题时,在他写博客文章之前,他一致认为客户端将继续发出请求并从已换出的服务器获得响应。

我了解您的情况有所不同,您不能依赖源服务器能够可靠地关闭连接。 我不确定我理解的是为什么对于您的情况,您不能使用 HttpMessageHandler 来检测 5XX 响应,关闭连接并重试请求。 或者更简单,只要说服您的 Web 服务器在返回 5XX 状态代码时返回 c onnection:close 即可

还值得注意的是,您关心的行为不在 HttpClient 中。 它在底层的 HttpHandler 甚至在它之下,并且有许多不同的 HttpHandler 实现在使用中。 在我看来,当前的行为与 HTTP 设计的工作方式是一致的。 我同意在连接打开时更新 DNS 设置会遇到挑战,但这不是 .net 问题。 这是一个 HTTP 架构问题。 这并不意味着.net 不能做一些事情来减少这个问题。

我很好奇您认为解决方案应该是什么。 您是否认为应该重新实现像 ConnectionLeaseTimeout 这样的东西,它只是定期断开连接,而不管是否有故障转移? 或者你有更好的解决方案吗?

对不起,

我不应该像昨天听起来那样好斗。 度过了糟糕的一天。

老实说,我不知道 ConnectionLeaseTimeout 是如何在幕后工作的。 所需的功能是 HttpClient 可配置,以便在建立新连接之前执行新的 DNS 查找,而不是缓存先前的 DNS 查找,直到客户端被释放。 我不相信这需要杀死现有的连接......

@kudoz83不用担心,我们都有这样的日子 :-)

据我了解 ConnectionLeaseTimeout 实际上是一个计时器,它会定期关闭来自客户端的空闲连接。 它在 ServicePointManager 中实现,在 core 中不存在。 这与“空闲连接”超时不同,“空闲连接”超时仅在一段时间未使用时关闭连接。

可能存在允许刷新 DNS 客户端缓存的 Win32 调用。 这将确保新连接进行 DNS 查找。 我快速寻找它但没有找到它,但我确定它在某个地方。

在 Windows 上的 .net core 中,HTTP 连接的管理实际上留给了操作系统。 最接近的是对 Win32 API WinHttpOpen 的调用https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs# L718我找不到可以使用 _sessionHandle 执行任何操作的公共 API。 要获得您正在寻找的那种行为改变,需要操作系统人员进行更改。 至少我是这么理解的。

让 Azure 流量管理器在 DNS 条目上设置一个非常低的 TTL 不是更容易吗? 如果您不关心长期存在的连接,那么低 TTL 应该可以最大限度地减少针对死服务器设置新连接的机会。

WinHttpHandler 构建方式的好处是我们可以通过操作系统更新免费获得 HTTP/2 之类的东西。 缺点是我们无法对托管代码进行大量控制。

@darrelmiller

[ConnectionLeaseTimeout] 在 ServicePointManager 中实现,core 中不存在。

似乎ServicePoint.ConnectionLeaseTimeout将在 .Net Standard 2.0/.Net Core 1.2 中回归。 (虽然我不知道例如 WinHttpHandler 连接是否会遵守它。)

我认为这里存在一些误解:DNS TTL 与 HTTP/TCP 连接生命周期完全无关。

新的 HTTP 连接执行 DNS 查找并连接到返回的主机地址。 可以通过同一个连接发出多个 HTTP 请求,但一旦打开 HTTP 连接(和底层 TCP 连接)就不会再次进行 DNS 查找。

这是正确的行为,因此即使您通过持续数天的连接发出 1000 个请求,DNS 解析也无关紧要,因为连接没有改变。 如果您通过新的 HTTP 连接发出新的 HTTP 请求

此 DNS 刷新与HttpClient无关,它默认使用HttpClientHandler (它本身使用HttpWebRequest套接字堆栈)并且任何 DNS 解析都由该堆栈处理。 ServicePointManager管理该堆栈中的ServicePoint对象,这些对象是与特定主机的连接。 ServicePointManager确实有一些最低限度的 DNS 查询客户端缓存,您可以对其进行调整。

默认设置旨在让连接无限期保持打开状态以提高效率。 如果连接的主机变得不可用,则连接将自动中断,从而使用新的 DNS 查找导致新连接,但是 DNS TTL 不会导致任何新连接。 如果需要,您必须将其编码到您的应用程序中。

如果您希望进行另一次 DNS 查找,您需要在客户端上重置连接。 您可以强制 TCP 连接的最大生命周期,发送connection: close标头或仅处理HttpClient或底层ServicePoint 。 您还可以使用Dns.GetHostAddresses进行 DNS 解析,并使用 IP 地址代替您的 HTTP 请求。

希望对您有所帮助,如果我对此有所帮助,请告诉我。

@manigandham

我认为这里存在一些误解:DNS TTL 与 HTTP/TCP 连接生命周期完全无关。

是的,DNS TTL 相当不相关,但并不完全相关。

新的 HTTP 连接执行 DNS 查找并连接到返回的主机地址。

对,那是正确的。 但是 Windows 有一个尊重 TTL 的本地 DNS 缓存。 如果 TTL 较高且 DNS 服务器已更改 DNS 记录,则由于客户端缓存 DNS 记录,新连接将针对旧 IP 地址打开。 刷新客户端 DNS 缓存是我知道的唯一解决方法。

可以通过同一个连接发出多个 HTTP 请求,但一旦打开 HTTP 连接(和底层 TCP 连接)就不会再次进行 DNS 查找。

再更正。 在被换出的服务器仍在响应的生产/暂存交换中,这将导致许多未来的请求转到旧服务器,这很糟糕。 但是,最近的讨论是关于服务器出现故障并且流量管理器切换到新服务器的场景。 与故障服务器的现有连接会发生什么情况是未知的。 如果是严重的硬件故障,则很有可能会断开连接。 这将迫使客户端重新建立连接,这就是低 TTL 有用的地方。 但是,如果服务器故障并没有破坏服务器并且它只是返回 5XX 响应,那么服务器返回 c onnection:close以确保将来的请求将在新连接上进行,希望对新的工作有帮助服务器

本次DNS刷新与HttpClient无关,只是默认使用HttpClientHandler(本身使用HttpWebRequest套接字栈)

是和不是。 在 .Net 核心上,HttpClientHandler 已被重新编写,不再使用 HttpWebRequest,因此不使用 ServicePointManager。 因此这个问题的原因。

我仔细研究了 IIS,似乎没有办法让 5XX 响应与connection:close标头一起出现。 有必要实现一个 HttpModule 来做到这一点。

作为一个仅供参考,这里有一篇相关的文章涵盖了这个问题和一个临时解决方案。 小心 HttpClient

任何一个 ServicePointManager 都应该被带到 .NET Core

虽然我们为平台奇偶校验带来了 ServicePointManager 和 HttpWebRequest,但它不支持 .Net Core 中的大部分功能,因为底层堆栈解析为不公开高级控制旋钮的 WinHTTP 或 Curl。

使用 ServicePointManager 来控制 HttpClient 实际上是在 .Net Framework 上使用 HttpWebRequest 和托管 HTTP 堆栈的副作用。 由于在 .Net Core 上使用 WinHttpHandler/CurlHandler,SPM 无法控制 HttpClient 实例。

或类似的等效功能应该以其他方式启用。

这是 WinHTTP 团队的回答:

  1. 出于安全原因,当客户端不断发送请求并且_有活动连接_时,WinHTTP 将继续使用目标 IP。 无法限制这些连接保持活动的时间。
  2. 如果到远程端点的所有连接都被服务器关闭,_new_ 连接将查询 DNS 并因此指向新 IP。

要强制客户端使用下一个 IP,唯一可行的解​​决方案是确保服务器拒绝新连接并关闭现有连接(已在此线程中提到)。
这与我从ATM 文档中了解的内容相同:检测是基于 GET 请求失败 4 次(非 200 响应)进行的,在这种情况下,服务器将关闭连接。

鉴于其他网络设备设备本身也在缓存 DNS(例如家庭路由器/AP),我不建议将 DNS 故障转移作为低延迟、高可用性技术的解决方案。 如果需要,我对受控故障转移的建议是拥有一个知道如何正确排水停止的专用 LB(硬件或软件)。

就目前而言,当先前解析的主机变得不健康时,由流量管理器(在不知不觉中)解析 DNS 的静态 HttpClient 的应用程序将永远不会收到新主机。

如上所述,如果不健康是指服务器返回 HTTP 错误代码并关闭 TCP 连接,而您观察到 DNS TTL 已过期,这确实是一个错误。

请更详细地描述您的场景:

  1. 在故障转移期间,客户端计算机的 DNS 缓存和 TTL 是多少。 你可以用ipconfig /showdnsnslookup -type=soa <domain_name>来比较机器认为的TTL是什么和权威的NS的TTL。
  2. 是否有到远程服务器的活动连接? 您可以使用netstat /a /n /b查看连接和拥有它们的进程。
  3. 创建一个 repro 对我们有很大帮助:请附上任何可用信息,例如:.Net Core/Framework 的版本、客户端/服务器的代码、网络跟踪、涉及的远程 DNS LB/流量管理器等。

如果你连接到一台机器并且 TCP/HTTP 连接继续工作,你为什么要在某些时候假设服务器不再合适。
我认为 Azure 方面还有改进的空间。 例如,当机器不健康或交换环境时,可以关闭现有连接。

或者只是处置 ... 底层ServicePoint

@manigandham怎么样? 它不是一次性的,我能看到的唯一相关方法是CloseConnectionGroup你不能真正使用它,因为组名是一个实现细节(目前是处理程序的一些哈希)。 此外,我不确定如果您在HttpClient使用它时尝试处置 ServicePoint(尤其是同时使用它)会发生什么。

@ohadschn

.NET Core 2.0 带回了ServicePoint类和功能: https :

您可以返回使用连接租用超时来设置活动连接在重置之前的最大时间范围。

HttpClient请求正在进行时关闭连接并没有什么特别之处,这与您在浏览互联网时失去连接一样。 请求永远不会成功,或者它成功了,但您没有得到响应 - 两者都应该导致返回 http 错误,您必须在应用程序中处理它们。

使用连接租用超时设置,您可以确保仅在发送请求后完成此操作,并在超过时间限制的下一个请求之前重置。

.NET Core 2.0 恢复了 ServicePoint 类和功能: https ://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint?view
您可以返回使用连接租用超时来设置活动连接在重置之前的最大时间范围。

实际上,它并没有完全恢复功能。 API 表面回来了。 但它基本上是无操作的,因为 HTTP 堆栈不使用该对象模型。

抄送: @stephentoub @geoffkizer

我认为这个线程中的许多参与者在这个主题上是一个“梦之队”,但我仍然不确定是否已经就我们今天所拥有的最佳实践达成了任何共识。 在几个关键点上从这个小组获得决定性的答案会很棒。

假设我有一个针对许多平台的通用 HTTP 库,并非所有平台都支持ServicePointManager ,并且我正在尝试提供默认情况下根据最佳实践智能管理HttpClient实例的行为。 我事先不知道将调用什么主机,更不用说它们在 DNS 更改/C onnection:close标头方面是否表现良好。

  1. 如何以最佳方式管理HttpClient实例? 每个端点一个? 每个主机/模式/端口一个? 从字面上看,总共是一个单身人士吗? (我对此表示怀疑,因为消费者可能希望利用默认标题等)

  2. 我将如何最有效地处理这个 DNS 问题? 1 分钟后处理实例? 将其视为“极不可能”并且默认情况下不提供任何处置行为?

(顺便说一句,不是假设的情况;我现在正在努力解决这些确切的问题。)

谢谢!

我什至不会被邀请参加梦之队的选拔赛,但我确实对你的单身HttpClient评论有一个见解。 首先,您始终可以使用SendAsync方法来指定不同的标头和地址,因此您可以将其包装在您的类中,该类提供类似于HttpClient共享数据(例如默认标头,基地址)最终调用同一个客户端。 但更好的是,您可以创建多个HttpClient实例,它们都共享相同的HttpClientHandlerhttps :

至于 DNS 问题,这里的共识似乎是:

要强制客户端使用下一个 IP,唯一可行的解​​决方案是确保服务器拒绝新连接并关闭现有连接

如果这对您不可行,Darrel Miller 在这里建议了更多选项: https :

@ohadschn谢谢,希望我不会离题太远,但是在多个主机之间共享相同的 HttpClient(或 HttpClientHandler)是否有任何_好处_? 我可能是错的,但在我看来,新主机意味着新的 DNS 查找、新的连接、新的套接字。 TIME_WAIT 中的套接字可以回收并重新用于不同的主机吗? 我原本以为不会,但我们肯定已经达到了我的专业知识的极限。 如果可以的话,也许这就是使用 HttpClient 单例的好处,即使有多个主机?

关于 DNS 建议,我觉得这是最缺乏共识的地方,这是可以理解的。 如果您无法控制服务器端发生的事情,那么确保服务器正常运行是不可行的。 我_可以_设计一些定期处理实例的东西。 问题是在大多数情况下这是否值得权衡。 我想这在很大程度上取决于我做出判断。 :)

对我来说,共享相同的HttpClient [ Handler ] 最明显的好处是简单。 您不必保留主机和客户端之间的映射等内容。 我不知道 TIME_WAIT 的东西,也不是我的专业知识。

关于 DNS,当您不控制服务器时,您提到了一种可能的方法。 您还可以实现一种机制,该机制仅在发生实际更改时定期查询 DNS 并处置实例……

虽然指定服务器端应该发生什么的给定答案对于理想世界来说是准确的,但在现实世界中,开发人员通常无法控制他们正在与之通信的完整服务器端堆栈。 在这方面,我真的很喜欢 ConnectionLeaseTimeout 的概念,因为它是对 John 和 Jane Doeveloper 日常生活现实的让步。
有人建议编写中间件以在 .NET Core 上强制执行此类超时。 这个基于 NodaTime 和 System.Collections.Immutable 的类会完成这项工作吗?

    public class ConnectionLeaseTimeoutHandler : DelegatingHandler
    {
        private static string GetConnectionKey(Uri requestUri) => $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}";

        private readonly IClock clock;
        private readonly Duration leaseTimeout;
        private ImmutableDictionary<string, Instant> connectionLeases = ImmutableDictionary<string, Instant>.Empty;

        public ConnectionLeaseTimeoutHandler(HttpMessageHandler innerHandler, IClock clock, Duration leaseTimeout)
            : base(innerHandler)
        {
            this.clock = clock;
            this.leaseTimeout = leaseTimeout;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string key = GetConnectionKey(request.RequestUri);
            Instant now = clock.GetCurrentInstant();

            Instant leaseStart = ImmutableInterlocked.GetOrAdd(ref connectionLeases, key, now);

            if (now - leaseStart > leaseTimeout)
            {
                request.Headers.ConnectionClose = true;
                ImmutableInterlocked.TryRemove(ref connectionLeases, key, out leaseStart);
            }

            return base.SendAsync(request, cancellationToken);
        }
    }

@snboisen我认为这看起来是对的,但不要

对我来说,更大的问题是发送Connection: close标头是否是一个可行的解决方案。 值得在这里重复@darrelmiller在提示该线程的博客文章的评论中所说的

ConnectionLeaseTimeout 是一个奇怪的野兽,直到今天 Oren 提到它,我才意识到它。 它将导致传出请求在租约到期后包含 C onnection:Close标头,从而导致连接在服务器发送响应后终止。 为什么将其设置为默认值是有道理的,因为当客户端主动发出请求时,强制关闭连接是一种奇怪的方式。

我尊重达雷尔的意见,这让整个方法看起来相当老套。 另一方面,我同意如果开发人员无法控制服务器端发生的事情,该怎么办? 如果它_有时_有效,也许这是我们最好的选择? 这肯定比定期处理 HttpClients 或定期进行 DSN 查找“便宜”。

是否有针对此问题的 Azure 票证?
从我的角度来看,看起来你们都在尝试解决在 Azure 流量管理器中实现的方式。
我不是 Azure 用户,所以我从远处看它,感觉不太愿意跳过它。

@tmds我还没有看到,但正如其他人所提到的,这不仅仅与 Azure 流量管理器有关:AWS 有一个类似的概念,还有 Azure 应用程序服务,它的暂存/生产槽交换。 我敢打赌,其他云提供商也会提供类似于这种交换机制的东西。

@snboisen您是说 AWS 和其他云提供商(哪个?)也在使用 DNS 超时来实现这些功能?

@tmds对于 AWS,是的,至少对于故障转移: https : //aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
我不了解其他云提供商,但似乎有些人也这样做。

@tmds对于 AWS,是的,至少对于故障转移: https : //aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
我不了解其他云提供商,但似乎有些人也这样做。

这里有两个有问题的场景:

  • 客户端连接到不再存在的服务器
  • 客户端连接到现在正在运行该软件的过时版本的服务器

DNS故障转移与第一个有关。 从 DNS(在 AWS、Azure 等)中删除无法访问的服务器是合适的。
在客户端检测它的正确方法是超时机制。 我不确定 HttpClient 上是否有这样的属性(已建立连接的最大回复超时)。
在协议级别,较新的 websocket 和 http2 协议定义了 ping/pong 消息来检查连接的活跃度。

当我建议使用 Azure 打开一个问题时,它是针对第二种情况的。
客户端没有理由关闭工作连接。 如果服务器不再合适,则应断开客户端并且必须使服务器无法访问。
我找到了一个关于 AWS Blue/Green 部署的好文档: https :
它列出了几种技术。 对于那些使用 DNS 的人,它提到了_DNS TTL 复杂性_。 对于那些使用负载平衡器的人_没有 DNS 复杂性_。

@tmds感谢您的链接,这很有趣。

我确实想知道依赖 DNS TTL 与使用负载均衡器相比有多普遍。 一般来说,后者似乎是一个更可靠的解决方案。

在 Azure App Services 中使用通过HttpClient通信的 2 个不同的 API 应用程序时遇到了一些类似的行为。 我们注意到负载测试中有大约 250 个请求,我们会开始出现超时和“无法建立连接” HttpRequestException - 无论是并发用户还是顺序请求都无关紧要。

当我们切换到using块以确保在每个请求中释放HttpClient ,我们没有收到任何这些异常。 从单例到using语法的性能影响(如果有的话)可以忽略不计。

当我们切换到using块以确保在每个请求中释放HttpClient ,我们没有收到任何这些异常。 从单例到using语法的性能影响(如果有的话)可以忽略不计。

这不一定与性能有关,而是关于不泄漏 TCP 连接并获得SocketException s。

来自MSDN

HttpClient 旨在被实例化一次并在应用程序的整个生命周期中重复使用。 尤其是在服务器应用程序中,为每个请求创建一个新的HttpClient实例将耗尽重负载下可用的套接字数量。 这将导致SocketException错误。

另请参阅https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

@khillang感谢您的链接(以及对SocketException的澄清,我肯定在那里有误会)- 我遇到过那个帖子和 3 或 4 个其他帖子,概述了同样的事情。

我知道单实例化是意图,但是当我们在 Azure 应用程序服务中使用它时,我们始终在相当适中的负载下达到HttpRequestExceptions (如上所述,250 个连续请求)。 除非有其他解决方案,否则处理HttpClient是我们必须实施的解决方法,以避免遇到这些一致的异常。

我假设一个“更正确”的解决方案是捕获这些异常,取消所有挂起的请求,明确处理HttpClient ,重新实例化HttpClient ,然后重试所有请求。

似乎有点乱七八糟的东西

旨在实例化一次并在应用程序的整个生命周期中重复使用

我们希望有一个更干净的解决方案,但看起来这不是我们的代码有问题,而是框架的限制。 我很高兴错了,我只需要一些关于我们如何按预期使用它而不遇到这些问题的指导。

@snboisen我测试了你的 ConnectionLeaseTimeoutHandler ,我发现它不适用于多个连接。 每次发生租用超时时,只会向一个连接发出“关闭”,但在并行发送请求时,可能会打开多个连接,并使用单个 HttpClient 实例。 从我在提琴手中看到的(我使用错误的 IP 地址来生成错误以检查 DNS 开关)发送“关闭”后,一些请求使用新 IP,一些请求使用旧 IP。 在我的情况下,我的连接限制为 2,并且需要多次迭代连接租用超时才能刷新两个连接。 来自生产服务器的更多并发连接显然会更糟,但它可能最终会在一段时间后切换。

我不知道有一种方法可以向 HttpClient 实例使用的每个单独的连接发出“关闭”。 这当然是理想的,但我要做的是为 HttpClient 使用 GetOrCreate() 工厂方法,并每 120 秒创建一个新的 HttpClient 实例以确保创建所有新连接。 我只是不确定如何在所有请求完成以关闭连接后处理旧的 HttpClient 实例,但我认为 .NET 会解决这个问题,因为为每个请求创建一个新的 HttpClient 实例从来都不是问题。

作为旁注,我相信刷新必须发生在客户端。 服务器不负责传递 DNS 更改或任何中间件。 DNS 的目的是抵消这种责任,因此您可以随时更改 IP,而无需在客户端和服务器之间进行协调,而不仅仅是为 IP 提供友好名称。 完美的用例是 AWS 弹性 IP 地址。 更改仅反映在 DNS 中。 AWS 不会知道从运行在 ec2 实例上的 Web 服务器发送“关闭”标头。 AWS 不会了解位于他们管理的 ec2 实例之上的每个 Web 服务器。 旧服务器不通过“关闭”标头发送的场景是新服务器部署,其中完全相同的软件正在部署到新服务器,所有流量都由 DNS 切换。 旧服务器上的任何内容都不应更改,尤其是在您必须切换回来的情况下。 一切都应该使用 DNS 在一个地方顺利更改。

不管这里讨论的更大问题是什么,ConnectionLeaseTimeout 现在是否在核心中实现? 如果是这样,为什么这个问题没有关闭? 我能够编写一个与原始帖子完全相同的示例,而刚才没有问题。 无论这是否解决了我的连接 DNS 问题,它_似乎_ConnectionLeaseTimeout_is_在核心中实现。

ServicePointMananager.ConnectionLeaseTimeout 未在 .NET Core 中实现。 在 .NET Core 中使用此属性是无操作的。 它仅在 .NET Framework 中起作用。 默认情况下,此属性在 .NET Framework 中设置为“关闭”。

我知道单实例化是意图,但是当我们在 Azure 应用服务中使用它时,我们始终在相当适中的负载(上面提到的 250 个连续请求)下遇到 HttpRequestExceptions。 除非有其他解决方案,否则处置 HttpClient 是我们必须实施的解决方法,以避免遇到这些一致的异常。

我们通过将HttpClient包装在一个自定义 API 中成功地解决了套接字耗尽问题,该 API 允许应用程序中的每个 http 请求重新使用相同的HttpClient实例(它不仅如此,例如RestSharp但这也是好处之一)。 检索它是在lock{}后面,因为每 60 分钟我们创建一个新的HttpClient并将其传递回去。 这使我们能够从我们的 Web 应用程序中执行大量 http 请求(由于我们拥有大量集成),同时仍然不会重新使用尚未关闭的旧连接。 我们已经成功地这样做了大约一年(在遇到套接字耗尽问题之后)。

我确信这仍然会影响性能(尤其是由于所有锁定),但它比由于套接字耗尽而死的所有东西要好。

@KallDrexx为什么需要锁定“全局”实例? 天真地我会把它存储在静态中,然后每 60 分钟用一个新的替换它。 那还不够吗?

我们获取HttpClient是:

```c#
var timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
锁(挂锁)
{
timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
_currentClient = new HttpClient();
_lastCreatedAt = DateTime.UtcNow;
}
}
}

        return _currentClient;

```

所以我猜锁每分钟只发生一次,这样多个线程就不会同时尝试创建一个新的HttpApiClient 。 我认为我们锁定得更积极。

我明白了,所以这是一个方便的原因。
或者,您可以使用基于计时器的(异步)更新,或者允许并行创建多个(线程不安全的延迟初始化样式),或者使用不同的同步机制并且在创建期间不要阻塞其他线程(它们可以重复使用旧的)。

我明白了,所以这是一个方便的原因。

我不知道我是否会将其归类为方便。

如果太多线程试图同时创建一个新的HttpClient ,我们仍然会冒着每分钟创建太多的套接字耗尽的风险,特别是如果旧的套接字在那一分钟内没有正确清理套接字(导致级联)。

此外,我仍然需要一些锁定措施,因为我不知道更新DateTime是否是原子操作(我怀疑不是),因此某些线程可能会读取_lastCreatedAt值更新操作的中间。

为了让所有线程重用旧客户端,同时让一个线程创建一个新客户端,需要大量复杂的逻辑,因此我们可以保证一个线程确实成功创建了新客户端。 更不用说我们仍然需要锁,因为我们不能保证一个线程不会返回_currentClient而另一个线程正在实例化它。

有太多的未知数和增加的复杂性来冒险在生产中这样做。

@KallDrexx

  • 似乎ReaderWriterLockSlim更适合您的使用。
  • 如果您正在运行真正的 x64,DateTime分配应该是原子的(因为它包含单个ulong字段)。 请注意,如果不是这种情况,您当前的代码不是线程安全的,因为您正在观察锁外的DateTime值。
  • @karelz建议的基于计时器的方法根本不需要DateTime ......计时器只需切换HttpClient 。 你仍然需要用一些内存屏障包围HttpClient字段( volatileInterlocked.Exchange等)。

似乎 ReaderWriterLockSlim 更适合您的使用。

整洁,我不知道那门课! 但是,您提到这一点也让我考虑使用SemaphoreSlim ,因为它是 async/await 友好的(它看起来ReaderWriterLockSlim不是),这可能有助于解决可能的线程争用需要一个新的HttpClient 。 然后再次ReaderWriterLockSlim将更好地处理您正确指出的潜在DateTime非原子性问题,所以我必须考虑一下,谢谢!

@karelz建议的基于计时器的方法根本不需要 DateTime ......计时器只会切换 HttpClient。 不过,您仍然需要用一些内存屏障包围 HttpClient 字段(易失性、Interlocked.Exchange 等)。

啊,我误读了他的意思。 这确实是有道理的,尽管我需要更熟悉内存屏障机制才能正确地做到这一点。

谢谢。

编辑:补充一下,这是我认为应该内置一些东西来处理这个问题的一个很好的理由,所以每个HttpClient消费者都不必重新实现/经历这个。

我想知道在.net core 2.0中它是否仍然是一个问题? 我们还需要担心 DNS 更改吗?

我想知道在.net core 2.0中它是否仍然是一个问题? 我们还需要担心 DNS 更改吗?

我的理解是,这不是 Dns 本身的问题,只是HttpClient将尝试重新使用现有连接(以防止套接字耗尽),因此即使 DNS 发生更改,您仍然可以连接到旧的服务器,因为您要通过上一个连接。

@KallDrexx ,所以这意味着蓝绿部署仍然存在问题?

@withinoneyear 据我了解是的。 如果您创建一个HttpClient并将其用于生产绿色,然后将生产交换为蓝色,则HttpClient将仍然与绿色保持打开连接,直到连接关闭。

我知道这很旧,但我相信您不应该使用 DateTime.UtcNow 进行时间间隔测量。 使用像 Stopwatch.GetTimestamp 这样的单调时间源。 这样您就不会受到操作员或时间同步的本地时间调整的影响。 有一些错误的芯片组使 QPC 值倒退,但我不相信它们中的许多现在都在使用。

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1分钟

这段代码是放在构造函数中的吗?

@whynotme8998 @lundcm我不HttpClient是单例并且您访问多个端点,则构造函数将不是放置它的正确位置。
检查这个实现。 此外,在项目的自述文件中,还有一个指向解释问题的博客文章的链接。 那篇文章也指向这个线程。

有人可以帮我理解ConnectionLeaseTimeout确切含义吗?

如果在我的应用程序中将其设置为 1 分钟,这是否意味着连接租约每分钟都会超时? 这对于与我的后端交谈到底意味着什么?

我看到人们在谈论后端的 DNS 更新? 在标准 Azure Web App 上,发布我的 ASP.NET 后端会导致 DNS 刷新吗?

1分钟的超时不会留下一个DNS更新但超时仍未发生的窗口吗?

编辑。 意识到这一点在 .NET Standard 2.0 中不起作用。 解决方法是什么?

使用 .netstandard 2.0 在 Xamarin 上更改 ConnectionLeaseTimeout 不起作用!!!

@paradisehuman我假设您的意思是ServicePoint.ConnectionLeaseTimeout 。 AFAIK 它在 .NET Core 上也不起作用。 在 .NET Core 2.1 中,我们引入了SocketsHttpHandler.PooledConnectionLifetime 。 此外,ASP.NET 中的新HttpClientFactory可以同时利用这两种方法,并且可以为您做更多的事情。
Mono/Xamarin 有自己的网络堆栈实现(他们不使用 CoreFX 源代码进行网络) - 所以最好在他们的 repo 上提交 bug 并提供更多详细信息。

不幸的是,今天没有办法用 .NET Core 做到这一点。 应该将 ServicePointManager 引入 .NET Core,或者应该以其他方式启用类似的等效功能。

@ onovotny,.NET Core 2.1 中的 SocketsHttpHandler 公开了 PooledConnectionLifetime,它的用途类似于 ConnectionLeaseTimeout,只是在处理程序级别。 我们可以考虑在这一点上解决这个问题吗?

@stephentoub现在才看到这个,是的,我认为 PooledConnectionLifetime 应该解决它。 如果仍然需要某些东西,社区可以打开另一个问题。

@onovotny @karelz @stephentoub

你们都同意这是对如何最好地解决原始单例 HttpClient/DNS 问题的准确总结吗?

  • 在 .NET Framework 2.0+ 中,使用ServicePoint.ConnectionLeaseTimeout

  • 在 .NET Core 2.1 中,使用SocketsHttpHandler.PooledConnectionLifetime

  • 对于所有其他平台/目标/版本,没有可靠的解决方案/黑客/解决方法,所以甚至不要费心尝试。

随着我继续从这个线程和其他线程中学习东西(例如ConnectionLeaseTimeout在 .NET Standard 2.0 中不能跨平台可靠地工作),我回到了绘图板上,试图以某种方式从库中解决这个问题可以在尽可能多的平台/版本上运行。 平台嗅探很好。 定期处理/重新创建HttpClient很好,如果有效的话。 我相当肯定我在第一次尝试时弄错了,即定期向主机发送一个connection:close标头。 提前感谢您的任何建议!

@tmenier对于所有其他平台/目标/版本,没有可靠的解决方案/黑客/解决方法,所以甚至不要费心尝试。

您始终可以定期回收实例(例如,只需创建新实例并将其设置为其他人使用的静态变量)。
这就是HttpClientFactory所做的。 您可以选择改用HttpClientFactory

@karelz谢谢,我认为这为我提供了前进的道路。 也许我应该跳过平台嗅探并在任何地方回收实例,或者建议使用其他可用的方法(以避免开销等)?

棘手的一点可能是知道_when_ 处理回收实例是安全的(我需要担心并发/挂起的请求),但我可以看看HttpClientFactory是如何处理它的。 再次感谢。

@tmenier不想循环情况下每HttpClient电话,因为这将导致你到TCP套接字耗尽。 需要管理回收(超时或维护一个可以过期的带有HttpClient的池。

@KallDrexx对,我只是在谈论_周期性地_回收一个单例(或“伪”-单例),例如每隔几分钟。 我确实相信这将涉及管理一个池和某种延迟处理死实例,一旦确定它们没有待处理的请求。

啊抱歉误解了你所说的回收的意思。

您不需要处理旧的。 让 GC 收集它们。
代码应该像基于计时器定期设置新的“伪”单例一样简单。 而已。

我发现不处理它们会导致速度变慢,因为您在高性能场景中用完了 http 连接。

@tmenier关于在 .NET Core 2.1 中使用 SocketsHttpHandler.PooledConnectionLifetime,HttpClientHandler 的创建完全封装在https://github.com/dotnet/wcf/blob/master/src/System.Private.ServiceModel/src/System/ ServiceModel/Channels/HttpChannelFactory.cs。 使用 WCF 客户端时有没有办法做到这一点?

@edavedian我建议在 dotnet/wcf repo 上提问。 抄送@mconnew @Lxiamail

底层堆栈解析为不公开高级控制旋钮的 WinHTTP 或 Curl

.Net Core 中是否有通过套接字工作的 HTTP 客户端?

.Net Core 中是否有通过套接字工作的 HTTP 客户端?

是的,SocketsHttpHandler,从 2.1 开始,它是默认的。

HttpClient缓存它命中的 URI,类似于我相信的RestClient吗? 如果是,请告诉我它支持哪个版本(.NET Core 3.0/.NET Framework 3.0 等)。

如果项目是 .Net Core 2.1+ 应用程序,我看到缓存仅在RestClient完成?

HttpClient 是否缓存它命中的 URI,类似于我相信的 RestClient 吗?

HttpClient 不做任何缓存。

@SpiritBob不清楚您指的是哪种形式的缓存。 此问题已关闭; 如果您愿意,请打开一个带有问题的新问题,我们可以帮助您找到答案。

@scalablecory我相信 davidsh 回应了我的想法。 谢谢!

刚读完这个帖子,好像有一个用例没有解决。

使用HttpClientFactory ,生成的HttpClient是否会在收到带有内部SocketException指示的HttpRequestException时自动关闭连接并执行新的 DNS 查找主机不再有效并且 DNS 可能已更改(错误代码 11001 - 不知道这样的主机)?

如果没有,在 DNS 记录上的 TTL 过期之前,让HttpClient执行 DNS 查找的解决方法是什么?

我目前正在使用HttpClientFactory和自定义HttpMessageHandler (使用AddHttpClient<T, U>().ConfigurePrimaryHttpMessageHandler(...)通过依赖注入配置)并且当 DNS 记录在 TTL 到期之前发生更改时,它会出错。 具体来说,我看到在向 Azure Blob 存储进行大量上传时会发生这种情况。

此页面是否有帮助?
0 / 5 - 0 等级