socket调试


现象1:客户端为FIN_WAIT1,服务端为ESTABLISHED

/proc/sys/net/ipv4/tcp_abort_on_overflow

值设置成1,那么connect就会被rerest。

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

       此时serveraccpet队列已满,并且inet_csk_reqsk_queue_yong(sk) > 1server端直接丢弃该数据包

148840 client端等待3秒后,重传SYN

       此时server端状态与之前送变化,仍然丢弃该数据包

150163 client端又等待6秒后,重传SYN

                   此时serveraccept队列仍然是满的,但是存在了重传握手的连接请求,server端接受连接请求,并发送synackclient端(150164

150166 client端收到synack,标记本地连接为ESTABLISHED状态,给server端应答ackconnect系统调用完成。

                   Server收到ack后,尝试将连接放到accept队列,但是因为accept队列已满,所以只是标记连接为acked,并不会将连接移动到accept队列中,也不会为连接分配sendbufrecvbuf等资源。

150167 client端的应用程序,检测到connect系统调用完成,开始向该连接发送数据。

                   Server端收到数据包,由于acept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

150225 client端由于没有收到刚才发送数据的ack,所以会重传刚才的数据包

150296 同上

150496 同上

150920 同上

151112 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,重新发送synackclient端。

151113 client端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

151896 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

152579 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

152581 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回

153455 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

155399  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

155400 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

156468 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

161309  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

161310 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

162884 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

164828 client端等待一段时间后,认为连接不可用,于是发送FINACKserver端。Client端的状态变为FIN_WAIT1,等待一段时间后,client端将看不到该链接。

164829 server端收到ACK后,此时cgi程序处理完一个请求,从accept队列中取走一个连接,此时accept队列中有了空闲,server端将请求的连接放到accept队列中。

这样cgi所在的服务器上显示该链接是established的,但是nginx(client)所在的服务器上已经没有该链接了。

之后,当cgi程序从accept队列中取到该连接后,调用read去读取sock中的内容,但是由于client端早就退出了,所以read就会block那里了。


先来回顾下三次握手里面涉及到的问题:

1. 当 client 通过 connect 向 server 发出 SYN 包时,client 会维护一个 socket 等待队列,而 server 会维护一个 SYN 队列

2. 此时进入半链接的状态,如果 socket 等待队列满了,server 则会丢弃,而 client 也会由此返回 connection time out;只要是 client 没有收到 SYN+ACK,3s 之后,client 会再次发送,如果依然没有收到,9s 之后会继续发送

3. 半连接 syn 队列的长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)  决定

4. 当 server 收到 client 的 SYN 包后,会返回 SYN, ACK 的包加以确认,client 的 TCP 协议栈会唤醒 socket 等待队列,发出 connect 调用

5. client 返回 ACK 的包后,server 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 的含义请看这里。需要注意的是, 一些 Linux 的发型版本可能存在对 somaxcon 错误 truncating 方式 。

6. 当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被相应,此时,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回,0 表示直接丢丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。上面说的只是些理论,如果服务器不及时的调用 accept(),当 queue 满了之后,服务器并不会按照理论所述,不再对 SYN 进行应答,返回 ETIMEDOUT。根据 这篇 文档的描述,实际情况并非如此,服务器会随机的忽略收到的 SYN,建立起来的连接数可以无限的增加,只不过客户端会遇到延时以及超时的情况。

可以看到,整个 TCP stack 有如下的两个 queue:

1. 一个是 half open(syn queue) queue(max(tcp_max_syn_backlog, 64)),用来保存 SYN_SENT 以及 SYN_RECV 的信息。

2. 另外一个是 accept queue(min(somaxconn, backlog)),保存 ESTAB 的状态,但是调用 accept()。


使用 ss 获取到的 Recv-Q/Send-Q 在 LISTEN 状态以及非 LISTEN 状态所表达的含义是不同的。从tcp_diag.c 源码 中可以看到二者的区别:

LISTEN 状态: Recv-Q 表示的当前等待服务端调用 accept 完成三次握手的 listen backlog 数值,也就是说,当客户端通过 connect() 去连接正在 listen() 的服务端时,这些连接会一直处于这个 queue 里面直到被服务端 accept();Send-Q 表示的则是最大的 listen backlog 数值,这就就是上面提到的 min(backlog, somaxconn) 的值。 其余状态: 非 LISTEN 状态之前理解的没有问题。Recv-Q 表示 receive queue 中的 bytes 数量;Send-Q 表示 send queue 中的 bytes 数值。

