朕喜技术(ZhenxiJS)
2026.08.30
1 分钟阅读
TCP 监听队列(backlog/accept队列)被打满导致新连接超时的诊断与解决
问题背景
Web 服务器(Nginx + 应用)突然「慢吞吞」,新 TCP 连接经常超时/被拒绝,但服务进程正常、CPU/内存不高。
ss -lnt 观察到监听端口的 Send-Q 顶满(= backlog 值),怀疑有大量「建立后挂起不交互」的连接占满了内核监听 socket 的 accept 队列。
复现步骤
git checkout 2026-08-30-0800
cd scenario/2026-08-30-0800
# 一键拉起环境
docker compose up -d --build
# 运行复现脚本(backlog=16, accept_delay=0.3s, 用 200 个连接填充)
docker compose exec victim bash /workspace/scenario/2026-08-30-0800/reproduce.sh
关键输出:
# 洪泛前正常连接
[client] reply: b'hello\n'
exit_before=0
# 队列被打满后
LISTEN 16 16 0.0.0.0:8080 0.0.0.0:* # Send-Q=16 已满
TimeoutError: timed out # 正常连接超时
exit_during=1
# 连续 10 次连接测试
oks=1, fails=9 # 90% 的新连接失败
根本原因
TCP 三次握手完成后,连接进入监听 socket 的 accept 队列等待应用 accept()。该队列容量由 listen(fd, backlog) 决定(上限受 net.core.somaxconn 约束)。
问题链路:
- 应用 backlog 设置太小(16)+ accept 处理慢(每 0.3s 才 accept 一个)
- 攻击方建立大量挂起连接 → accept 队列迅速被填满(Send-Q = backlog)
- 新连接到达时内核发现 accept 队列已满 → 丢弃 SYN 或放入 SYN 半连接队列
- SYN 队列也满 → 直接丢弃 → 客户端超时 / connection reset / refused
解决办法
1. 调大 backlog + 快速 accept(最有效)
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(False) # 非阻塞
s.bind(('0.0.0.0', 8080))
s.listen(1024) # 调大 backlog
while True:
try:
conn, addr = s.accept()
# 快速处理,不阻塞
except BlockingIOError:
pass
验证结果: 队列不积压(Send-Q=0),新连接 100% 成功(原始场景只有 10% 成功)。
2. 内核参数调优
# 调大系统级最大 backlog(注意 listen() 受此限制)
sysctl -w net.core.somaxconn=65535
# accept 队列溢出时直接 RST,避免客户端超时等待
sysctl -w net.ipv4.tcp_abort_on_overflow=1
# 调大 SYN 半连接队列
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# 开启 SYN Cookie 缓解 SYN 洪泛
sysctl -w net.ipv4.tcp_syncookies=1
3. iptables connlimit 限速
iptables -A INPUT -p tcp --dport 8080 \
-m connlimit --connlimit-above 50 --connlimit-mask 32 -j REJECT
4. 应用层优化
- 使用非阻塞/多线程 accept,避免连接长时间在队列中等待
- 设置合理的连接超时(
SO_RCVTIMEO/SO_SNDTIMEO),及时关闭挂起连接
验证
| 指标 | 原始(backlog=16) | 修复(backlog=1024+非阻塞) |
|---|---|---|
| 队列满时 Send-Q | 17/16(满) | 0/1024(空) |
| 200 个填充连接 | 199 成功、1 失败 | 200 全部成功 |
| 新连接成功率 | 10% (1/10) | 100% (10/10) |
经验与避坑
- backlog 不是越大越好,但太小很容易被攻击或业务高峰打满。建议根据并发量设置合理值(如 128-1024)。
ss -lnt的Send-Q列表示 accept 队列中等待 accept 的连接数,顶满 = backlog 值 = 队列已满。- 判断是 accept 队列还是 SYN 队列满:
ss -ltn state syn-recv查看半连接队列数量;ss -lnt看 Send-Q 是否顶满。 tcp_syncookies=1是默认推荐配置,可缓解 SYN 洪泛导致的半连接队列溢出。- 应用层快速 accept 是最根本的解决手段——backlog 再大也只是延迟问题,accept 慢才会真正让队列堆积。
- Docker 容器内改
/proc/sys通常只读,需在宿主机或特权容器中执行sysctl。 - iptables 操作需 NET_ADMIN capability,普通容器没有该权限,需在宿主环境或特权容器中执行。
- 排查此问题时,注意
connlimit、tcp_max_syn_backlog等参数的综合效果。
