[译文]深入理解Linux TCP backlog

0.586字数 2047阅读 9439

玩转Linux旧群已满,请加新群:278378501。

当应用程序调用listen系统调用让一个socket进入LISTEN状态时,需要指定一个参数:backlog。这个参数经常被描述为,新连接队列的长度限制。

tcp-state-diagram.png

由于TCP建立连接需要进行3次握手,一个新连接在到达ESTABLISHED状态可以被accept系统调用返回给应用程序前,必须经过一个中间状态SYN RECEIVED(见上图)。这意味着,TCP/IP协议栈在实现backlog队列时,有两种不同的选择:

  1. 仅使用一个队列,队列规模由listen系统调用backlog参数指定。当协议栈收到一个SYN包时,响应SYN/ACK包并且将连接加进该队列。当相应的ACK响应包收到后,连接变为ESTABLISHED状态,可以向应用程序返回。这意味着队列里的连接可以有两种不同的状态:SEND RECEIVEDESTABLISHED。只有后一种连接才能被accept系统调用返回给应用程序。

  2. 使用两个队列——SYN队列(待完成连接队列)和accept队列(已完成连接队列)。状态为SYN RECEIVED的连接进入SYN队列,后续当状态变更为ESTABLISHED时移到accept队列(即收到3次握手中最后一个ACK包)。顾名思义,accept系统调用就只是简单地从accept队列消费新连接。在这种情况下,listen系统调用backlog参数决定accept队列的最大规模。

历史上,起源于BSDTCP实现使用第一种方法。这个方案意味着,但backlog限制达到,系统将停止对SYN包响应SYN/ACK包。通常,协议栈只是丢弃SYN包(而不是回一个RST包)以便客户端可以重试(而不是异常退出)。

TCP/IP详解 卷314.5节中有提到这一点。书中作者提到,BSD实现虽然使用了两个独立的队列,但是行为跟使用一个队列并没什么区别。

Linux上,情况有所不同,情况listen系统调用man文档页:

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. When syncookies are enabled there is no logical maximum length and this setting is ignored.

意思是,backlog参数的行为在Linux2.2之后有所改变。现在,它指定了等待accept系统调用的已建立连接队列的长度,而不是待完成连接请求数。待完成连接队列长度由/proc/sys/net/ipv4/tcp_max_syn_backlog指定;在syncookies启用的情况下,逻辑上没有最大值限制,这个设置便被忽略。

也就是说,当前版本的Linux实现了第二种方案,使用两个队列——一个SYN队列,长度系统级别可设置以及一个accept队列长度由应用程序指定。

现在,一个需要考虑的问题是在accept队列已满而一个已完成新连接需要用SYN队列移动到accept队列(收到3次握手中最后一个ACK包),这个实现方案是什么行为。这种情况下,由net/ipv4/tcp_minisocks.ctcp_check_req函数处理:

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

对于IPv4,第一行代码实际上调用的是net/ipv4/tcp_ipv4.c中的tcp_v4_syn_recv_sock函数,代码如下:

    if (sk_acceptq_is_full(sk))
        goto exit_overflow;

可以看到,这里会检查accept队列的长度。如果队列已满,跳到exit_overflow标签执行一些清理工作、更新/proc/net/netstat中的统计项ListenOverflowsListenDrops,最后返回NULL。这会触发tcp_check_req函数跳到listen_overflow标签执行代码。

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

很显然,除非/proc/sys/net/ipv4/tcp_abort_on_overflow被设置为1(这种情况下发送一个RST包),实现什么都没做。

总结一下:Linux内核协议栈在收到3次握手最后一个ACK包,确认一个新连接已完成,而accept队列已满的情况下,会忽略这个包。一开始您可能会对此感到奇怪——别忘了SYN RECEIVED状态下有一个计时器实现:如果ACK包没有收到(或者是我们讨论的忽略),协议栈会重发SYN/ACK包(重试次数由/proc/sys/net/ipv4/tcp_synack_retries决定)。

看以下抓包结果就非常明显——一个客户正尝试连接一个已经达到其最大backlogsocket

  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

由于客户端的TCP实现在收到多个SYN/ACK包时,认为ACK包已经丢失了并且重传它。如果在SYN/ACK重试次数达到限制前,服务端应用从accept队列接收连接,使得backlog减少,那么协议栈会处理这些重传的ACK包,将连接状态从SYN RECEIVED变更到ESTABLISHED并且将其加入accept队列。否则,正如以上包跟踪所示,客户读会收到一个RST包宣告连接失败。

在客户端看来,第一次收到SYN/ACK包之后,连接就会进入ESTABLISHED状态。如果这时客户端首先开始发送数据,那么数据也会被重传。好在TCP有慢启动机制,在服务端还没进入ESTABLISHED之前,客户端能发送的数据非常有限。

相反,如果客户端一开始就在等待服务端,而服务端backlog没能减少,那么最后的结果是连接在客户端看来是ESTABLISHED状态,但在服务端看来是CLOSED状态。这也就是所谓的半开连接。

有一点还没讨论的是:man listen中提到每次收到新SYN包,内核往SYN队列追加一个新连接(除非该队列已满)。事实并非如此,net/ipv4/tcp_ipv4.ctcp_v4_conn_request函数负责处理SYN包,请看以下代码:

    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队列已满的情况下,内核会强制限制SYN包的接收速率。如果有大量SYN包待处理,它们其中的一些会被丢弃。这样看来,就完全依靠客户端重传SYN包了,这种行为跟BSD实现一样。

下结论前,需要再研究以下Linux这种实现方式跟BSD相比有什么优势。Stevens是这样说的:

accept队列已满或者SYN队列已满的情况下,backlog会达到限制。第一种情况经常发生在服务器或者服务器进程非常繁忙的情况下,进程没法足够快地调用accept系统调用从中取出已完成连接。后者是HTTP服务器经常面临的问题,在服务端客户端往返时间非常长的时候(相对于连接到达速率),因为新SYN包在往返时间内都会占据一个连接对象。

大多数情况下accept队列都是空的,因为一旦有一个新连接进入队列,阻塞等待的accept系统调用将返回,然后连接从队列中取出。

Stevens建议的解决方案是简单地调大backlog。但有个问题是,应用程序在调优backlog参数时,不仅需要考虑自身对新连接的处理逻辑,还需要考虑网络状况,包括往返时间等。Linux实现实际上分成两部分:应用程序只负责调解backlog参数,确保accept调用足够快以免accept队列被塞满;系统管理员则根据网络状况调节/proc/sys/net/ipv4/tcp_max_syn_backlog,各司其职。

本文译自:How TCP backlog works in Linux

推荐阅读更多精彩内容