要理解上面总结的这些,可以参见下这两个案例( 1 , 2 )。 

通过 "SYNs to LISTEN sockets dropped" 以及 "times the listen queue of a socket overflowed" 这两个 netstat -s 获取到的 TCP 状态,可以很快的发现系统存在的一些问题。 任何一个包含 "dropped" 或者 "overflowed" 并且数值一直居高不下的 metric 从字面含义理解来看,都不是一个好现象。

对于 Nginx 来说,backlog 的默认值为 511,这个可以通过 ss/netstat 的 Send-Q 确认:

State      Recv-Q Send-Q        Local Address:Port          Peer Address:Port

LISTEN     0      511                       *:80                       *:*     

可以通过适当的增大 nginx 的 backlog 以及 somaxconn 来增大队列: listen 80 backlog=1638


今天编程序的偶然遇到了一点问题,然后小研究了一下,发现一些以前不知道的事情,还有点小吃惊 ,记录一下。有些观点可能有问题,求指正。

我们都知道listen参数有个参数backlog。如果服务器不能及时调用accept,把连接从listen queue里面取走,那么UNP告诉我们,服务器的listen queue满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的connect就会返回ETIMEDOUT。但实际上Linux的行为不是这样的!

让一个程序在8000端口上listen,backlog值是n,不调用accept;再让一个客户端程序不停的调用connect。客户端的前n个connect调用立刻就成功返回,这是意料之中的。按照UNP的说法,以后再调用connect应该统统超时失败,但实际上我看到的是:有的connect超时失败了,有的立刻成功返回了,有的经过明显延迟后成功返回了。用这个命令观看:
watch "netstat -t -n | grep 8000 | grep -oP '\w+\s*$' | sort | uniq -c"
发现建立的连接数是可以无限增加的!再用tcpdump看,发现当客户的第一个syn发出后,服务器会随机的选择是否发送syn/ack,也就是tcp握手的第二步。所以对connect行为的解释就是:如果这次调用connect的第一个syn就被syn/ack了,那么connect就会立刻成功;如果第一个syn被忽略了,而地二个syn被服务器应答了,那么connect就会延迟成功;如果不幸三个syn都被服务器忽略了,connect就会返回超时失败。我google了半天,并没有发现任何文档描述这种行为,如果要确证只能看源码了。

结论:tcp的listen queue满后,linux不会如书中所说的拒绝连接,而是会随机的忽略发起连接的syn,建立起来的连接数可以无限增加,但是客户端会随机的遇到延迟和超时。

不管是拒绝连接还是随机的不理会连接请求,都是我们不愿意看到的,所以作为服务器工程师我们要监控这种现象。怎么在服务端看到呢?有两种方法,一是用netstat:
$ netstat -s | grep listen
    21391 times the listen queue of a socket overflowed
如果这个计数不停的增加就是有麻烦了。
令一种方法是针对单个listen socket的。调用getsockopt,传入TCP_INFO,返回一个tcp_info结构,这个结构中的成员tcpi_sacked是listen queue的大小,即传人listen的backlog值;成员tcpi_unacked是listen queue里连接的数量,如果tcp_unacked > tcpi_sacked,那就应该注意啦。具体调用方法用一个python程序说明:

import time
import socket
import struct

HOST = 'localhost'
PORT = 8000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(88)

#/usr/include/linux/tcp.h
struct_tcp_info = struct.Struct('7B 24I')

while True:
    buf = s.getsockopt(socket.IPPROTO_TCP,
                       socket.TCP_INFO,
                       1024)
    tcp_info = struct_tcp_info.unpack(buf)
    tcpi_unacked, tcpi_sacked = tcp_info[11:13]
         
    print 'tcpi_unacked:', tcpi_unacked,\
        'tcpi_sacked:', tcpi_sacked

    time.sleep(1)
可以看到刚开始运行时候输出是:
tcpi_unacked: 0 tcpi_sacked: 88
不断的往8000端口建立连接,到后来输出就成了:
tcpi_unacked: 89 tcpi_sacked: 88
这就表示listen queue已经满了。此时上面netstat报告的overflow计数也开始不停增加了。




现象2:netstat -antp观察服务端的socket,Recv-Q堆积的buffer越来越大

这个现象直接导致了客户端的socket连接timeout的比例越来越大

调试过程:

刚开始用netstat观察套接字Recv-Q为0,同时用tcpdump -s0 -S -xx 观察包体

现象1:客户端为FIN_WAIT1,服务端为ESTABLISHED

