3、TCP三次握手与超时:SYN_SENT状态、SYN超时重传机制、初始超时时间(RTO)的计算

说到连接超时,TCP三次握手是第一道坎。很多运维新手一看到连接失败,第一反应就是“网络不通”。其实不然。我见过太多案例,问题就出在握手阶段——客户端发了SYN,服务器没回,或者回得太慢。

这一节,咱们就聚焦在三次握手的第一个环节:SYN_SENT状态、SYN超时重传,以及那个决定成败的初始RTO。搞懂这些,你排查连接超时就有了“第一把尺子”。

3.1 SYN_SENT状态:连接发起的起点

当客户端调用connect(),内核会立即构造一个SYN报文,发往服务器。同时,这个连接在客户端进入SYN_SENT状态。

说白了,SYN_SENT就是“我发了请求,正在等回复”。这个状态很短暂——正常情况下,服务器回复SYN+ACK,客户端就转入ESTABLISHED了。

关键点:如果客户端长时间停留在SYN_SENT状态,说明服务器没有响应SYN报文。原因可能是服务器宕机、防火墙拦截、或者网络路径不通。

我记得有一次线上告警,某个微服务调用另一个服务总是超时。我登上去一看,ss -t输出里一堆SYN_SENT。当时我就判断:不是应用层的问题,是网络层或对端服务没起来。后来一查,对端服务器刚被运维重启了,服务没自动拉起。

排查技巧:用ss -t state syn-sent可以快速过滤出所有处于SYN_SENT状态的连接。如果数量持续增长,说明网络或对端有问题。

3.2 SYN超时重传机制:内核的“耐心”

客户端发了SYN,服务器没回。怎么办?内核不会一直傻等。它会启动一个定时器,超时后重传SYN。

这个机制叫SYN重传。重传次数和间隔由内核参数控制。我习惯用sysctl net.ipv4.tcp_syn_retries来查看和调整。

默认值通常是6次。重传间隔不是固定的,而是指数退避——第一次1秒,第二次2秒,第三次4秒,以此类推。总耗时大约1+2+4+8+16+32=63秒,加上最后一次等待,大概1分多钟。

重传次数 间隔(秒) 累计时间(秒)
1 1 1
2 2 3
3 4 7
4 8 15
5 16 31
6 32 63

你想想看,如果应用层设置的超时时间只有10秒,而内核要重试63秒才放弃,那应用层早就报错了。这就是为什么很多连接超时问题,应用层日志里是“connect timeout”,而内核还在默默重传。

避坑指南:我曾经遇到一个场景,应用层超时设了5秒,但内核tcp_syn_retries是默认的6次。结果应用层每5秒重试一次,内核每1秒重传一次,两边都在重试,网络瞬间被打满。后来我把tcp_syn_retries调成3次,问题解决。

3.3 初始RTO的计算:内核的“第一秒”从哪来

第一次SYN重传的间隔是1秒。这个1秒就是初始RTO(Retransmission Timeout)。但内核是怎么算出这1秒的?

对于SYN报文,内核没有历史RTT样本可用。所以它使用一个固定值——TCP_TIMEOUT_INIT,在Linux内核中定义为1秒(HZ=1000时是1秒,HZ=100时是10个jiffies)。

不过,这个值不是死的。内核会参考一些因素做微调:

  • TCP_USER_TIMEOUT:如果设置了socket选项,会覆盖默认值。
  • 路由度量:某些路由协议会携带RTT信息,内核可能用它来调整初始RTO。
  • 系统负载:高负载下,内核可能稍微增加初始RTO,避免拥塞。

但说实话,绝大多数场景下,初始RTO就是1秒。我做过抓包验证,从SYN发出到第一次重传,时间差正好是1.000秒左右。

核心公式:初始RTO = max(TCP_TIMEOUT_INIT, min(RTT样本, TCP_RTO_MAX))。对于SYN,因为没有RTT样本,所以直接取TCP_TIMEOUT_INIT,即1秒。

3.4 知识体系图:三次握手与超时

下面这张图把SYN_SENT状态、重传机制和RTO计算串起来了。你可以把它当作排查时的“路线图”。

TCP三次握手与超时知识体系 TCP三次握手与超时:知识体系 客户端 服务器 SYN (seq=x) SYN_SENT状态 SYN重传机制 指数退避:1s→2s→4s 默认重试6次 总耗时约63秒 初始RTO计算 TCP_TIMEOUT_INIT 默认 = 1秒 SYN+ACK (seq=y, ack=x+1) 排查要点 1. 用 ss -t state syn-sent 查看SYN_SENT连接 2. 抓包确认SYN重传间隔是否符合指数退避 3. 调整 tcp_syn_retries 控制重试次数

3.5 实战:如何验证和调整

光说不练假把式。我分享几个常用的排查和调整手段。

3.5.1 查看当前SYN重传参数

# 查看SYN重试次数
sysctl net.ipv4.tcp_syn_retries

# 查看初始RTO(不同内核版本路径可能不同)
cat /proc/sys/net/ipv4/tcp_syn_retries

3.5.2 抓包验证重传间隔

用tcpdump抓取SYN报文,观察重传时间戳:

tcpdump -i eth0 -n "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0"

输出示例:

12:00:00.000000 IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [S], seq 1000
12:00:01.000123 IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [S], seq 1000  # 1秒后重传
12:00:03.000456 IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [S], seq 1000  # 2秒后
12:00:07.000789 IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [S], seq 1000  # 4秒后

看到这个模式,就说明内核在按指数退避重传。如果间隔不对,可能是内核参数被改过,或者有中间设备在干扰。

3.5.3 调整SYN重试次数

有些场景下,默认的6次重试太久了。比如内网服务,网络延迟很低,3次重试(1+2+4=7秒)就足够判断了。

# 临时调整
sysctl -w net.ipv4.tcp_syn_retries=3

# 永久调整(写入/etc/sysctl.conf)
echo "net.ipv4.tcp_syn_retries=3" >> /etc/sysctl.conf

我的建议:对于公网服务,保留默认的6次;对于内网服务,可以调成3次。我曾经把某个内网API网关的tcp_syn_retries从6改成3,连接超时从63秒缩短到7秒,用户体验提升明显。

3.6 常见误区

  • 误区一:认为SYN重传间隔是固定的。其实是指数退避,第一次1秒,第二次2秒,以此类推。
  • 误区二:以为应用层超时能完全控制连接超时。实际上,内核的SYN重传机制在底层运行,应用层超时只是“上层感知”。两者需要配合调整。
  • 误区三:把SYN_SENT和SYN_RCVD搞混。SYN_SENT是客户端状态,SYN_RCVD是服务器状态。排查时一定要分清角色。