我们在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设置
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
image.png
从tcpdump的输出看到,第一次发送之后,只尝试了2次重试(2的0次+2的1次),因为第三次重试要等2的2次方秒,也就是4秒, 前面1+2 + 4是7秒,而应用层设置的超时时间是5秒,介于2~3之间,因此第三次重试不会进行. 如果应用程序设置的超时时间足够长,那么第三次重试应该在22:10:39进行.
小结
ps: 对 TCP 协议栈的理解总是需要慢慢积累
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。