TCP三次握手与超时:握手阶段超时的根源与参数

说到HTTP连接超时,咱们得先聊聊TCP三次握手。为啥?因为HTTP请求发出去之前,TCP连接得先建好。握手阶段要是卡住了,后面的请求全白搭。

我刚开始做后端时,遇到过一个问题:线上服务偶尔响应特别慢,查了半天发现是TCP握手超时导致的。那次排查让我对握手阶段的超时参数有了深刻理解。今天咱们就把这块掰开揉碎了讲清楚。

三次握手的本质:一次双向确认

TCP三次握手,说白了就是客户端和服务端互相确认「我准备好了,你呢?」的过程。

核心流程:

  1. SYN(客户端→服务端):客户端说「我想连你」,同时带上自己的初始序列号。
  2. SYN+ACK(服务端→客户端):服务端说「收到,我也准备好了」,带上自己的初始序列号,并确认客户端的序列号。
  3. ACK(客户端→服务端):客户端说「好的,确认收到」,连接建立完成。

你想想看,如果其中任何一步丢了或者延迟了,就会触发超时重传。这就是握手阶段超时的根源。

TCP三次握手流程与超时点 客户端 服务端 CLOSED → SYN_SENT LISTEN → SYN_RCVD ① SYN (seq=x) 超时点1:SYN超时重传 ② SYN+ACK (seq=y, ack=x+1) 超时点2:SYN+ACK超时重传 ③ ACK (ack=y+1) 超时点3:ACK丢失(服务端重传SYN+ACK) SYN_SENT → ESTABLISHED SYN_RCVD → ESTABLISHED 连接建立成功

握手超时的三个关键点

每个握手阶段都可能超时,但原因和影响各不相同。我习惯把它们分成三类来理解:

超时点 触发条件 默认行为 影响范围
SYN超时 客户端发SYN后,未收到SYN+ACK 重传SYN,指数退避 连接建立延迟
SYN+ACK超时 服务端发SYN+ACK后,未收到ACK 重传SYN+ACK,指数退避 服务端资源占用
ACK丢失 客户端ACK在网络中丢失 服务端重传SYN+ACK 连接建立延迟

核心参数详解

Linux内核提供了几个关键参数来控制握手超时行为。嗯,这里要注意,不同内核版本默认值可能不一样。

1. tcp_syn_retries(客户端SYN重传次数)

这个参数控制客户端发SYN后,没收到SYN+ACK时最多重传几次。默认值是6。

我的经验:默认6次重传,总超时时间大约127秒。对于互联网应用来说太长了。我一般会改成3次,总超时时间大约7秒。用户等7秒已经够难受了,127秒简直不可接受。

# 查看当前值
sysctl net.ipv4.tcp_syn_retries

# 临时修改(重启失效)
echo 3 > /proc/sys/net/ipv4/tcp_syn_retries

# 永久修改
echo "net.ipv4.tcp_syn_retries = 3" >> /etc/sysctl.conf
sysctl -p

2. tcp_synack_retries(服务端SYN+ACK重传次数)

服务端收到SYN后,回复SYN+ACK,如果没收到客户端的ACK,就会重传。默认值也是6。

注意:这个参数直接影响服务端的半连接队列(SYN Queue)占用。重传次数越多,半连接占用的时间越长。我曾经遇到过SYN Flood攻击,就是因为这个参数太大,导致半连接队列被占满,正常用户连不上。

# 查看当前值
sysctl net.ipv4.tcp_synack_retries

# 建议值:2-3
echo "net.ipv4.tcp_synack_retries = 2" >> /etc/sysctl.conf
sysctl -p

3. tcp_syncookies(SYN Cookie保护)

这个参数不是超时参数,但跟握手超时密切相关。当半连接队列满时,开启SYN Cookie可以让服务端不保存半连接状态,直接回复SYN+ACK。

建议:生产环境一定要开启。

echo "net.ipv4.tcp_syncookies = 1" >> /etc/sysctl.conf
sysctl -p

4. tcp_max_syn_backlog(半连接队列大小)

这个参数决定服务端能同时处理多少半连接。默认值通常只有128或256,对于高并发服务来说太小了。

场景 建议值 说明
低并发(<100 QPS) 256 默认值够用
中等并发(100-1000 QPS) 1024 需要调整
高并发(>1000 QPS) 4096+ 配合syncookies使用
# 查看当前值
sysctl net.ipv4.tcp_max_syn_backlog

# 修改为1024
echo "net.ipv4.tcp_max_syn_backlog = 1024" >> /etc/sysctl.conf
sysctl -p

避坑指南:我曾经踩过的坑

我曾经接手过一个项目,线上服务经常出现「连接超时」的报警。查了半天,发现是tcp_syn_retries被改成了10。为什么?因为之前的同事觉得「重试越多越可靠」。

结果呢?客户端发一次SYN,要等127秒才放弃。用户早就刷新页面了,但连接还在那里重试。更糟糕的是,这些「僵尸连接」占满了文件描述符,导致新连接进不来。

我的建议:

  • 客户端tcp_syn_retries设成3,总超时约7秒
  • 服务端tcp_synack_retries设成2,总超时约3秒
  • 一定要开启tcp_syncookies
  • 根据并发量调整tcp_max_syn_backlog

超时时间计算:指数退避的秘密

TCP重传不是固定间隔的,而是指数退避。第一次重传等1秒,第二次等2秒,第三次等4秒,以此类推。

以tcp_syn_retries=3为例:

第1次重传:等待 1秒
第2次重传:等待 2秒
第3次重传:等待 4秒
总超时时间 = 1 + 2 + 4 = 7秒

如果tcp_syn_retries=6:

第1次重传:等待 1秒
第2次重传:等待 2秒
第3次重传:等待 4秒
第4次重传:等待 8秒
第5次重传:等待 16秒
第6次重传:等待 32秒
总超时时间 = 1+2+4+8+16+32 = 63秒
加上初始SYN的等待时间,总共约127秒

关键点:指数退避的设计初衷是为了避免网络拥塞。但现代互联网环境下,127秒的超时时间显然不合理。我个人建议把总超时控制在10秒以内。

应用层超时 vs 内核超时

很多同学会混淆应用层超时和内核超时。举个例子:

  • 应用层超时:你在代码里设置的connect timeout,比如Java的Socket.connect(timeout)
  • 内核超时:Linux内核的tcp_syn_retries等参数

这两者是什么关系?应用层超时是「我希望等多久」,内核超时是「我最多能等多久」。实际超时时间取两者中的较小值。

常见误区:有人把应用层connect timeout设成3秒,但内核tcp_syn_retries是6。结果呢?应用层3秒就报超时了,但内核还在后台继续重试。这个连接虽然应用层放弃了,但内核还在占用资源。所以,应用层超时和内核超时要配合着调。

实战调优建议

说了这么多,最后给出一套我常用的配置方案:

参数 建议值 适用场景
tcp_syn_retries 3 互联网服务(总超时约7秒)
tcp_synack_retries 2 服务端(总超时约3秒)
tcp_syncookies 1 必须开启
tcp_max_syn_backlog 1024-4096 根据并发量调整
应用层connect timeout 3-5秒 配合内核参数

这套配置我用了好几年,线上没出过握手超时的问题。当然,具体值还得根据你的业务场景微调。比如内网服务,网络质量好,可以设得更激进一些;跨机房或者跨国服务,可以适当放宽。

记住一个原则:超时时间不是越长越好,也不是越短越好。太长浪费资源,太短容易误判。找到那个平衡点,才是调优的真谛。