TCP 连接超时与重试机制
TCP 连接建立时,三次握手并不是“一次失败就放弃”。实际上,操作系统会帮你自动重试若干次。这个重试的次数、间隔、最终超时时间,都受系统参数控制。说白了,就是内核替你扛了一部分“网络波动”。但如果你不知道这些参数在哪、怎么调,那只能干等着客户端转圈。
系统 TCP 超时参数
Linux 下,TCP 连接超时相关的内核参数集中分布在 /proc/sys/net/ipv4/ 里。我通常直接用 sysctl -a | grep tcp 扫一遍,重点关注下面几个:
| 参数名 | 默认值 | 含义 |
|---|---|---|
tcp_syn_retries |
6 | 发送 SYN 的最大重试次数(客户端视角) |
tcp_synack_retries |
5 | 发送 SYN+ACK 的最大重试次数(服务端视角) |
tcp_retries1 |
3 | 数据重传阈值,超过后通知上层(非致命) |
tcp_retries2 |
15 | 数据重传阈值,超过后放弃连接(致命) |
tcp_syn_retries 实际超时 |
约 127 秒 | 指数退避累加后的理论最大等待时间 |
为什么默认要重试 6 次?我记得早期内核文档里写过,6 次指数退避后,总时间大约 127 秒。这在局域网里太长了,但在公网上,某些中间设备丢包率极高的情况,确实需要这么久的耐心。
我经手的项目中,有次线上服务到 Redis 集群的 TCP 连接频繁“假死”。抓包发现 SYN 重试了 6 次才放弃,每次间隔指数增长,最后一次等待 64 秒。总共快两分钟!业务早就超时报错了。后来我把 tcp_syn_retries 改为 2,连接失败时间缩短到 3 秒左右,成功率反而上升了——因为重试次数少,前端错误重试策略能更快切换节点。
setsockopt 用法
系统参数是全局的,但很多时候你希望只影响某个 socket。这时候就得用 setsockopt。我个人常用的三个选项:
- SO_RCVTIMEO – 接收超时,调用
recv时最长等待时间 - SO_SNDTIMEO – 发送超时,调用
send时最长等待时间 - TCP_USER_TIMEOUT – TCP 级别超时,超过此时间后即使重传未完成也主动断开
// C 示例:设置接收超时 500ms
struct timeval tv = {0, 500000};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// 设置 TCP 用户超时 2 秒
unsigned int timeout = 2000; // 毫秒
setsockopt(sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout));
TCP_USER_TIMEOUT 这个选项我特别喜欢。它比 SO_RCVTIMEO 更底层,直接告诉 TCP 栈:“超过这个时间,就算重传还没结束,你也别等了”。在长连接池场景里,配一个合理的 TCP 用户超时,能避免大量“僵尸连接”把 fd 占满。
注意:TCP_USER_TIMEOUT 只能设置一次。如果你在连接建立后动态修改,部分内核版本可能不生效。我建议在 connect() 之前就设置好。
SYN 重试次数
SYN 重试是客户端建连时的“第一道防线”。客户端发出 SYN 后,如果迟迟收不到 SYN+ACK,内核会按指数退避策略重发。间隔依次是:1s、2s、4s、8s、16s、32s……直到 tcp_syn_retries 次数耗尽。
为什么会这样设计?你想想看,网络拥塞时,如果大家都用固定间隔重试,会造成拥塞崩溃。指数退避就是为了给网络喘息的时间。但代价你也看到了——最坏情况可能要等两分钟。
我建议:如果是内部网络,延迟可控,把 tcp_syn_retries 改到 2 或 3 就足够。如果是公网对公网,保留默认值 6 可能更安全,但一定要配合应用层的超时保护。
避坑指南:我曾经遇到过的一个案例——客户端 connect() 设置 5 秒超时,但内核 tcp_syn_retries 是 6,实际等待超过 120 秒。客户端认为 5 秒超时后抛异常关闭 socket,但内核还在后台上重试,导致端口被占住一段时间才释放。最终表现为客户端“连接成功但立即断开”,定位了整整一天。
解决方式很简单:要么降低内核重试次数,要么用 TCP_SYNCNT 选项在 socket 级别覆盖重试次数。
int syn_retries = 3;
setsockopt(sockfd, IPPROTO_TCP, TCP_SYNCNT, &syn_retries, sizeof(syn_retries));
这样,即使系统默认是 6,这个 socket 也只重试 3 次,总等待时间 ≈ 1+2+4+8 = 15 秒。配合应用层 20 秒超时,就不会出现上面的“灵魂出窍”情况了。
最后总结一下:系统参数控制全局行为,setsockopt 控制个别 socket,TCP_SYNCNT 专门控制 SYN 重试。我在实际调优时,通常先抓包看两端是否真的丢包,再决定调哪个参数。别盲目改大改小,不是所有问题都靠“加钱”能解决的。