/proc/sys/net/ipv4/tcp_abort_on_overflow

值设置成1,那么connect就会被rerest。

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

       此时serveraccpet队列已满,并且inet_csk_reqsk_queue_yong(sk) > 1server端直接丢弃该数据包

148840 client端等待3秒后,重传SYN

       此时server端状态与之前送变化,仍然丢弃该数据包

150163 client端又等待6秒后,重传SYN

                   此时serveraccept队列仍然是满的,但是存在了重传握手的连接请求,server端接受连接请求,并发送synackclient端(150164

150166 client端收到synack,标记本地连接为ESTABLISHED状态,给server端应答ackconnect系统调用完成。

                   Server收到ack后,尝试将连接放到accept队列,但是因为accept队列已满,所以只是标记连接为acked,并不会将连接移动到accept队列中,也不会为连接分配sendbufrecvbuf等资源。

150167 client端的应用程序,检测到connect系统调用完成,开始向该连接发送数据。

                   Server端收到数据包,由于acept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

150225 client端由于没有收到刚才发送数据的ack,所以会重传刚才的数据包

150296 同上

150496 同上

150920 同上

151112 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,重新发送synackclient端。

151113 client端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

151896 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

152579 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

152581 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回

153455 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

155399  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

155400 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

156468 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

161309  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synackclient端。

161310 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

162884 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

164828 client端等待一段时间后,认为连接不可用,于是发送FINACKserver端。Client端的状态变为FIN_WAIT1,等待一段时间后,client端将看不到该链接。

164829 server端收到ACK后,此时cgi程序处理完一个请求,从accept队列中取走一个连接,此时accept队列中有了空闲,server端将请求的连接放到accept队列中。

这样cgi所在的服务器上显示该链接是established的,但是nginx(client)所在的服务器上已经没有该链接了。

之后,当cgi程序从accept队列中取到该连接后,调用read去读取sock中的内容,但是由于client端早就退出了,所以read就会block那里了。


先来回顾下三次握手里面涉及到的问题:

1. 当 client 通过 connect 向 server 发出 SYN 包时,client 会维护一个 socket 等待队列,而 server 会维护一个 SYN 队列

2. 此时进入半链接的状态,如果 socket 等待队列满了,server 则会丢弃,而 client 也会由此返回 connection time out;只要是 client 没有收到 SYN+ACK,3s 之后,client 会再次发送,如果依然没有收到,9s 之后会继续发送

3. 半连接 syn 队列的长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)  决定

4. 当 server 收到 client 的 SYN 包后,会返回 SYN, ACK 的包加以确认,client 的 TCP 协议栈会唤醒 socket 等待队列,发出 connect 调用

5. client 返回 ACK 的包后,server 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 的含义请看这里。需要注意的是, 一些 Linux 的发型版本可能存在对 somaxcon 错误 truncating 方式 。

6. 当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被相应,此时,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回,0 表示直接丢丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。上面说的只是些理论,如果服务器不及时的调用 accept(),当 queue 满了之后,服务器并不会按照理论所述,不再对 SYN 进行应答,返回 ETIMEDOUT。根据 这篇 文档的描述,实际情况并非如此,服务器会随机的忽略收到的 SYN,建立起来的连接数可以无限的增加,只不过客户端会遇到延时以及超时的情况。

可以看到,整个 TCP stack 有如下的两个 queue:

1. 一个是 half open(syn queue) queue(max(tcp_max_syn_backlog, 64)),用来保存 SYN_SENT 以及 SYN_RECV 的信息。

2. 另外一个是 accept queue(min(somaxconn, backlog)),保存 ESTAB 的状态,但是调用 accept()。


使用 ss 获取到的 Recv-Q/Send-Q 在 LISTEN 状态以及非 LISTEN 状态所表达的含义是不同的。从tcp_diag.c 源码 中可以看到二者的区别:

LISTEN 状态: Recv-Q 表示的当前等待服务端调用 accept 完成三次握手的 listen backlog 数值,也就是说,当客户端通过 connect() 去连接正在 listen() 的服务端时,这些连接会一直处于这个 queue 里面直到被服务端 accept();Send-Q 表示的则是最大的 listen backlog 数值,这就就是上面提到的 min(backlog, somaxconn) 的值。 其余状态: 非 LISTEN 状态之前理解的没有问题。Recv-Q 表示 receive queue 中的 bytes 数量;Send-Q 表示 send queue 中的 bytes 数值。

