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计算串起来了。你可以把它当作排查时的“路线图”。
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是服务器状态。排查时一定要分清角色。