彻底理解connection timeout

发布时间 2023-11-30 10:47:31作者: 沧海一滴

 

我们在connect时常常遇到connection timeout这种错误, 如果你仔细去观察,会发现connect timout分两种情况,

Caused by: java.net.ConnectException: Operation timed out (Connection timed out)

另外一种是:

Caused by: java.net.SocketTimeoutException: connect timed out

那这两种 timeout 有什么区别?分别在什么情况下会发生?

首先无论是哪种语言,不管是客户端还是服务端,在 TCP 编程中通常都可以为 sock 设置一个 timeout 时间。而这个 timeout 又可以细分为 connect timeout、read timeout、write timeout。read timeout 和 write timeout 必须是在 connect 之后才能发生,今天不做过多讨论。上面那两种 timeout 均属于 connect timeout。

另外我们需要补充下 TCP 重传机制的相关知识:

我们知道在 TCP 的三次握手中,Client 发送 SYN,Server 收到之后回 SYN_ACK,接着 Client 再回 ACK,这时 Client 便完成了 connect() 调用,进入 ESTAB 状态。如果 Client 发送 SYN 之后,由于网络原因或者其他问题没有收到 Server 的 SYN_ACK,那么这时 Client 便会重传 SYN。重传的次数由内核参数 net.ipv4.tcp_syn_retries 控制,重传的间隔为 [1,3,7,15,31]s 等

如果 Client 重传完所有 SYN 之后依然没有收到 SYN_ACK,那么这时 connect() 调用便会抛出 connection timeout 错误。如果 Client 在重传 SYN 期间,Client 的 sock timeout 时间到了,那么这时 connect() 会抛出 timeout 错误。

理解net.ipv4.tcp_syn_retries设置

  • net.ipv4.tcp_syn_retries 的设置,表示应用程序进行connect()系统调用时,在对方不返回SYN + ACK的情况下(也就是超时的情况下),第一次发送之后,内核最多重试几次发送SYN包;并且决定了等待时间.
  • Linux上的默认值是 net.ipv4.tcp_syn_retries = 6 ,也就是说如果是本机主动发起连接,(即主动开启TCP三次握手中的第一个SYN包),如果一直收不到对方返回SYN + ACK ,那么应用程序最大的超时时间就是127秒

Linux 系统默认的建立 TCP 连接的超时时间为 127 秒,对于许多客户端来说,这个时间都太长了, 特别是当这个客户端实际上是一个服务的时候,更希望能够尽早失败,以便能够选择其它的可用服务重新尝试。

socket对象是Linux下应用程序需要用到的和远端建立TCP或者UDP连接的对象.

系统调用 connect(2) 则是用来尝试建立 socket 连接(TCP)的函数。 connect 对于 UDP 来说并不是必须的,而对于 TCP 来说则是一个必须过程,著名的 TCP 3 次握手实际上也由 connect 来完成。

网络中的连接超时非常常见,不管是广域网还是局域网,为了一定程度上容忍失败,所以连接加入了重试机制, 而另一方面,为了不给服务端带来过大的压力,重试也是有限制的。

在 Linux 中,连接超时典型为 2 分 7 秒,而对于一些 client 来说,这是一个非常长的时间;

下面来看看 2 分 7 秒是怎样来的,以及怎样配置 Linux kernel 来缩短这个超时。

2 分 7 秒即 127 秒,刚好是 2 的 7 次方减一,聪明的读者可能已经看出来了,如果 TCP 握手的 SYN 包超时重试按照 2 的幂来 backoff, 那么:

第 1 次发送 SYN 报文后等待 1s(2 的 0 次幂),如果超时,则重试
第 2 次发送后等待 2s(2 的 1 次幂),如果超时,则重试
第 3 次发送后等待 4s(2 的 2 次幂),如果超时,则重试
第 4 次发送后等待 8s(2 的 3 次幂),如果超时,则重试
第 5 次发送后等待 16s(2 的 4 次幂),如果超时,则重试
第 6 次发送后等待 32s(2 的 5 次幂),如果超时,则重试
第 7 次发送后等待 64s(2 的 6 次幂),如果超时,则超时失败

上面的结果刚好是 127 秒。也就是说 Linux 内核在尝试建立 TCP 连接时,最多会尝试 7 次。

接下来,我们用实验来进行验证:

首先,配置 iptables 来丢弃指定端口的 SYN 报文

# iptables -A INPUT --protocol tcp --dport 5000 --syn -j DROP

然后,打开 tcpdump 观察到达指定端口的报文

# tcpdump -i lo -Ss0 -n src 127.0.0.1 and dst 127.0.0.1 and port 5000

最后,使用 telnet 连接指定端口

date '+ %F %T'; telnet 127.0.0.1 5000; date '+ %F %T';

 

 

 

从tcpdump的输出也可以看到,一共发了7次SYN包(都是同一个seq号码),第一次是正常请求,后面6次是重试,正是该内核参数 设置的值.

怎样修改 connect timeout

Linux 内核中,net.ipv4.tcp_syn_retries 表示建立 TCP 连接时 SYN 报文重试的次数,默认为 6,可以通过 sysctl 命令查看。

 # sysctl -a | grep tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

将其修改为 1,则可以将 connect 超时时间改为 3 秒,例如:

# sysctl net.ipv4.tcp_syn_retries=1
 date; telnet 127.0.0.1 5000; date;
2020年 06月 19日 星期五 22:16:11 CST
Trying 127.0.0.1...
telnet: connect to address 127.0.0.1: Connection timed out
2020年 06月 19日 星期五 22:16:14 CST

注意:sysctl 修改的内核参数在系统重启后失效,如果需要持久化,可以修改系统配置文件,例如:,对于 CentOS 7 来说,添加 net.ipv4.tcp_syn_retries = 1 到 /etc/sysctl.conf 中即可。

应用层真正的超时时间

那么问题来了,应用层真正的超时时间一定是127秒吗?还是不能大于127秒. 通过上面的实验,基本可以得知应用层的超时间一定不能大于内核的设定. 如果应用层的设定小于内核的设定呢?超时时间应该是小于127秒的.我们继续通过实验来验证下.

现在我的机器上,内核参数是net.ipv4.tcp_syn_retries=6,最大超时时间是 127秒 应用层代码如下:

#!/usr/bin/python
import socket
from datetime import datetime
fmt = "%Y-%m-%d %H:%M:%S"
address = ('127.0.0.1',5000)
s = socket.socket()
s.settimeout(5) #设置socket超时时间为5秒
print datetime.now().strftime(fmt)
s.connect_ex(address)
print datetime.now().strftime(fmt)

我们再来观察下应用程序的表现和tcpdump的输出

python test_socket_connect_timeout.py
2020-06-19 22:10:32
2020-06-19 22:10:37
 

 

从tcpdump的输出看到,第一次发送之后,只尝试了2次重试(2的0次+2的1次),因为第三次重试要等2的2次方秒,也就是4秒, 前面1+2 + 4是7秒,而应用层设置的超时时间是5秒,介于2~3之间,因此第三次重试不会进行. 如果应用程序设置的超时时间足够长,那么第三次重试应该在22:10:39进行.

小结

  • net.ipv4.tcp_syn_retries是用于设置主动发起TCP连接超时时,SYN包的重试次数,该参数如果是x,那么connect(2)调用最大的超时时间为2的x次方 -1,单位是秒.
  • 应用程序最大的超时时间不能超过内核的设定,可以小于等于内核的设定.

ps: 对 TCP 协议栈的理解总是需要慢慢积累



作者:tracy_668
链接:https://www.jianshu.com/p/d5060b16650e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。