TCP三次握手与超时:握手阶段超时的根源与参数
说到HTTP连接超时,咱们得先聊聊TCP三次握手。为啥?因为HTTP请求发出去之前,TCP连接得先建好。握手阶段要是卡住了,后面的请求全白搭。
我刚开始做后端时,遇到过一个问题:线上服务偶尔响应特别慢,查了半天发现是TCP握手超时导致的。那次排查让我对握手阶段的超时参数有了深刻理解。今天咱们就把这块掰开揉碎了讲清楚。
三次握手的本质:一次双向确认
TCP三次握手,说白了就是客户端和服务端互相确认「我准备好了,你呢?」的过程。
核心流程:
- SYN(客户端→服务端):客户端说「我想连你」,同时带上自己的初始序列号。
- SYN+ACK(服务端→客户端):服务端说「收到,我也准备好了」,带上自己的初始序列号,并确认客户端的序列号。
- ACK(客户端→服务端):客户端说「好的,确认收到」,连接建立完成。
你想想看,如果其中任何一步丢了或者延迟了,就会触发超时重传。这就是握手阶段超时的根源。
握手超时的三个关键点
每个握手阶段都可能超时,但原因和影响各不相同。我习惯把它们分成三类来理解:
| 超时点 | 触发条件 | 默认行为 | 影响范围 |
|---|---|---|---|
| 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秒 | 配合内核参数 |
这套配置我用了好几年,线上没出过握手超时的问题。当然,具体值还得根据你的业务场景微调。比如内网服务,网络质量好,可以设得更激进一些;跨机房或者跨国服务,可以适当放宽。
记住一个原则:超时时间不是越长越好,也不是越短越好。太长浪费资源,太短容易误判。找到那个平衡点,才是调优的真谛。