4、网络协议优化:TCP三次握手优化、TCP_NODELAY与Nagle算法、HTTP/1.1 vs HTTP/2 vs HTTP/3、连接池管理
网络协议优化,说白了就是让数据在网络上跑得更快、更稳。我见过太多团队,代码写得漂亮,数据库调得飞起,结果一压测,瓶颈全卡在网络层。今天咱们就聊聊几个最关键的优化点。
4.1 TCP三次握手优化
TCP三次握手,大家都知道。但你真的优化过它吗?
先看标准流程:客户端发SYN,服务端回SYN+ACK,客户端再回ACK。这一来一回,至少一个RTT(往返时间)就没了。如果客户端在纽约,服务器在北京,光握手就得100多毫秒。
核心优化手段:
- TCP Fast Open (TFO):允许在SYN包中携带数据。第一次握手正常,后续连接可以直接在SYN里发数据。我曾在CDN节点上启用TFO,首屏加载时间直接降了15%。
- 调整tcp_syn_retries:默认重试5次,耗时太长了。生产环境我一般改成2-3次。
- 启用tcp_tw_reuse:让TIME_WAIT状态的连接能被重用。高并发场景下,这个参数能救你的命。
# 查看当前TCP参数
sysctl net.ipv4.tcp_syn_retries
sysctl net.ipv4.tcp_tw_reuse
# 优化配置(加到/etc/sysctl.conf)
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fastopen = 3
注意:tcp_tw_reuse只能在客户端用,服务端别乱开。我曾经在服务端开了这个参数,结果导致连接复用混乱,排查了一整天。
4.2 TCP_NODELAY与Nagle算法
Nagle算法,说白了就是「攒够了再发」。它会把小包攒起来,等凑够一个MSS(最大报文段长度)或者收到ACK再发。初衷是好的,减少小包数量。但实时性要求高的场景,这就是灾难。
举个例子:你写了个聊天应用,用户敲一个字发一个包。Nagle算法一看,包太小,先攒着。结果用户打了五个字才发出去。用户体验?直接炸了。
解决方案就是TCP_NODELAY。它告诉内核:别攒了,有数据立刻发。
// C语言示例
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
// Java示例
socket.setTcpNoDelay(true);
// Go示例
tcpConn, _ := conn.(*net.TCPConn)
tcpConn.SetNoDelay(true)
我的经验:对于HTTP/1.1的短连接,开TCP_NODELAY效果明显。但对于HTTP/2这种多路复用的场景,Nagle算法的影响其实不大。别盲目优化,先压测看看瓶颈在哪。
4.3 HTTP/1.1 vs HTTP/2 vs HTTP/3
这三个协议,我一个个说。
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 连接方式 | 串行,最多6个并发 | 多路复用,一个连接搞定 | 基于QUIC,0-RTT握手 |
| 队头阻塞 | 有(一个请求卡住,后面的全等) | TCP层仍有队头阻塞 | 无(基于UDP) |
| 首部压缩 | 无 | HPACK压缩 | QPACK压缩 |
| 传输层 | TCP | TCP | UDP (QUIC) |
HTTP/1.1:老当益壮,但问题不少。最烦人的就是队头阻塞。你想想看,一个页面加载10个资源,得排队等。虽然可以用域名分片(多个域名)来绕过,但治标不治本。
HTTP/2:多路复用是亮点。一个TCP连接里可以同时发多个请求。但有个坑——TCP层的队头阻塞依然存在。如果丢了一个包,所有流都得等这个包重传。我在项目中遇到过,弱网环境下HTTP/2反而比HTTP/1.1慢。
HTTP/3:基于QUIC,直接换成了UDP。0-RTT握手,连接建立几乎无延迟。而且丢包只影响单个流,其他流不受影响。目前CDN厂商基本都支持了,我建议新项目直接上HTTP/3。
选型建议:
- 内网服务:HTTP/1.1就够,别折腾
- 公网API:优先HTTP/2,兼容性好
- 实时性要求高(直播、游戏):直接上HTTP/3
4.4 连接池管理
连接池,说白了就是「复用连接」。每次请求都新建TCP连接,三次握手开销太大了。连接池的核心参数就几个:最大连接数、空闲超时、最大等待时间。
// Go语言连接池配置示例
transport := &http.Transport{
MaxIdleConns: 100, // 最大空闲连接数
MaxIdleConnsPerHost: 10, // 每个host最大空闲连接
IdleConnTimeout: 90 * time.Second, // 空闲超时
MaxConnsPerHost: 0, // 每个host最大连接数(0表示不限制)
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
避坑指南:
- 连接泄漏:我曾经遇到过,response.Body没关闭,导致连接一直不释放。最后连接池耗尽,服务挂了。记得用defer resp.Body.Close()。
- 空闲超时太短:设成10秒,结果频繁建连,反而增加了开销。一般建议60-120秒。
- 最大连接数设太大:你以为能提高并发,结果把后端打爆了。要根据后端容量来设。
连接池的监控也很重要。我一般会暴露这几个指标:
- 当前活跃连接数
- 空闲连接数
- 等待获取连接的请求数
- 连接创建/销毁速率
一旦发现等待数持续上升,说明连接池不够用了。要么扩容,要么优化代码。
网络协议优化,没有银弹。每个场景都要具体分析。我建议你从监控入手,先看看当前系统的瓶颈在哪,再针对性地优化。别一上来就全改,容易翻车。
总结一下我的优化清单:
- TCP层面:开TFO、调重试次数、复用TIME_WAIT
- 应用层面:实时场景开TCP_NODELAY
- 协议选择:新项目上HTTP/3,存量项目评估迁移
- 连接池:设好参数、监控泄漏、定期巡检
嗯,今天就聊到这。这些优化点,你回去可以挑一个先试试。效果好不好,压测数据说了算。