ServicePointManager.ReusePort和SO_REUSE_UNICASTPORT如何缓解短暂端口耗尽问题?

13
Windows 10和Windows Server 2016引入了SO_REUSE_UNICASTPORT套接字选项。从版本4.6开始,通过ServicePointManager.ReusePort静态属性在.NET中可供使用。在非常高的负载(通过HttpClient进行许多并发的出站请求)期间,我的.NET应用程序遭受短暂端口耗尽的困扰,我考虑使用此选项来解决问题。我知道其他处理该问题的方法(例如编辑Windows注册表以修改短暂端口的最大数或缩短TIME_WAIT时间),但我也想完全了解这个解决方案。
对于ServicePointManager.ReusePort的文档非常少:
将此属性值设置为true会导致HttpWebRequest的所有出站TCP连接在套接字上使用本机套接字选项SO_REUSE_UNICASTPORT。这会导致底层出站端口被共享。这对于在短时间内进行大量出站连接且应用程序有风险耗尽端口的情况很有用。
查看SO_REUSE_UNICASTPORT的文档没有提供任何额外的见解:
当设置时,允许Winsock API连接函数重用短暂端口,这些函数需要显式绑定,例如ConnectEx。请注意,具有隐式绑定(例如无需显式绑定即可连接)的连接函数默认情况下已启用此选项。在同时可用的平台上使用此选项,而不是SO_PORT_SCALABILITY。
我找不到任何关于网络上如何实现“短暂端口重用”,它在技术层面上如何工作以及它减少短暂端口耗尽风险的程度的解释。我可以期望多少改进?使用此功能,我如何计算应用程序的新限制?启用此功能是否存在任何缺点?
这一切都笼罩着神秘色彩,如果有人能解释这种新机制及其影响,我将不胜感激。

1
你能分享一下你查看了哪些日志或计数器,表明你的应用程序出现了端口耗尽的情况吗? - Anirudh Goel
1
@AnirudhGoel:我们遇到了可怕的System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted异常,而在那个异常的机器上运行nestat -a -o显示有许多短暂端口正在使用中。 - Allon Guralnek
1个回答

16

TCP连接通过(本地IP,本地端口,远程IP,远程端口)唯一标识。这意味着可以完全使用相同的(本地IP,本地端口)对于连接到不同远程终点的多个套接字。假设您想向“site1.com”和“site2.com”发出HTTP请求。您可以使用以下代码的套接字:

using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {                
    socket.Bind(new IPEndPoint(IPAddress.Parse("some local ip"), 45455));
    socket.Connect(server, port);
    socket.Send(someBytes);
    // ...
}

因此,您正在将套接字绑定到具有端口 45455 的特定本地端点。如果您现在尝试同时向“site1.com”和“site2.com”发出请求,您将收到“地址已在使用中”的异常。

但是,如果您在绑定之前添加 ReuseAddress 选项(请注意,这不是您问题所涉及的选项):

socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

您将能够绑定到相同的本地(IP,端口)套接字,并且在 netstat 中您将看到两个已建立连接。

所有上述都是为了表明理论上没有什么阻止使用短暂端口来建立到不同远程点的多个连接。但是,当您绑定到短暂端口(0)时——尚不知道您要连接到哪个远程点。假设所有短暂端口都在使用中,并且您正在绑定到 0。操作系统在绑定阶段为您提供一些可重用的端口,有一个套接字正在使用此端口已连接到“site1.com”。您也正在尝试连接到“site1.com”,但这会失败(因为两个套接字的四个标识 TCP 连接的值都相同)。

SO_REUSE_UNICASTPORT 的作用是使绑定到 0 的短暂端口选择推迟到实际连接阶段(例如进行 Connect() 调用)。在这个阶段(与绑定不同),您已经知道了要连接到的本地 IP、远程 IP 和远程端口,需要选择短暂端口。假设所有端口都在使用中。现在,您可以选择连接到不同远程端点的端口来重用,而不是选择某个随机端口(可能在连接后失败)。

