如何防止 Socket/Port 耗尽?

19

我正在尝试通过多线程向网站发送请求来进行性能测试。每个线程都执行n次请求(在循环中)。

然而,我遇到了问题。具体来说是WebException ("无法连接到远程服务器")并带有内部异常:

  

由于系统缓冲区空间不足或队列已满,因此无法执行套接字操作127.0.0.1:52395

我正在尝试运行100个线程,每个线程500个迭代。

最初,我使用的是System.Net中的HttpWebRequest进行GET请求。目前,我正在使用WebClient,因为我假设每次迭代都使用一个新的套接字(因此在短时间内会有100 * 500个套接字)。我假设WebClient(每个线程实例化一次)只会使用一个套接字。

我不需要同时打开50,000个套接字,因为我想发送GET请求、接收响应并关闭套接字,以便在下一次循环迭代中释放它。我知道这可能是一个问题

但是,即使使用WebClient,也会请求一堆套接字,导致大量套接字处于TIME_WAIT模式(使用netstat检查)。这会导致其他应用程序(如Internet浏览器)停止响应和运行。

我可以降低测试的迭代次数和/或线程数量,因为套接字似乎最终会退出此TIME_WAIT状态。然而,这并不是解决方案,因为它不能充分测试Web服务器的能力。

问题:

如何在每个线程迭代后显式关闭套接字(从客户端)以防止TIME_WAIT状态和套接字耗尽?

代码:

HttpRequest的包装类

编辑:将WebClient包装在using中,因此为每个迭代实例化、使用和处理一个新的WebClient。问题仍然存在。

  public sealed class HttpGetTest : ITest {
    private readonly string m_url;

    public HttpGetTest( string url ) {          
        m_url = url;
    }

    void ITest.Execute() {
        using (WebClient webClient = new WebClient()){
            using( Stream stream = webClient.OpenRead( m_url ) ) {          
            }
        }
    }
}

创建新线程的ThreadWrapperClass的部分:

public void Execute() {
    Action Hammer = () => {
        for( int i = 1; i <= m_iterations; i++ ) {
            //Where m_test is an ITest injected through constructor
            m_test.Execute();
        }       
    };
    ThreadStart work = delegate {
        Hammer();
    };
    Thread thread = new Thread( work );
    thread.Start();
}

需要考虑的一件事是不要使用“喝水龙头”的测试方法。您应该从慢开始,逐渐增加请求/秒数到一个固定的最大值来正确测试您的系统。然后,您可以在多次运行中逐步增加最大值,直到找到极限。无限制的网络请求将告诉您很少东西。 - Gray
请注意,只有大约65000个可用端口,而且并非所有端口都可以用于传出连接。因此,您需要使用多个IP / NIC来完成您尝试进行的50000个连接。 - Stefan H
@StefanH 我理解如果我执行大量线程,这可能是一个问题,但是一旦循环的迭代完成,我就不再需要套接字了,但它仍然存在,导致下一次迭代打开一个新的套接字。我正在寻找一种方法来防止这种情况发生。 - James
2
你正在对返回的流进行using操作,但没有对你的Web客户端进行操作。你可以尝试在WebClient上使用using语句,以便它也被处理掉。或者在读取完成后手动处理掉它。 - Stefan H
5个回答

17

你了解TIME_WAIT的目的吗?它是一个时间段,在这个时间段内重发的丢失数据包可能会被成功传输,因此重复使用端口可能会不安全。

你可以在注册表中进行调整,但我怀疑这是否是明智的下一步。

在测试环境中创建真实负载的经验非常令人沮丧。当然,从本地主机运行负载测试器绝不是真实的,而且我使用的大多数 .net http API 进行的网络测试似乎需要客户端比服务器本身更大的工作量。

因此,最好移动到第二台机器上为您的服务器生成负载...然而国内路由设备很少能够支持任何会对良好编写的服务器应用程序造成负载的连接数量,因此现在您需要升级您的路由/交换设备!

最后,我在 .net Http 客户端 API 周围遇到了一些非常奇怪和意外的性能问题。归根结底,它们都使用 HttpWebRequest 来完成繁重的工作。我认为它的性能远远达不到它的潜力。DNS 是同步的,即使异步调用 API(虽然如果您只从单个主机请求,这不是问题),并且在持续使用后,CPU 使用率会上升,直到客户端变为受 CPU 限制而不是 IO 限制。如果您要生成持续和重负载,依赖于 HttpWebRequest 的任何请求密集型应用程序在我看来都是一个虚假的投资。