要理解上面总结的这些,可以参见下这两个案例( 1 , 2 )。 

通过 "SYNs to LISTEN sockets dropped" 以及 "times the listen queue of a socket overflowed" 这两个 netstat -s 获取到的 TCP 状态,可以很快的发现系统存在的一些问题。 任何一个包含 "dropped" 或者 "overflowed" 并且数值一直居高不下的 metric 从字面含义理解来看,都不是一个好现象。

对于 Nginx 来说,backlog 的默认值为 511,这个可以通过 ss/netstat 的 Send-Q 确认:

State      Recv-Q Send-Q        Local Address:Port          Peer Address:Port

LISTEN     0      511                       *:80                       *:*     

可以通过适当的增大 nginx 的 backlog 以及 somaxconn 来增大队列: listen 80 backlog=1638


今天编程序的偶然遇到了一点问题,然后小研究了一下,发现一些以前不知道的事情,还有点小吃惊 ,记录一下。有些观点可能有问题,求指正。

我们都知道listen参数有个参数backlog。如果服务器不能及时调用accept,把连接从listen queue里面取走,那么UNP告诉我们,服务器的listen queue满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的connect就会返回ETIMEDOUT。但实际上Linux的行为不是这样的!

让一个程序在8000端口上listen,backlog值是n,不调用accept;再让一个客户端程序不停的调用connect。客户端的前n个connect调用立刻就成功返回,这是意料之中的。按照UNP的说法,以后再调用connect应该统统超时失败,但实际上我看到的是:有的connect超时失败了,有的立刻成功返回了,有的经过明显延迟后成功返回了。用这个命令观看:
watch "netstat -t -n | grep 8000 | grep -oP '\w+\s*$' | sort | uniq -c"
发现建立的连接数是可以无限增加的!再用tcpdump看,发现当客户的第一个syn发出后,服务器会随机的选择是否发送syn/ack,也就是tcp握手的第二步。所以对connect行为的解释就是:如果这次调用connect的第一个syn就被syn/ack了,那么connect就会立刻成功;如果第一个syn被忽略了,而地二个syn被服务器应答了,那么connect就会延迟成功;如果不幸三个syn都被服务器忽略了,connect就会返回超时失败。我google了半天,并没有发现任何文档描述这种行为,如果要确证只能看源码了。

结论:tcp的listen queue满后,linux不会如书中所说的拒绝连接,而是会随机的忽略发起连接的syn,建立起来的连接数可以无限增加,但是客户端会随机的遇到延迟和超时。

不管是拒绝连接还是随机的不理会连接请求,都是我们不愿意看到的,所以作为服务器工程师我们要监控这种现象。怎么在服务端看到呢?有两种方法,一是用netstat:
$ netstat -s | grep listen
    21391 times the listen queue of a socket overflowed
如果这个计数不停的增加就是有麻烦了。
令一种方法是针对单个listen socket的。调用getsockopt,传入TCP_INFO,返回一个tcp_info结构,这个结构中的成员tcpi_sacked是listen queue的大小,即传人listen的backlog值;成员tcpi_unacked是listen queue里连接的数量,如果tcp_unacked > tcpi_sacked,那就应该注意啦。具体调用方法用一个python程序说明:

import time
import socket
import struct

HOST = 'localhost'
PORT = 8000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(88)

#/usr/include/linux/tcp.h
struct_tcp_info = struct.Struct('7B 24I')

while True:
    buf = s.getsockopt(socket.IPPROTO_TCP,
                       socket.TCP_INFO,
                       1024)
    tcp_info = struct_tcp_info.unpack(buf)
    tcpi_unacked, tcpi_sacked = tcp_info[11:13]
         
    print 'tcpi_unacked:', tcpi_unacked,\
        'tcpi_sacked:', tcpi_sacked

    time.sleep(1)
可以看到刚开始运行时候输出是:
tcpi_unacked: 0 tcpi_sacked: 88
不断的往8000端口建立连接,到后来输出就成了:
tcpi_unacked: 89 tcpi_sacked: 88
这就表示listen queue已经满了。此时上面netstat报告的overflow计数也开始不停增加了。




现象2:netstat -antp观察服务端的socket,Recv-Q堆积的buffer越来越大

这个现象直接导致了客户端的socket连接timeout的比例越来越大

调试过程:

刚开始用netstat观察套接字Recv-Q为0,同时用tcpdump -s0 -S -xx 观察包体

猜你喜欢

转载自blog.csdn.net/wangshuminjava/article/details/80926579