3. TCP三次握手与并发:高并发下的SYN洪泛、半连接队列溢出、Wireshark中的SYN重传特征

说到高并发,TCP三次握手往往是第一个倒下的环节。很多团队在压测时发现服务卡死,第一反应是“代码有bug”,结果查了半天,最后发现是握手阶段就崩了。我遇到过不止一次,线上服务突然大面积超时,抓包一看——满屏的SYN重传,半连接队列早就被撑爆了。

这一章,我们就来拆解三次握手在高并发下的脆弱点,以及如何用Wireshark快速定位问题。

3.1 三次握手与半连接队列

先回顾一下标准的三次握手流程:

  1. 客户端发送SYN,进入SYN_SENT状态
  2. 服务端收到SYN,分配连接资源,进入SYN_RCVD状态,并将该连接放入半连接队列
  3. 服务端回复SYN+ACK
  4. 客户端回复ACK,服务端将连接从半连接队列移到全连接队列,状态变为ESTABLISHED

这里的关键点是:半连接队列。服务端在收到SYN但还没收到ACK之前,连接是“半吊子”状态。这个队列的大小是有限的,由内核参数 tcp_max_syn_backlog 控制。

核心结论:半连接队列溢出,是SYN洪泛攻击和高并发下连接失败的根源。

我个人的习惯是,在压测前先检查这个队列的默认值。很多Linux发行版默认只有128或256,对于稍微有点并发的服务来说,根本不够用。

3.2 SYN洪泛攻击原理

SYN洪泛,说白了就是攻击者伪造大量源IP,疯狂发SYN包,但从不回复ACK。服务端傻傻地给每个SYN分配资源,塞进半连接队列。队列满了之后,正常用户的SYN包就被丢弃了。

攻击特征非常明显:

  • 服务端收到大量来自不同源IP的SYN包
  • 服务端回复SYN+ACK后,收不到ACK
  • 服务端会重传SYN+ACK(默认重传5次,间隔指数退避)
  • 半连接队列持续满,正常连接无法建立

我曾经在客户现场遇到过类似情况。当时他们的电商平台在做秒杀活动,流量突然暴增,结果服务直接挂了。运维以为是代码问题,我抓包一看——好家伙,半连接队列溢出导致的SYN重传,根本不是业务代码的锅。

3.3 Wireshark中的SYN重传特征

在Wireshark里,SYN重传有非常明显的特征。我们来看一个典型的抓包截图(用文字描述):

序号 时间 源IP 目标IP 信息 Wireshark标记
1 0.000 10.0.0.1 10.0.0.2 SYN [SYN]
2 1.000 10.0.0.1 10.0.0.2 SYN [TCP Retransmission] [SYN]
3 3.000 10.0.0.1 10.0.0.2 SYN [TCP Retransmission] [SYN]
4 7.000 10.0.0.1 10.0.0.2 SYN [TCP Retransmission] [SYN]

看到没?时间间隔是1秒、2秒、4秒……指数退避。Wireshark会在重传包上打上 [TCP Retransmission] 标签。如果你看到大量这样的SYN重传,基本可以断定是半连接队列满了。

小技巧:在Wireshark的过滤栏输入 tcp.analysis.retransmission && tcp.flags.syn==1,可以快速筛选出所有SYN重传包。

3.4 如何区分正常重传和洪泛攻击

正常情况下的SYN重传,通常是网络丢包导致的。但洪泛攻击的重传有几个不同点:

  • 源IP分布:正常重传的源IP是固定的几个;洪泛攻击的源IP是随机的、大量的
  • 重传次数:正常重传一般1-2次就成功了;洪泛攻击会一直重传直到超时
  • 服务端行为:正常重传时服务端会回复SYN+ACK;洪泛攻击时服务端可能根本不回复(因为队列满了)

嗯,这里要注意:如果服务端连SYN+ACK都不回了,那基本就是半连接队列溢出了。我曾经用Wireshark的统计功能,直接看 Statistics -> TCP Stream Graph -> Time-Sequence Graph,如果看到大量孤立的SYN点,没有对应的SYN+ACK,那问题就很明确了。

3.5 内核参数调优与防御

解决半连接队列溢出,最直接的办法是调大队列长度。但光调大还不够,还得配合其他机制。

下面是我常用的调优参数:

参数 默认值 推荐值 说明
net.ipv4.tcp_max_syn_backlog 128/256 1024/4096 半连接队列大小
net.ipv4.tcp_syncookies 0/1 1 启用SYN Cookie,队列满时用Cookie验证
net.ipv4.tcp_syn_retries 5 3 SYN重传次数,减少可加快失败回收
net.ipv4.tcp_synack_retries 5 2 SYN+ACK重传次数
net.core.somaxconn 128 1024 全连接队列大小,应用层listen()的backlog参数不能超过此值

避坑指南:我曾经只调大了 tcp_max_syn_backlog,但忘了调 somaxconn,结果全连接队列也满了。两个队列都要调,而且应用层的 listen(fd, backlog) 参数不能超过 somaxconn。

另外,tcp_syncookies 是个好东西。当半连接队列满时,内核会启用SYN Cookie机制,不分配资源,而是通过计算一个Cookie来验证客户端。这样即使队列满了,正常用户也能连上。但注意,SYN Cookie会禁用TCP的一些高级特性(如时间戳、窗口缩放),所以不是万能的。

3.6 抓包实战:定位半连接队列溢出

我们来看一个完整的定位流程:

  1. 现象:客户端连接超时,服务端CPU正常,但连接数上不去
  2. 抓包:在服务端抓包,过滤 tcp.port == 80
  3. 观察:看到大量 [TCP Retransmission] [SYN] 包,且服务端没有回复SYN+ACK
  4. 确认:用 ss -t state syn-recv | wc -l 查看半连接队列当前数量
  5. 对比:查看 net.ipv4.tcp_max_syn_backlog 的值,发现队列已满
  6. 解决:调大参数,启用SYN Cookie,重启应用

我记得有一次,调完参数后问题依旧。后来发现是应用层代码里 listen(fd, 5) 写死了backlog为5,覆盖了系统参数。所以,代码层面也要检查。

3.7 核心逻辑图

下面这张图展示了三次握手、半连接队列、SYN洪泛攻击以及Wireshark特征之间的关系:

TCP三次握手与SYN洪泛攻击核心逻辑 TCP三次握手与SYN洪泛攻击核心逻辑 正常三次握手 客户端 服务端 ① SYN ② SYN+ACK ③ ACK 半连接队列 全连接队列 放入 ACK后转移 SYN洪泛攻击 攻击者(伪造IP) 服务端 大量SYN(不回复ACK) 队列满(溢出) 正常用户 连接被丢弃 Wireshark特征 SYN重传标记 [TCP Retransmission] [SYN] 指数退避时间间隔 1s → 2s → 4s → 8s 服务端无SYN+ACK回复 队列满,不分配资源

这张图把整个逻辑串起来了。从左到右看:正常流程是三次握手,连接进入半连接队列,收到ACK后转移到全连接队列。SYN洪泛攻击时,攻击者发大量SYN但不回ACK,导致半连接队列满,正常用户被拒。Wireshark里看到的就是SYN重传、指数退避、服务端无响应。

3.8 总结

半连接队列溢出,说白了就是服务端“忙不过来”了。调优参数、启用SYN Cookie、检查应用层代码,这三步走完,大部分问题都能解决。但别忘了,抓包才是定位问题的第一手段。下次遇到连接超时,别急着查代码,先抓个包看看是不是握手阶段出了问题。