我們?cè)赾onnect時(shí)常常遇到connection timeout這種錯(cuò)誤, 如果你仔細(xì)去觀察,會(huì)發(fā)現(xiàn)connect timout分兩種情況,
Caused by: java.net.ConnectException: Operation timed out (Connection timed out)
另外一種是:
Caused by: java.net.SocketTimeoutException: connect timed out
那這兩種 timeout 有什么區(qū)別?分別在什么情況下會(huì)發(fā)生?
首先無(wú)論是哪種語(yǔ)言,不管是客戶端還是服務(wù)端,在 TCP 編程中通常都可以為 sock 設(shè)置一個(gè) timeout 時(shí)間。而這個(gè) timeout 又可以細(xì)分為 connect timeout、read timeout、write timeout。read timeout 和 write timeout 必須是在 connect 之后才能發(fā)生,今天不做過(guò)多討論。上面那兩種 timeout 均屬于 connect timeout。
另外我們需要補(bǔ)充下 TCP 重傳機(jī)制的相關(guān)知識(shí):
我們知道在 TCP 的三次握手中,Client 發(fā)送 SYN,Server 收到之后回 SYN_ACK,接著 Client 再回 ACK,這時(shí) Client 便完成了 connect() 調(diào)用,進(jìn)入 ESTAB 狀態(tài)。如果 Client 發(fā)送 SYN 之后,由于網(wǎng)絡(luò)原因或者其他問(wèn)題沒(méi)有收到 Server 的 SYN_ACK,那么這時(shí) Client 便會(huì)重傳 SYN。重傳的次數(shù)由內(nèi)核參數(shù) net.ipv4.tcp_syn_retries 控制,重傳的間隔為 [1,3,7,15,31]s 等
如果 Client 重傳完所有 SYN 之后依然沒(méi)有收到 SYN_ACK,那么這時(shí) connect() 調(diào)用便會(huì)拋出 connection timeout 錯(cuò)誤。如果 Client 在重傳 SYN 期間,Client 的 sock timeout 時(shí)間到了,那么這時(shí) connect() 會(huì)拋出 timeout 錯(cuò)誤。
理解net.ipv4.tcp_syn_retries設(shè)置
- net.ipv4.tcp_syn_retries 的設(shè)置,表示應(yīng)用程序進(jìn)行connect()系統(tǒng)調(diào)用時(shí),在對(duì)方不返回SYN + ACK的情況下(也就是超時(shí)的情況下),第一次發(fā)送之后,內(nèi)核最多重試幾次發(fā)送SYN包;并且決定了等待時(shí)間.
- Linux上的默認(rèn)值是 net.ipv4.tcp_syn_retries = 6 ,也就是說(shuō)如果是本機(jī)主動(dòng)發(fā)起連接,(即主動(dòng)開(kāi)啟TCP三次握手中的第一個(gè)SYN包),如果一直收不到對(duì)方返回SYN + ACK ,那么應(yīng)用程序最大的超時(shí)時(shí)間就是127秒
Linux 系統(tǒng)默認(rèn)的建立 TCP 連接的超時(shí)時(shí)間為 127 秒,對(duì)于許多客戶端來(lái)說(shuō),這個(gè)時(shí)間都太長(zhǎng)了, 特別是當(dāng)這個(gè)客戶端實(shí)際上是一個(gè)服務(wù)的時(shí)候,更希望能夠盡早失敗,以便能夠選擇其它的可用服務(wù)重新嘗試。
socket對(duì)象是Linux下應(yīng)用程序需要用到的和遠(yuǎn)端建立TCP或者UDP連接的對(duì)象.
系統(tǒng)調(diào)用 connect(2) 則是用來(lái)嘗試建立 socket 連接(TCP)的函數(shù)。 connect 對(duì)于 UDP 來(lái)說(shuō)并不是必須的,而對(duì)于 TCP 來(lái)說(shuō)則是一個(gè)必須過(guò)程,著名的 TCP 3 次握手實(shí)際上也由 connect 來(lái)完成。
網(wǎng)絡(luò)中的連接超時(shí)非常常見(jiàn),不管是廣域網(wǎng)還是局域網(wǎng),為了一定程度上容忍失敗,所以連接加入了重試機(jī)制, 而另一方面,為了不給服務(wù)端帶來(lái)過(guò)大的壓力,重試也是有限制的。
在 Linux 中,連接超時(shí)典型為 2 分 7 秒,而對(duì)于一些 client 來(lái)說(shuō),這是一個(gè)非常長(zhǎng)的時(shí)間;
下面來(lái)看看 2 分 7 秒是怎樣來(lái)的,以及怎樣配置 Linux kernel 來(lái)縮短這個(gè)超時(shí)。
2 分 7 秒即 127 秒,剛好是 2 的 7 次方減一,聰明的讀者可能已經(jīng)看出來(lái)了,如果 TCP 握手的 SYN 包超時(shí)重試按照 2 的冪來(lái) backoff, 那么:
第 1 次發(fā)送 SYN 報(bào)文后等待 1s(2 的 0 次冪),如果超時(shí),則重試
第 2 次發(fā)送后等待 2s(2 的 1 次冪),如果超時(shí),則重試
第 3 次發(fā)送后等待 4s(2 的 2 次冪),如果超時(shí),則重試
第 4 次發(fā)送后等待 8s(2 的 3 次冪),如果超時(shí),則重試
第 5 次發(fā)送后等待 16s(2 的 4 次冪),如果超時(shí),則重試
第 6 次發(fā)送后等待 32s(2 的 5 次冪),如果超時(shí),則重試
第 7 次發(fā)送后等待 64s(2 的 6 次冪),如果超時(shí),則超時(shí)失敗
上面的結(jié)果剛好是 127 秒。也就是說(shuō) Linux 內(nèi)核在嘗試建立 TCP 連接時(shí),最多會(huì)嘗試 7 次。
接下來(lái),我們用實(shí)驗(yàn)來(lái)進(jìn)行驗(yàn)證:
首先,配置 iptables 來(lái)丟棄指定端口的 SYN 報(bào)文
# iptables -A INPUT --protocol tcp --dport 5000 --syn -j DROP
然后,打開(kāi) tcpdump 觀察到達(dá)指定端口的報(bào)文
# 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的輸出也可以看到,一共發(fā)了7次SYN包(都是同一個(gè)seq號(hào)碼),第一次是正常請(qǐng)求,后面6次是重試,正是該內(nèi)核參數(shù) 設(shè)置的值.
怎樣修改 connect timeout
Linux 內(nèi)核中,net.ipv4.tcp_syn_retries 表示建立 TCP 連接時(shí) SYN 報(bào)文重試的次數(shù),默認(rèn)為 6,可以通過(guò) sysctl 命令查看。
# sysctl -a | grep tcp_syn_retries
net.ipv4.tcp_syn_retries = 6
將其修改為 1,則可以將 connect 超時(shí)時(shí)間改為 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 修改的內(nèi)核參數(shù)在系統(tǒng)重啟后失效,如果需要持久化,可以修改系統(tǒng)配置文件,例如:,對(duì)于 CentOS 7 來(lái)說(shuō),添加 net.ipv4.tcp_syn_retries = 1 到 /etc/sysctl.conf 中即可。
應(yīng)用層真正的超時(shí)時(shí)間
那么問(wèn)題來(lái)了,應(yīng)用層真正的超時(shí)時(shí)間一定是127秒嗎?還是不能大于127秒. 通過(guò)上面的實(shí)驗(yàn),基本可以得知應(yīng)用層的超時(shí)間一定不能大于內(nèi)核的設(shè)定. 如果應(yīng)用層的設(shè)定小于內(nèi)核的設(shè)定呢?超時(shí)時(shí)間應(yīng)該是小于127秒的.我們繼續(xù)通過(guò)實(shí)驗(yàn)來(lái)驗(yàn)證下.
現(xiàn)在我的機(jī)器上,內(nèi)核參數(shù)是net.ipv4.tcp_syn_retries=6,最大超時(shí)時(shí)間是 127秒 應(yīng)用層代碼如下:
#!/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) #設(shè)置socket超時(shí)時(shí)間為5秒
print datetime.now().strftime(fmt)
s.connect_ex(address)
print datetime.now().strftime(fmt)
我們?cè)賮?lái)觀察下應(yīng)用程序的表現(xiàn)和tcpdump的輸出
python test_socket_connect_timeout.py
2020-06-19 22:10:32
2020-06-19 22:10:37

從tcpdump的輸出看到,第一次發(fā)送之后,只嘗試了2次重試(2的0次+2的1次),因?yàn)榈谌沃卦囈?的2次方秒,也就是4秒, 前面1+2 + 4是7秒,而應(yīng)用層設(shè)置的超時(shí)時(shí)間是5秒,介于2~3之間,因此第三次重試不會(huì)進(jìn)行. 如果應(yīng)用程序設(shè)置的超時(shí)時(shí)間足夠長(zhǎng),那么第三次重試應(yīng)該在22:10:39進(jìn)行.
小結(jié)
- net.ipv4.tcp_syn_retries是用于設(shè)置主動(dòng)發(fā)起TCP連接超時(shí)時(shí),SYN包的重試次數(shù),該參數(shù)如果是x,那么connect(2)調(diào)用最大的超時(shí)時(shí)間為2的x次方 -1,單位是秒.
- 應(yīng)用程序最大的超時(shí)時(shí)間不能超過(guò)內(nèi)核的設(shè)定,可以小于等于內(nèi)核的設(shè)定.
ps: 對(duì) TCP 協(xié)議棧的理解總是需要慢慢積累