posts

Nov 18, 2018

Linux的TCP backlog如何工作

Estimated Reading Time: 3 minutes (583 words)

本文翻译自How TCP backlog works in Linux

当某个应用程序使用listen系统调用将一个socket置为LISTEN状态时,需要为这个socket设置参数backlog,该参数通常被描述为传入(incoming)连接队列的数量限制。

TCP state diagram

因为TCP使用三步握手(3-way handshake),在一个传入的连接在到达ESTABLISHED状态之前必须经过中间(intermediate)状态SYN RECEIVED,并且可由accept系统调用返回到应用程序(请参阅上面复制的TCP状态图)。这意味着TCP/IP堆栈有两个选项来实现LISTEN状态的socket的积压队列(backlog queue):

  1. 使用大小由listen系统调用backlog参数决定的单队列实现。当某个connection接收SYN分组时,它会发回SYN/ACK分组并将连接入列;当接收到相应的ACK分组时,连接将其状态改变为ESTABLISHED并且有资格切换到应用程序。也就是说,队列中包含两种不同状态——SYN RECEIVEDESTABLISHED,只有后一种状态的connection才能通过accept系统调用返回给应用程序。

  2. 使用一个SYN队列(未完成的连接队列)和一个accept队列(已完成的连接队列)。状态SYN RECEIVED中的connection被添加到SYN队列中,并且当它们的状态变为ESTABLISHED时,即当接收到3次握手中的ACK分组时,移动到accept队列。顾名思义,accept系统调用然后只是为了消费(consume)来自accept队列的连接而实现。在这种情况下,listen系统调用backlog参数确定accept队列的大小。

从历史上看,BSD派生的TCP实现使用第一种选项,意味着当达到最大backlog时,系统将不再发回SYN/ACK分组以响应SYN分组。通常,TCP实现只会丢弃SYN分组(而不是响应RST分组),以便客户端重试。这是W. Richard Stevens的经典教材TCP/IP详解 卷3的第14.5节listen Backlog Queue描述的内容。

请注意,Stevens实际上解释了BSD实现确实使用了两个单独的队列,但它们表现为单个队列,其固定的最大大小由backlog参数确定(但不一定完全等于),即BSD在逻辑上表现如第一个选项所述:

队列限制适用于[…]不完整连接队列上的条目数和[…]已完成连接队列[…]上的条目数之和。

在Linux上,事情是不同的,如listen系统调用手册页中所述:

Linux 2.2修改了TCP socket的backlog参数的行为。现在它指定了等待被accept的完全建立的套接字的队列长度,而不是未完成的连接请求的数量。可以在文件/proc/sys/net/ipv4/tcp_max_syn_backlog中设置未完成的socket队列的长度。

这意味着当前的Linux版本使用具有两个不同队列的第二个选项:具有由系统范围设置指定的大小的SYN队列和具有由应用程序指定的大小的accept队列。

现在有趣的问题是,如果接受队列已满并且需要将连接从SYN队列移动到接受队列,即当接收到3次握手的ACK分组时,这种实现如何表现。这种情况由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;

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

if (sk_acceptq_is_full(sk))
        goto exit_overflow;

此处的代码对accept队列进行了check。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中的TCP实现接收到3次握手的ACK分组并且accept队列已满,它将基本上忽略该分组。乍看起来很奇怪,但是别忘记有一个与SYN RECEIVED状态相关联的定时器:若没有收到ACK分组(或者如果它被忽略,如此处所考虑的情况),那么TCP实现将重新发送SYN / ACK分组(具有由/proc/sys/net/ipv4/tcp_synack_retries指定的重试次数,并使用指数退避算法)。

对于尝试连接(并发送数据)到达已达到其最大backlog的socket的客户端,可以在以下数据包跟踪中看到:

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分组丢失并重新发送(请参阅上面跟踪中带有TCP Dup ACK的行)。若服务器端的应用程序在达到最大SYN/ACK重试次数之前减少了backlog(即从accept队列中消费了一个entry),那么TCP实现最终将处理其中一个重复的ACK,转换状态从SYN RECEIVEDESTABLISHED的connection,并将其添加到accept队列。否则,客户端最终将获得RST分组(如上面显示的示例)。

上述数据包跟踪还显示了此行为的另一个有趣方面。从客户端的角度来看,在接收到第一个SYN/ACK分组后,connection将处于ESTABLISHED状态。如果它发送数据(不先从服务器等待数据),那么也将重传该数据。幸运的是,TCP慢启动应该限制在此阶段发送的段数(the number of segments sent)。

另一方面,如果客户端首先等待来自服务器的数据并且服务器永远不会减少backlog,那么最终结果是在客户端,连接处于ESTABLISHED状态,而在服务器端,connection被视为CLOSED。这意味着我们最终会建立半开连接

还有一个方面我们尚未讨论。来自listen系统调用的手册页的引用表明每个SYN分组都会导致一个connection被添加到SYN队列(除非该队列已满)。事实并非如此。原因在net/ipv4/tcp_ipv4.ctcp_v4_conn_request方法(处理SYN分组)的如下代码中:

/* Accept backlog 已满。如果我们已经在`SYN`队列中排队了足够的热条目,则删除请求。它比使用指数增加超时的openreqs堵塞`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提出以下有趣的观点:

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.

Stevens建议的解决方案只是增加backlog。这样做的问题在于,它假定应用程序需要调整backlog,不仅要考虑它如何处理新建立的传入连接(incoming connection),还要考虑到诸如往返时间(round-trip time)等流量特性(traffic characteristics)的功能。Linux中的实现有效地区分了这两个问题:应用程序只负责调整backlog,以便它可以足够快地调用accept系统调用以避免填充accept队列),然后,系统管理员可以根据流量特征调整/proc/sys/net/ipv4/tcp_max_syn_backlog