How TCP backlog works in Linux
linux linux
Lastmod: 2019-08-18

原文: http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html

当一个应用程序使用 listen() 系统调用把一个 socket fd 设置成 LISTEN 状态时,也需要指定一个 backlog 值。通常我们可以认为这个 backlog 代表这个 socket fd 可以接受最大的连接请求数。

#include <sys/types.h>         
#include <sys/socket.h>
int listen(int sockfd, int backlog);

因为 TCP 的三次握手,在 server 端 accept() 系统调用返回,并且tcp 状态在变成 ESTABLISHED 之前,会有一个短暂的 SYN RECEIVED 状态。那么这个状态的 tcp 链接应该放在哪个 queue 里面呢?

  • 单个 queue,其大小就是 listen() 参数 backlog。当一个 SYN 包到达时,server 返回一个 SYN/ACK 给 client,并且把这个链接放入 queue。当 client 的 ACK 到达时,TCP 的状态变成 ESTABLISHED。这就意味着这一个 queue 有两种不同的状态:SYN RECEIVED 和 ESTABLISHED。只有在 ESTABLISHED 状态的链接才能被 accept() 返回给用户程序。

  • 两个 queue,未完成的链接在 SYN queue,建立好的链接在 accept queue。也就是说 SYN REVEIVED 状态的链接会先被放到 SYN queue 里面,当它变成 ESTABLISHED 状态时再被移动到另一个 queue。所以,accept() 系统调用实现起来就简单了,它只从 accept queue 中获取链接返回给应用程序。这时,listen() 系统调用的 backlog 参数决定了 accept queue 的大小。

历史上,BSD 系的 tcp 使用第一种方式。也就是说,当达到 backlog 值时,系统不会对 client 发来的 SYN 包回复 SYN/ACK,一般都是简单的丢掉这个包(而不是发 RST 包),这样的话 client 会认为包丢失,于是会重试。(详见 TCP/IP 详解 14 章)

实际上,在《TCP/IP 详解》一书中,作者解释了 BSD 在实现上其实是两个 queue,但是在逻辑上它们是作为一个 queue 的。

The queue limit applies to the sum of […] the number of entries on the incomplete connection queue […] and […] the number of entries on the completed connection queue […].

而到了 Linux 系统上,却用了第二种方式,以下是引用自 man listen

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog.

所以 Linux 上,SYN queue 的大小是系统的全局变量;accept queue 大小是应用程序调用 listen() 设置的值。

那么问题来了:当 accept queue 满了以后,有新的连接需要从 SYN queue 移动到 accept queue,linux 会怎么做?

net/ipv4/tcp_minisocks.c 中的 tcp_check_req() 负责处理这种情况。

child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
    goto listen_overflow;

函数指针实际调用的是 net/ipv4/tcp_ipv4.c 中的 tcp_v4_syn_recv_sock()

if (sk_acceptq_is_full(sk))
    goto exit_overflow;

exit_overflow 处的代码会更新 /proc/net/netstat 中的 ListenOverflows 和 ListenDrops。

又因为发生了 overflow,所以会触发tcp_check_req() 中的

listen_overflow:
        if (!sysctl_tcp_abort_on_overflow) {
                inet_rsk(req)->acked = 1;
                return NULL;
        }

这就意味着如果/proc/sys/net/ipv4/tcp_abort_on_overflow 为 1,linux 会发 RST 给 client;而如果为默认值 0,linux 什么都不做,也就是默默地丢掉这个包。

总结一下就是:当 linux 收到一个 client 发来的 tcp 三次握手的第三个 ACK 包,并且此时 accept queue 已满,linux 直接忽略这个包。

似乎挺起来很奇怪,但是考虑到 SYN RECEIVED 状态有定时器:如果没有收到 client 的ACK 包(或者忽略了这个包),超时后 server 将会重新发送 SYN/ACK。

server 端会重试固定的次数,这个数由 /proc/sys/net/ipv4/tcp_synack_retries 决定,并采用指数退避的算法。

