关于TCP 半连接队列和全连接队列

问题描述
JAVA的client和server,使用socket通信。server使用NIO。

  1. 间歇性的出现client向server建立连接三次握手已经完成,但server的selector没有响应到这连接。
  2. 出问题的时间点,会同时有很多连接出现这个问题。
  3. selector没有销毁重建,一直用的都是一个。
  4. 程序刚启动的时候必会出现一些,之后会间歇性出现。

分析问题
正常TCP建连接三次握手过程:

在这里插入图片描述

  • 第一步:client 发送 syn 到server 发起握手;
  • 第二步:server 收到 syn后回复syn+ack给client;
  • 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack。(此时client的56911端口的连接已经是established)

从问题的描述来看,有点像TCP建连接的时候全连接队列(accept队列)满了,尤其是症状2、4。为了证明是这个原因,马上通过 netstat -s | egrep “listen” 去看队列的溢出统计数据:

1
667399 times the listen queue of a socket overflowed

反复看了几次之后发现socket overflowed一直在增加,那么可以明确的是server上全连接队列一定溢出了。

接着查看溢出后,OS怎么处理:

1
2
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

tcp_abort_on_overflow 为0表示如果三次握手第三步的时候全连接队列满了,那么server扔掉client 发过来的ack(在server端认为连接还没建立起来)。

为了证明客户端应用代码的异常跟全连接队列满有关系,先把tcp_abort_on_overflow修改成 1,1表示第三步的时候如果全连接队列满了,server发送一个reset包给client,表示废掉这个握手过程和这个连接(本来在server端这个连接就还没建立起来)。接着测试然后在客户端异常中可以看到很多connection reset by peer的错误,到此证明客户端错误是这个原因导致的。

java 源代码中socket 默认的backlog(这个值控制全连接队列的大小)是50,于是改大重新跑,经过12个小时以上的压测,这个错误一次都没出现过,同时 overflowed 也不再增加了。
到此问题解决,简单来说TCP三次握手后有个accept队列,进到这个队列才能从Listen变成accept,默认backlog 值是50,很容易就满了。满了之后握手第三步的时候server就忽略了client发过来的ack包(隔一段时间server重发握手第二步的syn+ack包给client),如果这个连接一直排不上队就异常了。

深入理解TCP握手过程中建连接的流程和队列

img

如上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)。

三次握手中,在第一步server收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client(第二步),此时server处于SYN_RCVD状态。

syn floods攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列满其它正常请求无法进来;syns queue的长度由/proc/sys/net/ipv4/tcp_max_syn_backlog设置。

第三步的时候server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出相关信息放入到全连接队列中,表示连接建立,进入ESTABLISHED状态,否则按tcp_abort_on_overflow指示的执行。

这时如果全连接队列满了并且tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,就很容易异常了;accept queue的长度取值min(backlog, somaxconn)

retry 第二步的配置项:

1
net.ipv4.tcp_synack_retries = 2

如果TCP连接队列溢出,有哪些指标可以看呢?

netstat –s

1
2
3
[root@server ~]#  netstat -s | egrep "listen|LISTEN" 
667399 times the listen queue of a socket overflowed
667399 SYNs to LISTEN sockets ignored

比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。

overflowed表示全连接队列溢出的次数,

sockets dropped表示半连接队列溢出的次数。

ss

1
2
3
[root@server ~]# ss -lnt
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 50 *:3306 *:*

上面看到的第二列Send-Q 表示第三列的listen端口上的全连接队列最大为50,第一列Recv-Q为全连接队列当前使用了多少

全连接队列的大小取决于:min(backlog, somaxconn)

backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数/proc/sys/net/core/somaxconn

半连接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)

不同版本的os会有些差异。

netstat 命令
netstat跟ss命令一样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数

1
2
3
4
5
6
$netstat -tn  
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp0 0 server:8182 client-1:15260 SYN_RECV
tcp0 28 server:22 client-1:51708 ESTABLISHED
tcp0 0 server:2376 client-1:60269 ESTABLISHED

netstat -tn 看到的 Recv-Q 跟全连接半连接没有关系,这里特意拿出来说一下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆。

Recv-Q 和 Send-Q 的说明

Recv-Q
Established: The count of bytes not copied by the user program connected to this socket.
Listening: Since Kernel 2.6.18 this column contains the current syn backlog.

Send-Q
Established: The count of bytes not acknowledged by the remote host.
Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.

通过 netstat 发现问题的案例
自身太慢,比如如下netstat -t 看到的Recv-Q有大量数据堆积,那么一般是CPU处理不过来导致的:

image.png

下面的case是接收方太慢,从应用机器的netstat统计来看,也是压力端回复太慢(本机listen 9108端口)

image.png

send-q表示回复从9108发走了,没收到对方的ack,基本可以推断PTS到9108之间有瓶颈

上面是通过一些具体的工具、指标来认识全连接队列(工程效率的手段)

实践验证一下上面的理解
把java中backlog改成10(越小越容易溢出),继续跑压力,这个时候client又开始报异常了,然后在server上通过 ss 命令观察到:

1
2
3
Fri May  5 13:50:23 CST 2017
Recv-Q Send-QLocal Address:Port Peer Address:Port
11 10 *:3306 *:*

按照前面的理解,这个时候我们能看到3306这个端口上的服务全连接队列最大是10,但是现在有11个在队列中和等待进队列的,肯定有一个连接进不去队列要overflow掉,同时也确实能看到overflow的值在不断地增大。

Tomcat和Nginx中的Accept队列参数
Tomcat默认短连接,backlog(Tomcat里面的术语是Accept count)Ali-tomcat默认是200, Apache Tomcat默认100.

1
2
3
#ss -lnt
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 100 *:8080 *:*

Nginx默认是511

1
2
3
4
$sudo ss -lnt
State Recv-Q Send-Q Local Address:PortPeer Address:Port
LISTEN 0 511 *:8085 *:*
LISTEN 0 511 *:8085 *:*

因为Nginx是多进程模式,所以看到了多个8085,也就是多个进程都监听同一个端口以尽量避免上下文切换来提升性能

其它问题
全连接队列满了会影响半连接队列吗?

TCP三次握手第一步的时候如果全连接队列满了会影响第一步半连接的发生。

大概流程的如下:

1
2
3
4
tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlogk队列已满,且未超时的request socket的数量大于1,则丢弃当前清求
if(sk_acceptq_is_full(sk)&inet_csk_reqsk_queue_yong(sk)>1)
goto drop;

如果client走完了TCP握手的第三步,在client看来连接已经建立好了,但是server上的对应连接实际没有准备好,这个时候如果client发数据给server,server会怎么处理呢?

clienti认为连接建立成功,但是server上这个连接实际没有ready,所以serveri没有回复,一段时间后clienti认为丢包了然后重传这个包,一直到超时,client主动发fin包,断开该连接。
这个问题也叫client fooling,可以看这里:tcp/dccp:drop SYN packets if accept queue is full


转载自:

https://blog.csdn.net/jiankunking/article/details/132487950

更多阅读:

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

http://www.cnblogs.com/zengkefu/p/5606696.html

http://www.cnxct.com/something-about-phpfpm-s-backlog/

http://jaseywang.me/2014/07/20/tcp-queue-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%97%AE%E9%A2%98/

http://jin-yang.github.io/blog/network-synack-queue.html#

http://blog.chinaunix.net/uid-20662820-id-4154399.html

https://www.atatech.org/articles/12919

https://www.cnblogs.com/xiaolincoding/p/12995358.html