总的来说,这是一项相当棘手的工作,最终只有在实际环境中才能证明,除非你有大量现金可以花费在更好的设备上。

[提示:我使用异步 Socket API 和第三方 DNS 客户端库编写的自己的客户端获得了更好的性能]


我很感谢这篇文章。我将尝试使用独立的硬件/办公资源分配路径,但我也想尝试实施一种不需要额外资源的技术解决方案。你使用了什么库?其他技术(比如C++)会更好吗? - James
1
DNS对你来说不是问题,但你可能会发现这很有趣:https://dev59.com/cGgu5IYBdhLWcg3wOUq-。我发现使用.net Socket api聊天HTTP可以获得更好的性能。我自己写了一个小库(用于爬虫),但不能分享,因为它属于我的公司,并且只实现了HTTP的非常有限的子集。然而,当进行HTTP时,它确实使.net非常快速,几乎处于空闲状态时达到IO极限。 - spender

3

问:如何显式关闭套接字…以防止TIME_WAIT状态?

答:伙计,TIME_WAIT是TCP/IP本身的一个重要部分!

您可以调整操作系统以减少TIME_WAIT(这可能会产生负面影响)。

并且您可以调整操作系统以增加#/短暂端口:

以下是有关TIME_WAIT存在的原因以及它为何是好事的链接:


2
这不是在你的应用程序中关闭套接字或释放资源的问题。TIME_WAIT 是TCP堆栈对已释放套接字的超时时间,以防止在任何剩余数据包尚未过期的情况下重复使用该套接字。

为了测试目的,您可以将等待时间从默认值(几分钟,据我所知)减少到较小的值。当负载测试服务器时,我将其设置为六秒。

它在注册表中的某个地方 - 如果您搜索,就会找到它。

找到了:

更改TIME_WAIT延迟


1

看起来你没有强制 WebClient 释放已分配的资源。你正在对返回的流执行 Using 操作,但是你的 WebClient 仍然有资源。

要么将你的 WebClient 实例化包装在 using 块中,要么在从 URL 读取完成后手动调用 dispose。

试试这个:

public sealed class HttpGetTest : ITest {
    private readonly string m_url;

    public HttpGetTest( string url ) {
        m_url = url;        
    }

    public void ITest.Execute() {
        using( var m_webClient = new WebClient())
        {
            using( Stream stream = m_webClient.OpenRead( m_url ) ) 
            {

            }
        }
    }
}

我最初将其“未包装”,因为我认为它只会在其生命周期中创建1个套接字。我编辑了我的代码(如上所示),但问题仍然存在。 - James
@James 我不确定是什么,很抱歉我不能提供更多帮助。 - Stefan H

0

你不需要在TIME_WAIT上瞎折腾就能实现你想要的。

问题在于每次调用Execute()时,你都会处理WebClient。这样做会关闭与服务器的套接字连接,并使TCP端口保持忙碌状态,直到TIME_WAIT期间结束。

更好的方法是在HttpGetTest类的构造函数中创建WebClient,并在整个测试过程中重复使用同一个对象。

WebClient默认使用keep alive,并将为其所有请求重用相同的连接,因此在您的情况下,仅会打开100个连接。


  1. 这是问题代码的原始版本(问题仍然谈论“我假设WebClient(每个线程实例化一次)只使用一个套接字。”)
  2. 仅当您要与同一主机名建立第二个连接时,Keep-alive才会重用连接。问题是关于在关闭连接后重用客户端套接字。
- Ben Voigt
嗯...问题被编辑了,我没有看到原始代码。1.是的,正如我所说的那样,这是正确的。2.是的,同一主机,还能怎么样呢?而且,他也没有说有很多主机,即使是一个Webfarm,100个线程可能足以击中所有服务器(假设它远远少于100个)。可能还有其他事情发生了,如果您重用它,WebClient应该会表现正确。问题是“如何防止套接字/端口耗尽?”:正确使用WebClient(或者也许是HttpClient)将防止端口耗尽。 - andrecarlucci

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