3. TCP三次握手与四次挥手:握手延迟分析、SYN洪水与半连接队列、挥手过程中的TIME_WAIT问题
TCP的连接与断开,说白了就是两台机器之间互相确认「我在,你也在,咱们可以聊了」以及「我说完了,你也说完了,咱们散了吧」的过程。很多性能问题,根源就藏在这两个阶段里。
我刚开始做网络排查时,总觉得握手挥手就那么几个包,能有什么好看的?直到有一次线上服务频繁超时,抓包一看——好家伙,握手阶段就卡了一秒多。嗯,从那以后我再也不敢小看这三握四挥了。
3.1 三次握手:不只是「你好你好你好」
三次握手的本质,是双方协商初始序列号、确认对方存活、交换窗口大小等关键参数。你想想看,如果这一步就慢了,后面的数据传输全得排队等。
核心流程回顾:
- 客户端发送 SYN(seq=x)—— 我来了,这是我的初始序号
- 服务端回复 SYN+ACK(seq=y, ack=x+1)—— 收到,这是我的序号
- 客户端发送 ACK(seq=x+1, ack=y+1)—— 确认收到,咱们开聊
但实际抓包时,你看到的往往不是这么干净利落。我习惯用 Wireshark 的 tcp.flags.syn==1 过滤出所有握手包,然后看时间戳列。如果 SYN 发出到 SYN+ACK 回来超过 200ms,那基本可以断定有问题。
3.1.1 握手延迟的常见原因
| 现象 | 可能原因 | Wireshark 排查点 |
|---|---|---|
| SYN 重传(间隔 1s、3s、7s…) | 服务端未响应,客户端超时重传 | 查看 tcp.analysis.retransmission |
| SYN→SYN+ACK 间隔 > 1s | 服务端 backlog 满,连接被排队 | 检查 tcp.analysis.ack_rtt |
| SYN+ACK→ACK 间隔异常 | 客户端处理慢或中间设备干扰 | 看客户端 TCP 窗口是否为零 |
| 初始 RTT 波动大 | 网络路径不稳定或丢包 | 用 tcp.analysis.ack_rtt 统计 |
我在项目中遇到过最典型的一次:客户端连数据库,每次握手都要 3 秒。抓包一看,SYN 发了三次才收到 SYN+ACK。查到最后,是中间防火墙把 SYN 包当攻击流量给限速了。说白了,中间设备对 TCP 的「过度保护」反而成了瓶颈。
我的排查习惯:在 Wireshark 里用 tcp.flags.syn==1 and tcp.flags.ack==0 只看纯 SYN 包,然后看 Expert Info 里的「Retransmission」和「Fast Retransmission」。如果 SYN 重传超过 1 次,先怀疑网络路径,再怀疑服务端负载。
3.2 SYN 洪水与半连接队列
SYN 洪水攻击,说白了就是攻击者只发 SYN,不发最后的 ACK。服务端傻傻地等着,把连接放到半连接队列(SYN Queue)里。队列满了,正常用户的 SYN 就进不来了。
你想想看,服务端资源是有限的。半连接队列的大小由 tcp_max_syn_backlog 和 somaxconn 共同决定。一旦被填满,新连接直接丢包——用户那边看到的就是「连接超时」。
我曾经踩过的坑:有一次线上告警说服务不可用,我第一反应是应用挂了。重启后好了五分钟,又挂了。抓包一看,半连接队列里全是来自同一个 IP 的 SYN,ack 号随机乱填。典型的 SYN 洪水。后来加了 SYN Cookie 才稳住。
3.2.1 如何用 Wireshark 识别 SYN 洪水
- 看 SYN 包数量:用
tcp.flags.syn==1 and tcp.flags.ack==0统计,如果每秒上千个且来源 IP 分散,基本就是攻击 - 看 SYN+ACK 重传:服务端发了 SYN+ACK 但没等到 ACK,会重传。用
tcp.analysis.retransmission and tcp.flags.syn==1 and tcp.flags.ack==1过滤 - 看目标端口:如果所有 SYN 都打同一个端口,攻击目标明确
- 看 TTL:攻击流量往往 TTL 相同或呈规律变化
防御三板斧:
- SYN Cookie:服务端收到 SYN 时不分配资源,而是用 Cookie 做无状态校验。Linux 下
sysctl -w net.ipv4.tcp_syncookies=1 - 调整半连接队列:增大
net.ipv4.tcp_max_syn_backlog和应用的somaxconn - 限制 SYN 速率:用 iptables 或硬件防火墙做限速,比如
iptables -A INPUT -p tcp --syn -m limit --limit 100/s -j ACCEPT
3.3 四次挥手与 TIME_WAIT 问题
挥手比握手复杂,因为它是「半关闭」的——每一方都要独立关闭自己的数据流。四次挥手的过程我就不赘述了,但 TIME_WAIT 这个状态,是每个网络工程师都绕不开的坎。
3.3.1 TIME_WAIT 为什么存在?
说白了,TIME_WAIT 有两个目的:
- 确保被动关闭方收到最后的 ACK(如果 ACK 丢了,对方会重传 FIN)
- 让旧连接的报文在网络中消散,避免干扰新连接
TIME_WAIT 持续 2MSL(通常 1-4 分钟)。对于高并发的短连接服务(比如 Nginx、Redis 客户端),这个状态会堆积大量端口和内存。
我的经验:用 netstat -tan | grep TIME_WAIT | wc -l 看数量。如果超过几万,而且你的服务是短连接模型,那就要处理了。我见过一台机器上 TIME_WAIT 堆到 30 万,新连接直接报错「Cannot assign requested address」。
3.3.2 Wireshark 中看挥手问题
| 现象 | 过滤表达式 | 说明 |
|---|---|---|
| FIN 重传 | tcp.analysis.retransmission and tcp.flags.fin==1 |
对方没收到 FIN,或者 ACK 丢了 |
| RST 替代 FIN | tcp.flags.reset==1 |
异常关闭,可能应用层主动 RST |
| 挥手卡住 | tcp.flags.fin==1 or tcp.flags.reset==1 |
看时间戳,如果 FIN 发出后长时间没收到 FIN+ACK,可能是对端卡住 |
| 大量 TIME_WAIT | 无法直接过滤,但可通过统计看 | 用 Statistics → Conversations 看连接数 |
3.3.3 TIME_WAIT 的优化策略
注意,我不是让你盲目调优。TIME_WAIT 本身是安全机制,只有在确认不会导致问题时才调整。
- 开启端口复用:
net.ipv4.tcp_tw_reuse=1(客户端场景,允许重用 TIME_WAIT 状态的端口) - 快速回收:
net.ipv4.tcp_tw_recycle=1(已废弃,不推荐,NAT 环境下会出大问题) - 减少 MSL:修改
/proc/sys/net/ipv4/tcp_fin_timeout,默认 60s,可降到 30s - 应用层改造:用长连接替代短连接,或者用连接池
我曾经犯过的错:为了快速解决 TIME_WAIT 堆积,我直接开了 tcp_tw_recycle。结果 NAT 环境下的用户频繁掉线,因为不同用户经过同一个 NAT 出口时,时间戳乱了。后来老老实实改成长连接 + 端口复用,问题才解决。
3.4 本章知识体系图
下面这张图把三次握手、半连接队列、四次挥手和 TIME_WAIT 的核心逻辑串在一起。你可以对照着看,哪个环节出问题,就从哪个方向排查。
3.5 实战排查清单
最后,我把自己平时排查握手挥手问题的步骤整理成清单,你遇到类似问题时可以直接照着做:
- 抓包:在客户端和服务端分别抓,用
tcp.port==目标端口过滤 - 看握手:找到第一个 SYN,看它到 SYN+ACK 的间隔。超过 200ms 就标记
- 看重传:Expert Info 里有没有红色或黄色的 Retransmission?有的话看是哪个方向
- 看半连接:服务端用
ss -s看当前半连接数,对比tcp_max_syn_backlog - 看挥手:找到 FIN 包,看它到最后一个 ACK 的时间。如果超过 60s,检查 TIME_WAIT 堆积
- 看 TIME_WAIT:
netstat -tan | grep TIME_WAIT | wc -l,超过 5 万就要关注 - 看应用日志:有时候是应用层主动 RST,Wireshark 只能告诉你「断了」,为什么断还得看日志
一个小技巧:在 Wireshark 里用 tcp.time_delta > 0.5 过滤出所有间隔超过 500ms 的 TCP 包。这些「慢包」往往就是瓶颈所在。我每次排查性能问题,第一步就是跑这个过滤,然后看这些慢包都出现在哪个阶段。
握手挥手,看似基础,但很多线上问题就藏在这几个包的交互里。你只要把这一章的内容吃透,以后遇到连接超时、响应慢、端口耗尽这类问题,心里就有底了。