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, 32RFC 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