3. 高并发流量捕获策略与性能调优
说到高并发,TCP三次握手往往是第一个倒下的环节。很多团队在压测时发现服务卡死,第一反应是“代码有bug”,结果查了半天,最后发现是握手阶段就崩了。我遇到过不止一次,线上服务突然大面积超时,抓包一看——满屏的SYN重传,半连接队列早就被撑爆了。
这一章,我们就来拆解三次握手在高并发下的脆弱点,以及如何用Wireshark快速定位问题。
3.1 三次握手与半连接队列
先回顾一下标准的三次握手流程:
- 客户端发送SYN,进入SYN_SENT状态
- 服务端收到SYN,分配连接资源,进入SYN_RCVD状态,并将该连接放入半连接队列
- 服务端回复SYN+ACK
- 客户端回复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 抓包实战:定位半连接队列溢出
我们来看一个完整的定位流程:
- 现象:客户端连接超时,服务端CPU正常,但连接数上不去
- 抓包:在服务端抓包,过滤
tcp.port == 80 - 观察:看到大量
[TCP Retransmission] [SYN]包,且服务端没有回复SYN+ACK - 确认:用
ss -t state syn-recv | wc -l查看半连接队列当前数量 - 对比:查看
net.ipv4.tcp_max_syn_backlog的值,发现队列已满 - 解决:调大参数,启用SYN Cookie,重启应用
我记得有一次,调完参数后问题依旧。后来发现是应用层代码里 listen(fd, 5) 写死了backlog为5,覆盖了系统参数。所以,代码层面也要检查。
3.7 核心逻辑图
下面这张图展示了三次握手、半连接队列、SYN洪泛攻击以及Wireshark特征之间的关系:
这张图把整个逻辑串起来了。从左到右看:正常流程是三次握手,连接进入半连接队列,收到ACK后转移到全连接队列。SYN洪泛攻击时,攻击者发大量SYN但不回ACK,导致半连接队列满,正常用户被拒。Wireshark里看到的就是SYN重传、指数退避、服务端无响应。
3.8 总结
半连接队列溢出,说白了就是服务端“忙不过来”了。调优参数、启用SYN Cookie、检查应用层代码,这三步走完,大部分问题都能解决。但别忘了,抓包才是定位问题的第一手段。下次遇到连接超时,别急着查代码,先抓个包看看是不是握手阶段出了问题。