下面的这个 trace 就是当 server 端已经达到最大 backlog 时,client 端看到的情况:

  0.000  127.0.0.1 -> 127.0.0.1  TCP 74 53302 > 9999 [SYN] Seq=0 Len=0
  0.000  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
  0.000  127.0.0.1 -> 127.0.0.1  TCP 66 53302 > 9999 [ACK] Seq=1 Ack=1 Len=0
  0.000  127.0.0.1 -> 127.0.0.1  TCP 71 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  0.207  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  0.623  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  1.199  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
  1.199  127.0.0.1 -> 127.0.0.1  TCP 66 [TCP Dup ACK 6#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
  1.455  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  3.123  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  3.399  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
  3.399  127.0.0.1 -> 127.0.0.1  TCP 66 [TCP Dup ACK 10#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
  6.459  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
  7.599  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
  7.599  127.0.0.1 -> 127.0.0.1  TCP 66 [TCP Dup ACK 13#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
 13.131  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
 15.599  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
 15.599  127.0.0.1 -> 127.0.0.1  TCP 66 [TCP Dup ACK 16#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
 26.491  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
 31.599  127.0.0.1 -> 127.0.0.1  TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
 31.599  127.0.0.1 -> 127.0.0.1  TCP 66 [TCP Dup ACK 19#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
 53.179  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
106.491  127.0.0.1 -> 127.0.0.1  TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
106.491  127.0.0.1 -> 127.0.0.1  TCP 54 9999 > 53302 [RST] Seq=1 Len=0

可以看到 client 端看到了 server 发来的多个 SYN/ACK,因此 client 认为它的 ACK 包丢了,并且重发(TCP DUP ACK 包)。

如果这时 server 端的某个应用程序运行结束释放资源,使 backlog 小于最大值,并且此时 server 的 retry 还没到最大值,那么 server 就会接收这么多 DUP ACK 中的一个,把这个链接放入 accept queue。否则的话,最后 server 就会发出 RST 。

上面的这个 trace 还展示了一个有趣的行为:从 client 端来说,这个链接从收到第一个 SYN/ACK 开始就已经 ESTABLISHED,client 可以送 data 给 server,但是 TCP 的慢启动机制确保了 在这个阶段 client 只能送很少的数据。

另一方面,如果 client 等 server 发数据,但是 server 无法减小 backlog,那么结果就是:client 端的链接是 ESTABLISHED,但是 server 是 CLOSED,出现了 half-open connection!

还有一个问题需要注意,listen 的 man page 建议每一个 SYN 包都会在 server 的 SYN queue 产生一个新的链接。但事实上,linux 并不是这样实现的。

请看 net/ipv4/tcp_ipv4.c 的 tcp_v4_conn_request()

        /* Accept backlog is full. If we have already queued enough
         * of warm entries in syn queue, drop request. It is better than
         * clogging syn queue with openreqs with exponentially increasing
         * timeout.
         */
        if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
                goto drop;
        }

这就意味着,如果 accept queue 已经满了,linux 会限制接收 SYN 包的速率,多余的 SYN 包会直接丢弃。

Stevens 大佬就指出,linux 的这种实现比 BSD 高明:

The backlog can be reached if the completed connection queue fills (i.e., the server process or the server host is so busy that the process cannot call accept fast enough to take the completed entries off the queue) or if the incomplete connection queue fills. The latter is the problem that HTTP servers face, when the round-trip time between the client and server is long, compared to the arrival rate of new connection requests, because a new SYN occupies an entry on this queue for one round-trip time. […]

The completed connection queue is almost always empty because when an entry is placed on this queue, the server’s call to accept returns, and the server takes the completed connection off the queue.

以上这段话的大意是,未完成的链接会占用 SYN queue,会占用多少时间呢?一般由 client/server 之间的 RTT 决定,因为在 server 收到 client 的 SYN 之后都一直在未完成 queue 里。

而已完成 queue,也就是本文说的 accept queue 则几乎一直会是空的,因为链接一旦进入这个 queue,立刻 accpet() 系统调用就会返回,会把链接从 queue 中拿走。

Stevens 建议的解决方法是增加 backlog 值,但是涉及到增加多少呢?因为 server 不但要考虑现在收到新请求的 SYN 率有多少,还要考虑网络的 RTT 时间。

linux 的实现有效的区分了这两个顾虑:应用程序只负责调整 backlog 值,让它自己可以尽量快的调用 accept() 并且避免占满 accept queue;系统管理官则按照当前流量的特征,调整/proc/sys/net/ipv4/tcp_max_syn_backlog 到一个合适的值。

comments powered by Disqus