您可以在此MS 支持文章中进行确认:

SO_REUSE_UNICASTPORT

要实现连接方案,必须在绑定套接字之前设置套接字选项。该选项指示系统推迟端口分配,直到了解连接的 4 元组(四元组)为止。

请注意,SO_REUSE_UNICASTPORT 仅对显式绑定产生影响(如您所引用的问题摘录中所述,但仍值得重申)。如果您进行隐式绑定(例如仅 Connect() 而未进行绑定),则此选项已默认设置(在支持的情况下)。

关于此对您特定应用程序的影响。首先,从上面的内容可以清楚地看出,如果您的应用程序向同一远程端点(例如同一 HTTP 服务器)发出大量请求,则此选项不会产生影响。但是,如果您向不同的端点发出大量请求,这应该有助于防止端口耗尽。仍然,ServicePointManager.ReusePort 本身的效果取决于 HttpClient 内部如何与套接字交互。如果它只是进行 Connect() 而没有显式绑定,则此选项应该已启用(在支持的系统上)默认设置,因此将 ServicePointManager.ReusePort 设置为 true 将不会产生额外的效果;否则,它将会有所帮助。由于您不知道(也不应该依赖)其内部实现,因此在您特定的情况下启用 ServicePointManager.ReusePort 是值得考虑的。

您还可以通过限制短暂端口的范围(使用类似 netsh


这是一个很好的答案,特别是解释了调用相同服务器集合与许多不同服务器之间产生的不同效果。在我们的情况下,问题在于我们的.NET应用程序充当API网关,有时将调用转发到另一个系统(原样)。当另一个系统出现缓慢时,由于响应时间增加但传入流量保持不变,因此对相同服务器集合的并发调用数量增加,最终导致端口耗尽。因此,在这种情况下,ServicePointManager.ReusePort 对我们没有帮助。 - Allon Guralnek
@AllonGuralnek 是的,看起来是这样。在这种情况下,我建议向系统添加更多IP地址(接口),因为这样您就会有(IP地址数*动态端口范围)可用的临时端口(在这种情况下还要查看SO_PORT_SCALABILITY,尽管据我所知,SO_REUSE_UNICASTPORT已经包含了SO_PORT_SCALABILITY)。 - Evk
好的建议。除了将 IP 地址添加到源主机之外,为目标系统添加更多 IP 地址可能也有所帮助,特别是在使用 SO_REUSE_UNICASTPORT 的时候,因为会有更多的目标 IP,所以每个连接上会有更多的重用潜力。顺便说一句,我还没有找到在 .NET 中使用 SO_PORT_SCALABILITY 的方法,特别是在高级网络 API(如 HttpClient 或甚至 HttpWebRequest)中。你知道有什么方法吗? - Allon Guralnek
@AllonGuralnek 不好意思,我不知道如何(如果可能的话)从HttpClientHttpWebRequest获取底层套接字。但是文档说“使用SO_REUSE_UNICASTPORT而不是SO_PORT_SCALABILITY(可伸缩性端口)(如果可能的话)”,这表明前者已经暗示了后者,所以您可以使用ServicePointManager.ReusePort来实现此目的。 - Evk
@AllonGuralnek 但如果你手头有socket,我认为你可以这样设置选项:socket.SetSocketOption(SocketOptionLevel.Socket, (SocketOptionName) 0x3006, true); SocketOptionName 中没有预定义的值,但是在 .NET 中枚举只是一个数字,所以你可以使用任何数字,即使它在枚举本身中没有预定义的值。使用枚举的代码可能会使用 Enum.IsDefined 进行检查,以检查数字是否真的具有相应的枚举值,但对于套接字选项来说并非如此 - 它按原样传递给本机 API。 - Evk
很遗憾,对于我们来说,使用套接字不是一个选项,因为我们需要应用程序范围内的HTTP连接重用和流水线处理,这只有在使用高级HTTP API而不是套接字时才由ServicePointManager提供。我还没有找到一种方法来告诉这些API的底层套接字使用特定的选项。 - Allon Guralnek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接