TCP SYN 超時重傳機制
背景
最近寫了一個壓測代碼,測試一個 http 接口,代碼大概是這個樣子,代碼跑在 Linux 機器上,內核版本:3.10.107
。
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
)
func main() {
for {
time.Sleep(time.Millisecond * 10)
cli := http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://xxx.com", nil)
if err != nil {
fmt.Println(err)
continue
}
now := time.Now()
rsp, err := cli.Do(req)
if err != nil {
fmt.Printf("%v, cost:%v\n", err, time.Since(now))
continue
}
body, err := ioutil.ReadAll(rsp.Body)
defer rsp.Body.Close()
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("len of body:%v", len(body))
}
}
預期 error
提供 http 服務的 server,在這樣的壓測條件下會來不及處理這麼多的請求,因此會存在 5s 超時的情況。5s 超時的時候,cli.Do(req)
會返回下面的錯誤信息。原因是 http.Client 經歷 5s 沒有收到結果,context 到達了 Deadline。
context deadline exceeded (Client.Timeout exceeded while awaiting headers), cost:5.00031874s
其他 error
在壓測過程中,還出現了其他的 error,並且數量要多於預期的 context deadline exceeded
錯誤。這是錯誤信息,錯誤信息裏隱去了 ip、port。
dial tcp ($ip):($port): connect: connection timed out, cost:3.017266219s
出現這個錯誤的調用耗時只有 3s,但是在代碼中初始化 cli 的時候設置了 5s 的超時,這是爲什麼呢?
分析
上面的 dial tcp
錯誤顯示是發起 tcp 調用時出的錯,那就需要從 tcp 的方面進行分析。
祖傳三次握手鎮樓。
client 向 server 發起第一次握手的時候,會發送 SYN 信號。如果 client 等待了一個超時時間之後沒有收到 server 的 ACK,client 則會重試。如果重試之後還是等待超時了,就再重試。
在 Linux 中,client 重傳 SYN 的次數由內核參數 net.ipv4.tcp_syn_retries
控制,默認爲 6。
通過以下指令在壓測機器上查看 SYN 重傳次數,可以看到壓測機器上發 tcp 請求時只會超時重傳一次 SYN。
$: sysctl -a | grep tcp_syn_retries
net.ipv4.tcp_syn_retries = 1
重傳間隔是怎麼規定的呢?
SYN 重傳間隔存在過一個 bug: kernel/git/torvalds/linux.git - Linux kernel source tree
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4d22f7d372f5769c6c0149e427ed6353e2dcfe61
在 bug 修復前,超時時間是由TCP_RTO_MIN
這個參數計算的,該參數在內核代碼/include/net/tcp.h
中定義。
#define TCP_RTO_MIN ((unsigned(HZ/5))
bug 修復之後,超時時間由TCP_TIMEOUT_INIT
計算,代碼地址:
https://elixir.bootlin.com/linux/v3.10.107/source/include/net/tcp.h#L136
#define TCP_TIMEOUT_INIT((unsigned(1*HZ)
這個值在RFC 6298
中,定義爲 1 秒。在RFC 1122
中爲 3 秒。這是最開始的超時等待時間,如果在這段時間內沒有收到 ACK,超時等待時間按 2 的指數倍增長。如果重試次數爲 6 次,那麼 RFC 6298
的超時重傳間隔就是 1, 2, 4, 8, 16, 32
;RFC 1122
中就是 3, 6, 9, 18, 36, 72
。
通過壓測機器的內核版本號,查證源碼得到該機器的初始超時重傳時間爲 1s。
那麼這就解釋的通了,壓測機器 SYN 重傳次數爲 1,所以 tcp 握手的時候第一次發 SYN,等待了 1s 沒有收到 ACK,又重傳一次,等待 2s 也沒有收到 ACK,這樣總共耗時了 3s,就報了 dial tcp: connection timeout
。
復現
接下來複現一下這種情況。
找一臺服務器,將 net.ipv4.tcp_syn_retries
設置爲 1。通過編輯 /etc/sysctl.conf
文件實現:
vim /etc/sysctl.conf
net.ipv4.tcp_syn_retries = 1
在終端中:
$: iptables -A INPUT --protocol tcp --dport 5000 --syn -j DROP
$: tcpdump -i lo -Ss0 -n src 127.0.0.1 and dst 127.0.0.1 and port 5000
開一個新終端:
$: date '+ %F %T'; telnet 127.0.0.1 5000; date '+ %F %T';
可以看到 tcpdump 中,只收到了兩次 SYN(16:50:20 和 16:50:21),並且兩次間隔爲 1s。
而在新終端中,看到整個調用的耗時爲 3s(16:50:20 - 16:50:23)。
總結
http 或 tcp 調用時的 dial tcp (ip):(port): connect: connection timed out
錯誤是 SYN 的超時重傳機制引起的。如果遇到這種錯誤,一方面需要考慮 server 可以處理請求的 QPS,另一方面也要檢查 client 端重傳相關參數的設置。
參考文獻
[1] 理解 timeout,這一篇就夠了 - poslua | ms2008 Blog
https://ms2008.github.io/2017/04/14/tcp-timeout/
[2] net.ipv4.tcp_syn_retries 參數的含義_來自萬古的憂傷的博客 - CSDN 博客
https://blog.csdn.net/weixin_45413603/article/details/113891804
[3] [TCP] tcp 連接 SYN 超時重傳次數和超時時間_陶士涵的菜地的技術博客_51CTO 博客
https://blog.51cto.com/u_15274085/2919125
[4] 《關於 TCP SYN 包的超時與重傳》——那些你應該知道的知識(四)_BBIE 的博客 - CSDN 博客_syn 重傳
https://blog.csdn.net/sinat_17736151/article/details/82804404
[5] SYN retransmits: Add new parameter to retransmits_timed_out()
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4d22f7d372f5769c6c0149e427ed6353e2dcfe61
[6] tcp.h - include/net/tcp.h - Linux source code (v3.10.107) - Bootlin
https://elixir.bootlin.com/linux/v3.10.107/source/include/net/tcp.h#L136
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9Vr1bNc8rEwG7QaSNf1xjA