Go 验证 TCP SYN 超时重传机制

共 3730字,需浏览 8分钟

 ·

2022-04-18 02:56

背景

最近写了一个压测代码,测试一个 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



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

浏览 290
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报