Go 验证 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
推荐阅读