游戏后台开发
分类: LINUX
2014-03-21 14:14:03
原文地址:Linux协议栈accept和syn队列问题 作者:瀚海书香
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方式。
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.
netstat -s
cat
/proc/net/netstat
最后感谢Tiger提供的测试数据