天天看點

java.net.sockttimeout,徹底了解connection timeout

我們在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';

java.net.sockttimeout,徹底了解connection timeout

image.png

java.net.sockttimeout,徹底了解connection timeout

image.png

從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

java.net.sockttimeout,徹底了解connection timeout

image.png

從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 協定棧的了解總是需要慢慢積累