与 gen_tcp 并行建立大量连接时出现 CLOSED 错误(错误?)

CLOSED error when establishing lots of connections with gen_tcp in parallel (Bug?)

在尝试并行建立大量 TCP 连接时,我观察到一些奇怪的行为,我认为这是 gen_tcp 中的一个潜在错误。

场景是服务器监听一个端口,有多个并发接受器。从客户端我通过调用 gen_tcp:connect/3 建立连接,然后我向服务器发送 "Ping" 消息并在被动模式下等待 "Pong" 响应。当按顺序执行 'get_tcp:connect/3' 调用时,一切正常,包括大量连接(我测试了 ~ 28000)。

当尝试建立大量并行连接时会出现此问题(取决于机器在 ~75 到几百之间)。虽然大多数连接仍然建立,但一些连接失败并在 gen_tcp:recv/3 中出现 closed 错误。奇怪的是,这些连接之前并没有失败,对 gen_tcp:connect/3gen_tcp:send/2 的调用都成功了 (即返回 ok)。在服务器端,我没有看到这些 "weird" 连接的匹配连接,即没有返回 gen_tcp:accept/1。据我了解,成功的 'get_tcp:connect/3' 应该会在服务器端产生匹配的已接受连接。

我已经提交了 bug report,您可以在其中找到更详细的描述和最简单的代码示例来演示问题。我能够在 Linux 和 Mac OS X 以及不同的 Erlang 版本上重现该问题。

我的问题是:

  1. 是否有人能够重现该问题并确认这是错误行为?
  2. 有什么解决方法吗?如何处理这个问题,其他按顺序启动所有连接(这需要永远)?

TCP 3 次握手 客户端服务器

  connect()│──┐          │listen()
           │  └──┐       │
           │      SYN    │
           │        └──┐ │
           │           └▶│   STATE
           │          ┌──│SYN-RECEIVED
           │       ┌──┘  │
           │   SYN-ACK   │
           │ ┌──┘        │
   STATE   │◀┘           │
ESTABLISHED│──┐          │
           │  └──┐       │
           │     └ACK    │
           │        └──┐ │   STATE
           │           └▶│ESTABLISHED
           ▽             ▽

问题在于用于建立 TCP 连接的 3 次握手和监听套接字处的传入连接队列的更精细细节。有关详细信息,请参阅此 excellent article,本文提供了以下大部分解释。

在Linux中实际上有两个传入连接队列。当服务器收到一个连接请求(SYN数据包)并转换到状态SYN-RECEIVED时,这个连接被放入SYN队列中。如果收到相应的 ACK,连接将被放入接受队列以供应用程序使用。 gen_tcp:listen/2{backlog, N}(默认值:5)选项决定了访问队列的长度。

当服务器接收到 ACK 而接受队列已满时,ACK 基本上被忽略并且没有 RST 被发送到客户端。有一个与 SYN-RECEIVED 状态关联的超时:如果没有收到 ACK(或被忽略,就像这里的情况),服务器将重新发送 SYN-ACK。然后客户端重新发送 ACK。如果应用程序在达到最大重试次数 SYN-ACK 之前从接受队列中消耗了一个条目,服务器最终将处理其中一个重复项 ACKs 并转换到状态 ESTABLISHED。如果已达到最大重试次数,服务器将向客户端发送 RST 以重置连接。

回到并行启动大量连接时观察到的行为。解释是,服务器上的接受队列比我们的应用程序消耗接受的连接更快地填满。 gen_tcp:connect/3 在客户端 return 收到第一个 SYN-ACK 后立即成功调用。连接不会立即重置,因为服务器重试 SYN-ACK。服务器不会将这些连接报告为成功,因为它们仍处于状态 SYN-RECEIVED.

在 BSD 派生系统(包括 Mac OS X)上,传入连接的队列有点不同,请参见上面提到的 article