环境:
Client
通过
tcp
连接
server
,
server
端只是
listen
,但是不调用
accept
。通过
netstat –ant
查看两端的连接情况。
server端listen,不调用accept。
client一直去connect server。
问题:
运行一段时间后,为什么
server
端的
ESTABLISHED
连接的个数基本是固定的
129
个,但是
client
端的
ESTABLISHED
连接的个数却在不断增加?
分析
Linux内核协议栈为一个tcp连接管理使用两个队列,一个是半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求),一个是accpetd队列(用来保存处于established状态,但是应用层没有调用accept取走的请求)。
第一个队列的长度是/proc/sys/net/ipv4/tcp_max_syn_backlog,默认是1024。如果开启了syncookies,那么基本上没有限制。
第二个队列的长度是/proc/sys/net/core/somaxconn,默认是128,表示最多有129个established链接等待accept。(为什么是129?详见下面的附录1)。
现在假设acceptd队列已经达到129的情况:
client发送syn到server。client(SYN_SENT),server(SYN_RECV)
server端处理流程:tcp_v4_do_rcv--->tcp_rcv_state_process--->tcp_v4_conn_request
if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
goto drop;
inet_csk_reqsk_queue_yong(sk)的含义是请求队列中有多少个握手过程中没有重传过的段。
在第一次的时候,之前的握手过程都没有重传过,所以这个syn包server端会直接drop掉,之后client会重传syn,当inet_csk_reqsk_queue_yong(sk) < 1,那么这个syn被server端接受。server会回复synack给client。这样一来两边的状态就变为client(ESTABLISHED), server(SYN_SENT)
Client收到synack后回复ack给server。
server端处理流程: tcp_check_req--->syn_recv_sock-->tcp_v4_syn_recv_sock
if(sk_acceptq_is_full(sk)
goto exit_overflow;
如果server端设置了sysctl_tcp_abort_on_overflow,那么server会发送rst给client,并删除掉这个链接;否则server端只是记录一下LINUX_MIB_LISTENOVERFLOWS(详见附录2),然后返回。默认情况下是不会设置的,server端只是标记连接请求块的acked标志,之后连接建立定时器,会遍历半连接表,重新发送synack,重复上面的过程(具体的函数是inet_csk_reqsk_queue_prune),如果重传次数超过synack重传的阀值(/proc/sys/net/ipv4/tcp_synack_retries),会把该连接从半连接链表中删除。
一次异常问题分析
Nginx
通过
FASTCGI
协议连接
cgi
程序,出现
cgi
程序
read
读取
socket
内容的时候永远
block
。通过
netstat
查看,
cgi
程序所在的服务器上显示连接存在,但是
nginx
所在的服务器上显示不存在该连接。
下面是原始数据图:
我们从上面的数据流来分析一下:
出现问题的时候,
cgi
程序(
tcp server
端)处理非常慢,导致大量的连接请求放到
accept
队列,把
accept
队列阻塞。
148021 nginx(tcp client
端
)
连接
cgi
程序,发送
syn
此时
server
端
accpet
队列已满,并且
inet_csk_reqsk_queue_yong(sk) > 1
,
server
端直接丢弃该数据包
148840 client
端等待
3
秒后,重传
SYN
此时
server
端状态与之前送变化,仍然丢弃该数据包
150163 client
端又等待
6
秒后,重传
SYN
此时
server
端
accept
队列仍然是满的,但是存在了重传握手的连接请求,
server
端接受连接请求,并发送
synack
给
client
端(
150164
)
150166 client
端收到
synack
,标记本地连接为
ESTABLISHED
状态,给
server
端应答
ack
,
connect
系统调用完成。
Server
收到
ack
后,尝试将连接放到
accept
队列,但是因为
accept
队列已满,所以只是标记连接为
acked
,并不会将连接移动到
accept
队列中,也不会为连接分配
sendbuf
和
recvbuf
等资源。
150167 client
端的应用程序,检测到
connect
系统调用完成,开始向该连接发送数据。
Server
端收到数据包,由于
acept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回。
150225 client
端由于没有收到刚才发送数据的
ack
,所以会重传刚才的数据包
150296
同上
150496
同上
150920
同上
151112 server
端连接建立定时器生效,遍历半连接链表,发现刚才
acked
的连接,重新发送
synack
给
client
端。
151113 client
端收到
synack
后,根据
ack
值,使用
SACK
算法,只重传最后一个
ack
内容。
Server
端收到数据包,由于
accept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回。
151896 client
端等待
3
秒后,没有收到对应的
ack
,认为之前的数据包也丢失,所以重传之前的内容数据包。
152579 server
端连接建立定时器生效,遍历半连接链表,发现刚才
acked
的连接,
synack
重传次数在阀值以内,重新发送
synack
给
client
端。
152581 cient
端收到
synack
后,根据
ack
值,使用
SACK
算法,只重传最后一个
ack
内容。
Server
端收到数据包,由于
accept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回
153455 client
端等待
3
秒后,没有收到对应的
ack
,认为之前的数据包也丢失,所以重传之前的内容数据包。
155399 server
端连接建立定时器生效,遍历半连接链表,发现刚才
acked
的连接,
synack
重传次数在阀值以内,重新发送
synack
给
client
端。
155400 cient
端收到
synack
后,根据
ack
值,使用
SACK
算法,只重传最后一个
ack
内容。
Server
端收到数据包,由于
accept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回。
156468 client
端等待几秒后,没有收到对应的
ack
,认为之前的数据包也丢失,所以重传之前的内容数据包。
161309 server
端连接建立定时器生效,遍历半连接链表,发现刚才
acked
的连接,
synack
重传次数在阀值以内,重新发送
synack
给
client
端。
161310 cient
端收到
synack
后,根据
ack
值,使用
SACK
算法,只重传最后一个
ack
内容。
Server
端收到数据包,由于
accept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回。
162884 client
端等待几秒后,没有收到对应的
ack
,认为之前的数据包也丢失,所以重传之前的内容数据包。
Server
端收到数据包,由于
accept
队列仍然是满的,所以
server
端处理也只是标记
acked
,然后返回。
164828 client
端等待一段时间后,认为连接不可用,于是发送
FIN
、
ACK
给
server
端。
Client
端的状态变为
FIN_WAIT1
,等待一段时间后,
client
端将看不到该链接。
164829 server
端收到
ACK
后,此时
cgi
程序处理完一个请求,从
accept
队列中取走一个连接,此时
accept
队列中有了空闲,
server
端将请求的连接放到
accept
队列中。
这样
cgi
所在的服务器上显示该链接是
established
的,但是
nginx(client
端
)
所在的服务器上已经没有该链接了。
之后,当
cgi
程序从
accept
队列中取到该连接后,调用
read
去读取
sock
中的内容,但是由于
client
端早就退出了,所以
read
就会
block
那里了。
问题解决
或许你会认为在
164829
中,
server
端不应该建立连接,这是内核的
bug
。但是内核是按照
RFC
来实现的,在
3
次握手的过程中,是不会判断
FIN
标志位的,只会处理
SYN
、
ACK
、
RST
这三种标志位。
从应用层的角度来考虑解决问题的方法,那就是使用非阻塞的方式
read
,或者使用
select
超时方式
read
;亦或者
nginx
中关闭连接的时候使用
RST
方式,而不是
FIN
方式。
附录1
when I use linux TCP socket, and find there is a bug in function sk_acceptq_is_full()
:
When a new SYN comes, TCP module first checks its validation. If valid,send SYN,ACK to the client and add the sock
to the syn hash table.
Next time if received the valid ACK for SYN,ACK from the client. server will accept this connection and increase the
sk->sk_ack_backlog -- which is done in function tcp_check_req().
We check wether acceptq is full in function tcp_v4_syn_recv_sock().
Consider an example:
After listen(sockfd, 1) system call, sk->sk_max_ack_backlog is set to
As we know, sk->sk_ack_backlog is initialized to 0. Assuming accept() system call is not invoked now
1. 1st connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=0 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
2. 2nd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=1 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
3. 3rd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=2 sk->sk_max_ack_backlog=1, function return 1. Refuse this connection.
I think it has bugs. after listen system call. sk->sk_max_ack_backlog=1
but now it can accept 2 connections.
附录2
netstat -s
cat /proc/net/netstat
最后感谢Tiger提供的测试数据
转自:http://blog.chinaunix.net/uid-20662820-id-4154399.html
http://jm.taobao.org/2017/05/25/525-1/
关于TCP 半连接队列和全连接队列
发表于 2017-05-25 | 作者 蛰剑 | 分类于 网络 |
最近碰到一个client端连接异常问题,然后定位分析并查阅各种资料文章,对TCP连接队列有个深入的理解
查资料过程中发现没有文章把这两个队列以及怎么观察他们的指标说清楚,希望通过这篇文章能把他们说清楚一点
问题描述
JAVA的client和server,使用socket通信。server使用NIO。
1.间歇性的出现client向server建立连接三次握手已经完成,但server的selector没有响应到这连接。
2.出问题的时间点,会同时有很多连接出现这个问题。
3.selector没有销毁重建,一直用的都是一个。
4.程序刚启动的时候必会出现一些,之后会间歇性出现。
分析问题
正常TCP建连接三次握手过程:
image.png
- 第一步:client 发送 syn 到server 发起握手;
- 第二步:server 收到 syn后回复syn+ack给client;
- 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack(此时client的56911端口的连接已经是established)
从问题的描述来看,有点像TCP建连接的时候全连接队列(accept队列)满了,尤其是症状2、4. 为了证明是这个原因,马上通过 ss -s 去看队列的溢出统计数据:
667399 times the listen queue of a socket overflowed
反复看了几次之后发现这个overflowed 一直在增加,那么可以明确的是server上全连接队列一定溢出了
接着查看溢出后,OS怎么处理:
# 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握手过程中建连接的流程和队列
(图片来源: http://www.cnxct.com/something-about-phpfpm-s-backlog/)
如上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)
三次握手中,在第一步server收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client(第二步);
比如syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列满其它正常请求无法进来
第三步的时候server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出相关信息放入到全连接队列中,否则按tcp_abort_on_overflow指示的执行。
这时如果全连接队列满了并且tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,就很容易异常了。
在我们的os中retry 第二步的默认次数是2(centos默认是5次):
net.ipv4.tcp_synack_retries = 2
如果TCP连接队列溢出,有哪些指标可以看呢?
上述解决过程有点绕,那么下次再出现类似问题有什么更快更明确的手段来确认这个问题呢?
netstat -s
[root@server ~]# netstat -s | egrep "listen|LISTEN"
667399 times the listen queue of a socket overflowed
667399 SYNs to LISTEN sockets ignored
比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。
ss 命令
[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级别的系统参数
半连接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。 不同版本的os会有些差异
实践验证下上面的理解
把java中backlog改成10(越小越容易溢出),继续跑压力,这个时候client又开始报异常了,然后在server上通过 ss 命令观察到:
Fri May 5 13:50:23 CST 2017
Recv-Q Send-QLocal Address:Port Peer Address:Port
11 10 *:3306 *:*
按照前面的理解,这个时候我们能看到3306这个端口上的服务全连接队列最大是10,但是现在有11个在队列中和等待进队列的,肯定有一个连接进不去队列要overflow掉
容器中的Accept队列参数
Tomcat默认短连接,backlog(Tomcat里面的术语是Accept count)Ali-tomcat默认是200, Apache Tomcat默认100.
#ss -lnt
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 100 *:8080 *:*
Nginx默认是511
$sudo ss -lnt
State Recv-Q Send-Q Local Address:PortPeer Address:Port
LISTEN 0 511 *:8085 *:*
LISTEN 0 511 *:8085 *:*
因为Nginx是多进程模式,也就是多个进程都监听同一个端口以尽量避免上下文切换来提升性能
进一步思考
如果client走完第三步在client看来连接已经建立好了,但是server上的对应连接实际没有准备好,这个时候如果client发数据给server,server会怎么处理呢?(有同学说会reset,还是实践看看)
先来看一个例子:
image.png
(图片来自: http://blog.chinaunix.net/uid-20662820-id-4154399.html)
如上图,150166号包是三次握手中的第三步client发送ack给server,然后150167号包中client发送了一个长度为816的包给server,因为在这个时候client认为连接建立成功,但是server上这个连接实际没有ready,所以server没有回复,一段时间后client认为丢包了然后重传这816个字节的包,一直到超时,client主动发fin包断开该连接。
这个问题也叫client fooling,可以看这里: https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071 (感谢浅奕的提示)
从上面的实际抓包来看不是reset,而是server忽略这些包,然后client重传,一定次数后client认为异常,然后断开连接。
过程中发现的一个奇怪问题
[root@server ~]# date; netstat -s | egrep "listen|LISTEN"
Fri May 5 15:39:58 CST 2017
1641685 times the listen queue of a socket overflowed
1641685 SYNs to LISTEN sockets ignored
[root@server ~]# date; netstat -s | egrep "listen|LISTEN"
Fri May 5 15:39:59 CST 2017
1641906 times the listen queue of a socket overflowed
1641906 SYNs to LISTEN sockets ignored
如上所示:
overflowed和ignored居然总是一样多,并且都是同步增加,overflowed表示全连接队列溢出次数,socket ignored表示半连接队列溢出次数,没这么巧吧。
翻看内核源代码( http://elixir.free-electrons.com/linux/v3.18/source/net/ipv4/tcp_ipv4.c):
image.png
可以看到overflow的时候一定会drop++(socket ignored),也就是drop一定大于等于overflow。
同时我也查看了另外几台server的这两个值来证明drop一定大于等于overflow:
server1
150 SYNs to LISTEN sockets dropped
server2
193 SYNs to LISTEN sockets dropped
server3
16329 times the listen queue of a socket overflowed
16422 SYNs to LISTEN sockets dropped
server4
20 times the listen queue of a socket overflowed
51 SYNs to LISTEN sockets dropped
server5
984932 times the listen queue of a socket overflowed
988003 SYNs to LISTEN sockets dropped
那么全连接队列满了会影响半连接队列吗?
来看三次握手第一步的源代码( http://elixir.free-electrons.com/linux/v2.6.33/source/net/ipv4/tcp_ipv4.c#L1249):
image.png
TCP三次握手第一步的时候如果全连接队列满了会影响第一步drop 半连接的发生。大概流程的如下:
tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlog队列已满,且未超时的request socket的数量大于1,则丢弃当前请求
if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
goto drop;
总结
全连接队列、半连接队列溢出这种问题很容易被忽视,但是又很关键,特别是对于一些短连接应用(比如Nginx、PHP,当然他们也是支持长连接的)更容易爆发。 一旦溢出,从cpu、线程状态看起来都比较正常,但是压力上不去,在client看来rt也比较高(rt=网络+排队+真正服务时间),但是从server日志记录的真正服务时间来看rt又很短。
希望通过本文能够帮大家理解TCP连接过程中的半连接队列和全连接队列的概念、原理和作用,更关键的是有哪些指标可以明确看到这些问题。
另外每个具体问题都是最好学习的机会,光看书理解肯定是不够深刻的,请珍惜每个具体问题,碰到后能够把来龙去脉弄清楚。
参考文章:
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
企业级互联网架构Aliware,让您的业务能力云化: https://www.aliyun.com/aliware