天天看点

TCP 重传三次握手的syn+ack以及最后一个ack包

我们的一个数据库出现了连接上去之后info请求不返回的问题,为了找到问题原因,我做了一个tcpdump,结果发现,他有大量重传tcp的第二个,第三个握手包,并且在重传几次之后reset:

TCP 重传三次握手的syn+ack以及最后一个ack包

嗯,第一个syn包重传我遇到过了,见我之前的文章,但是第二个和第三个同时重传的,我还真没遇到过。而且在我的环境下,这个问题很神奇,因为:

  1. 第一个包能到达,因为返回了第二个syn包。说明网络是好的。
  2. 第二个包也回来了,说明linux认为连接能被建立。
  3. 已经建立的连接,通讯完全正常。
  4. 当时cpu负载并不高,至少操作系统没有很忙。
  5. 连接数不多,大概就两百个左右。
  6. 检查了防火墙, 没有任何过滤条件

我查了一下netstat,发现该端口有大量的close_wait的连接,于是怀疑是不是服务器的哪里阻塞住了,导致了这个问题。并且所有close wait的连接,recv queue都是有15byte数据等待读取的,让我更加怀疑这个是服务器没有正确处理socket读取请求导致的问题。

直觉让我认为,有可能是服务器的应用在忙着做什么东西,导致一直没有accept连接。但是已经accept的,可能在其他线程正常工作。

Listen Queue Length

如果写过linux socket的人可能知道,一般创建一个端口并监听,连接,需要以下动作:

  1. 创建socket描述符
  2. bind到特定地址,端口
  3. 将描述符给listen调用,开始监听,监听的时候给定一个listen queue length,用以限制等待接入的队列长度
  4. 循环调用accept(或者如果你使用epoll等模型,就把这个描述符丢到poll里面,等待有连接再accept)

这里就有一个问题了,如果不做accept,会怎样呢?

于是我做了一个实验,代码可以在这里看到。

这个代码,在ubuntu12.04上面编译之后运行,就开始监听7777端口。然后使用telnet上去,之后主动把telnet kill掉。第一次往往是正常的,之后,大概第二次或者第三次的样子,就会开始出现我说的那个问题了。配合wireshark可以更加明显地观察到这个现象。

事实上,如果你调大代码里的LENGTH_OF_LISTEN_QUEUE,你会发现,要重现这个问题会需要更多的连接尝试。注意,队列长度还与 /proc/sys/net/ipv4/tcp_max_syn_backlog 有关

这个很奇怪,明明队列满了,为什么还会回复syn和ack?难道不应该直接reset吗?于是我进一步进行搜索。

/proc/sys/net/ipv4/tcp_abort_on_overflow

进一步的调查,我发现,在linux上面有一个叫做tcp_abort_on_overflow的选线,这个选项控制队列满的时候的行为就是,如果为1,则在listen队列满的时候返回reset,如果为0,则还是正常三次握手。

于是我在本地做了一次实验:

TCP 重传三次握手的syn+ack以及最后一个ack包

好了,这个时候就返回了reset了。不过是在连接三次握手之后,马上reset。

那么,为什么linux默认的行为是重传syn+ack而不是直接发reset呢?

可以参考阿里大神写的这个文章:

tcp半连接队列与全连接队列

实际上,tcp连接进入accept队列,是在收到第三个ack之后,也就是说,只有在这个时候,内核才知道,是不是能进入accept队列。

另外,如果直接发reset,对于服务器和客户端来说,都是不断重连的过程。但是如果后面重传syn和ack,再次收到ack之后,能进入accept队列,实际上对客户端来说,并没有重新建立连接,对服务器来说,也只是一个定时器没有关的问题而已。所以,linux的man手册推荐,除非应用层真的accept不过来,否则不要打开这个选项。

TCP_DEFER_ACCEPT

同时,为了找到问题的原因,我另外google了一下。发现了一个有意思的东西,TCP_DEFER_ACCEPT,这是一个与socket绑定的选项。这个选项的行为如下:

  1. 收到第三次握手ack的时候,内核将这个连接标记为acked,然后把这个包丢掉。
  2. 不往上传递accept,也就是应用层不会accept,并且连接保持在syn_recv的状态。
  3. 重传超时计时器继续,也就是如果之后没有带数据的包过来,就会重传syn+ack,并且在超过syn次数之后,reset这个连接
  4. 如果在重传之前,有数据包过来,才会带着数据包,将accept请求传递上去。

这个选线开启的时候,如果连接上去之后没有发送任何数据,其行为跟我抓到的包是完全一致的。然而实际上,在我的截图的第六个包,是有数据的,并且带了PSH标志,所以我的情况跟这个不同。这个行为在apache web服务器上面可以看到。

继续阅读