第一堂课:三次握手、四次挥手与状态机
讲 TCP 优化,头一条你得把它的生命周期吃透。说白了,TCP 就三个阶段:建连、传数、断连。每个阶段都靠状态机驱动,状态机一旦跑偏,性能就崩。
三次握手:不是多此一举
客户端要跟服务端说话,得先“握手”。三次,少一次都不行。你想想看,如果只握两次,服务端怎么知道客户端收到了自己的 SYN+ACK?万一丢了,服务端会一直等,白白浪费资源。
具体流程:
- 客户端发
SYN=1, seq=x。状态变成SYN_SENT。 - 服务端回
SYN=1, ACK=1, seq=y, ack=x+1。状态变成SYN_RCVD。 - 客户端发
ACK=1, seq=x+1, ack=y+1。双方进入ESTABLISHED。
这里要注意半连接队列和全连接队列。半连接队列(tcp_max_syn_backlog)存的是刚收到 SYN、还没完成三次握手的连接。一旦它满了,新 SYN 直接丢,客户端会反复重试,表现为连接建立超慢。我建议调大 net.core.somaxconn 和 net.ipv4.tcp_max_syn_backlog,尤其高并发场景。
四次挥手:好聚好散
断连比建连复杂。因为 TCP 是双工的,两端都要关闭自己的发送通道,所以需要四次交互。
- 主动方发
FIN=1, seq=u→FIN_WAIT1 - 被动方回
ACK=1, seq=v, ack=u+1→CLOSE_WAIT - 被动方发
FIN=1, seq=w, ack=u+1→LAST_ACK - 主动方回
ACK=1, seq=u+1, ack=w+1→TIME_WAIT,2MSL 后关闭
怎么记这个顺序?我自己的口诀:
- 先断写(主动FIN)
- 对方确认(被动ACK)
- 对方也断(被动FIN)
- 我方确认(主动ACK)
net.ipv4.tcp_tw_reuse 和 tcp_tw_recycle(不过注意,内核 4.12 后移除了 recycle),配合 tcp_fin_timeout 缩短 TIME_WAIT 时长,在高并发短连接场景效果明显。
状态机:一张图看懂全部
TCP 状态机把上面所有流程串起来。不像 HTTP 那么无脑,TCP 状态切换有严格的前后依赖。下面这张图我亲手画出来,方便你对照抓包时看到的状态。
(注:上面 SVG 是示意图,展示主要状态间的跳转。实际 TCP 状态机文档中有 11 种状态,这里我保留了最关键的 9 个,方便你快速对照 ss 或 netstat 的输出。)
状态机里的几个“死结”
| 状态 | 常见原因 | 影响 |
|---|---|---|
SYN_RCVD 堆积 |
半连接队列满,或服务端 accept 太慢 | 新连接超时重传,连接建立延迟 |
CLOSE_WAIT 过多 |
被动方应用没调用 close() |
连接泄漏,文件描述符耗尽 |
TIME_WAIT 过多 |
短连接频繁,且主动关闭方在客户端 | 端口耗尽,无法发起新连接 |
FIN_WAIT2 长时间卡住 |
被动方不发 FIN(应用没关闭 socket) | 连接半开,资源占着不释放 |
netstat -anp | grep CLOSE_WAIT 数数,但其实 ss -s 能一眼看出整体状态分布。我自己的服务器上习惯每分钟记录一次状态快照,丢到时序库里,出问题时能快速回放到底是哪个状态堆积了。
怎么用这些知识做优化?
你知道握手阶段最怕什么?**SYN Flood**。攻击者只发 SYN 不回 ACK,服务端挂一堆半连接,CPU 忙着重试,正常用户连不上。我建议:
- 开启
net.ipv4.tcp_syncookies,内核在队列满时改用 cookie 验证,省掉半连接占用。 - 调小
net.ipv4.tcp_syn_retries,默认 6 次(127 秒),改成 3 次(13 秒)足够。 - 设置
net.core.somaxconn和net.ipv4.tcp_abort_on_overflow:后者改成 1 时,队列满直接发 RST 切断,别让客户端傻等。
断连阶段,TIME_WAIT 是老大难。我经历过一个支付通知服务,单机并发 2 万 QPS,TIME_WAIT 冲上 6 万,端口耗光,服务直接拒绝新连接。当时的解法:
- 调整
net.ipv4.tcp_fin_timeout从 60 降到 15 秒。 - 开启
net.ipv4.tcp_tw_reuse(需要 NAT 对称路由没问题时才安全)。 - 改成非主动关闭方——让服务端先 close,把 TIME_WAIT 留给客户端(虽然不完美,但客户端端口多,不容易耗光)。
状态机这个东西,你只要记住了那九个状态之间的箭头,再看 ss -tan 的输出就一目了然。嗯,建连和断连的流程先讲到这里。下一小节我们聊 TSO、GSO 和接收卸载 —— 不过那些是内核数据路径的事,咱们先把这棵根基扎牢。