TCP三次握手中的全连接与半连接队列

前言

在使用第三方服务的过程中,网络偶尔会超时,因些深入调研了下网络超时的具体原因,在此过程中了解到了TCP的全连接队列与半连接队列,以及如何查看队列是否溢出以及溢出的解决方案

三次握手全貌

学会计算机网络基础的同学应该都知道三次握手,四次握手,三次握手过程中存在全连接队列与半连接队列,整体流程如下

由上图可以看出

  • Server端需要先调用bind()方法,绑定ip和端口号,再调用listen()方法,然后就可以等待来自Client连接了

  • Client 调用connect()后,就会发送SYN到Server,此时Client端处理SYN_SENT状态

  • Server收到ACK后,Server进入SYN_RCVD,回复SYN + ACK,此时该socket放入半连接队列(SYN QUEUE)中。(这里要侧重说明一下,这里并不是说从Listen状态变成了SYN_RCVD,而是会生成一个新的(或者复用旧的)socket,新socket状态会变成SYN_RCVD,Server端的原来监听的socket状态还依然是LISTEN状态)

  • Client 收到Server发回的ACK后,会再发送一次ACK到Server,Server收到ACK后三次连接建立,此时会把该socket从半连接队列中取出来放入全连接队列(ACCEPTED QUEUE)中,此时Client与Server端全部处理ESTABLISHED状态

  • Server端调用accept()方法后,该连接会从全连接队列移出,交给应用层处理。

接下来会先介绍全连接队列,后介绍半连接队列,一是全连接队列更容易,二是当全连接队列满了后,会影响半连接队列的表现。

全连接队列

原理部分

再次强调下全连接队列的含义:全连接队列是指已经完成三次握手,Server还未调用accept()函数的,即还没有交给应用层的socket的队列。即如果发起的连接速度大于应用程序可以处理的速度,那么全连接队列即会增长。

全连接的队列长度限制因素

全连接队列是有长度限制的,限制参数有两个

  • 一是net.core.somaxconn,即/proc/sys/net/core/somaxcon 文件中的值,默认为128,长度限制是 min(net.core.somaxconn, backlog), 此时net.core.somaxconn/proc/sys/net/core/somaxcon 文件中

  • 二是backlog,即为listen时传入的参数,比如java中ServeSocket 的构造函数

    1
    2
    3
    4
    // ServerSocket.java 文件,此处的backlog即为限制的队列长度
    public ServerSocket(int port, int backlog) throws IOException {
    this(port, backlog, null);
    }

最终队列的长度qlen = min(net.core.somaxconn, backlog),即取两者中的最小值。

查看全连接队列

我们可以通过 ss -ltn 命令来查看全连接的的情况,举个例子

ss 命令中, -l是指只查看LISTEN的状态的连接, -t 是指查看 tcp协议的连接,-n表示就用数字呈现就可以,如果不指定-n的话,截图中的 port字段会显示 ssh

当连接在LISTEN状态下时, Send-Q表示最大的全连接队列长度,Recv-Q表示当前全连接队列的长度,上述可以看到,ssh的的全连接的最大长度为128。再次强调,此外的128并不是说最多只能有128个请求,而是说最多只能有129(下面会介绍)完成了三次握手但是还没有被ssh程序处理的连接

实验验证

我们来实验验证一下,server端的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket socket = new ServerSocket(8080, 50);
while(true) {
TimeUnit.HOURS.sleep(1);
}
}
}

/proc/sys/net/core/somaxcon 使用默认值,即128,即按上述的定义,我们最终的全连接队列长度为 50。
客户端的的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
new Socket("39.105.125.58", 8080);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
TimeUnit.HOURS.sleep(1);
}

来查看Server端的全连接情况,采用命令 ss -ltn, 结果如下

我们可以看到,我们开启的8080端口的服务的全连接最大长度为50,当前全连接的队列的长度为51(51 是因为最新的一个连接加上队列中的50个连接),并且我们发现Recv-Q不会再增长。

另外,我们可以通过netstat -s 查看溢出的socket的溢出情况,执行前与执行后的结果如下,可以看到有(133-84=)49个socket由于溢出被drop掉了,等于100(客户端的请求数)-51(全连接的长度)。

在上面讲到ack_backlog = min(net.core.somaxconn, backlog),上面的例子中的全连接队列长度受backlog受制,当backlog大于net.core.somaxconn后,队列的最大长度即为net.core.somaxconn,这里不再展开,有兴趣可以自行测试。

全连接队列满了怎么办

如果请求过快,而应用程序处理太慢,就会导致全连接队列增长,直到达到最大长度,那全连接队列满了以后会怎么办呢,这里依赖参数/proc/sys/net/ipv4/tcp_abort_on_overflow 的控制,取值有 0或者1,

  • 如果为1时,则表示直接abort,则回复RST 给client端,此时client端会收到Connection reset by peer 错误。(上述的实验中,server端该值为1)。
  • 如果值为0时,则直接把第三次握手的ACK丢弃,后续等待client重发ack,则后续sever端会响应或者client重发ACK超时。

半连接队列

半连接队列并没有像查看全连接队列长度那样的查看半连接的长度,并且全连接队列满了的话,如果没有开启tcp_syncookies(后面介绍),则也会直接drop掉相应的请求,即全连接队列长度也会影响半连接的数量。不同的内核版本对于半连接队列的实现可能是不一样的。半连接队列并不是一个真实的队列,实际上是通过一个hash表实现的,可参考源码地址
这里只阐述一下如何查看半连接队列是否有溢出以及如何解决半连接队列溢出的情况。

半连接队列溢出确认方法

执行 netstat -s | grep -i dropped | grep -i listen, 可以看到SYN丢弃的次数,执行多次,并且该值继续上升的话,即说明当前半连接队列溢出了。

半连接队列溢出解决方案

半连接队列溢出后,可以调整两个参数

  • 修改/proc/sys/net/ipv4/tcp_synack_retries 值,该值表示是Server收到SYN后发送ACK+SYN的重试次数,如果网络情况不好,减少重试次数的话可以快速失败,减少半队列长度,不过另外一方面也可能会导致连接失败数增加
  • 修改/proc/sys/net/ipv4/tcp_syncookies,打开cookies后,当Server端收到SYN后,会计算一个cookies返给Client端,Client端发送该cookies和ACK从而完成三次握手。该值在应对SYN攻击情况下会十分有效。该值的取值有三种情况
    • 0:表示不启用cookies,即当半连接队列溢出后,直接丢弃SYN
    • 1:表示当半连接队列满了后,开启cookies
    • 2:无论何时都会开启cookies

总结

本文介绍了全连接队列,半连接队列溢出的查看方案及解决思路,对于java开发者来者往往不关心这该部分内容,但是当有队列溢了后,Server端从cpu、线程状态看起来都比较正常,server端的响应时间也很短,但是在Client看来rt也比较高(rt=网络+排队+真正服务时间),希望本文可以帮助大家理解半连接列与全连接队列,在遇到类似问题后,有更多的方案去查看和处理相应的问题。