带宽没跑满却越来越慢:Linux TCP 半连接/全连接队列溢出排查实战,ss 与 nstat 定位法

为什么"带宽看着没跑满,网站却越来越慢"

这是个人站长最容易误判的一类故障。打开云服务商控制台,带宽曲线只有 20Mbps,买的却是 100Mbps,于是判断"服务器性能过剩、瓶颈在前端"。但用户实际访问时,页面要转两三秒才出首屏。

这类现象绝大多数不是带宽问题,而是连接建立过程中的 TCP 队列溢出。它有几个非常典型的特征:

  • 带宽、CPU、内存、磁盘 IO 四项监控全部正常,唯独响应时间飘忽;
  • abwrk 单机小并发压测时速度很快,一上线就慢;
  • 日志里看不到明显的 5xx,但 Nginx 的 $request_time 忽高忽低;
  • 高峰期报错集中在移动网络用户,宽带用户基本正常。

本文按"现象 → 定位 → 调参 → 验证"的顺序,把这条链路彻底拆开。所有命令都在一台 2 核 4G、跑 Typecho + Nginx + PHP-FPM 的普通 VPS 上实测过。

第一步:先分清是内核丢包还是上游丢包

Linux 内核把 TCP 连接建立分为两个队列,理解这两个队列是排查的全部基础。

半连接队列(SYN Queue)

客户端发 SYN,服务器回 SYN+ACK,此时连接还没建立完成,先进半连接队列,长度由上界 net.ipv4.tcp_max_syn_backlog 和实际生效值 net.core.somaxconn 中较小者决定。这个队列溢出时,客户端表现为"连不上"、超时重试,用户看到的是"打不开"。

全连接队列(Accept Queue)

三次握手完成,连接进入全连接队列,等待应用调用 accept() 把它取走。队列长度等于 listen(fd, backlog) 的 backlog 值与 net.core.somaxconn 中较小者。这个队列溢出时,连接虽然握上了,但被内核直接丢掉或按 tcp_abort_on_overflow 复位,用户看到的是"转圈后失败"或"偶发卡顿"。

Nginx 在 listen 指令里可以显式指定 backlog:

server {
    listen 443 ssl backlog=8192;
    listen 80 backlog=8192;
    # ...
}

但注意,如果 net.core.somaxconn 只有默认的 128 或 4096,listen 里写 8192 也不会生效——取的是两者最小值。这是最常被忽略的一层。

用 ss 直接读出队列溢出计数

不要靠猜,内核自己就记着数:

# 查看监听套接字的队列情况
ss -lnt

# 输出示例
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port
LISTEN  129     511     0.0.0.0:443         0.0.0.0:*
LISTEN  0       511     0.0.0.0:80          0.0.0.0:*

对于 LISTEN 状态的套接字,Recv-Q 表示当前全连接队列里等待 accept 的连接数Send-Q 表示全连接队列的最大长度。上面例子里 443 端口 Recv-Q=129,说明有 129 个连接排着队没被取走,而队列上限只有 511——已经用掉四分之一,高峰期必然溢出。

再看内核累计的溢出次数:

# 全连接队列溢出(常见元凶)
nstat -az | grep -i -E 'ListenOverflows|ListenDrops'

# 半连接队列溢出(SYN 攻击或爬虫刷)
nstat -az | grep -i -E 'TcpExtSyncookiesSent|TcpExtTCPReqQFullDrop'

# 实时观察(每 1 秒刷新一次差值)
watch -n1 'nstat -az | grep -E "ListenOverflows|ListenDrops"'

TcpExtListenOverflows 增长,就是全连接队列溢出的铁证;TcpExtSyncookiesSent 大量增长,说明半连接队列已经被打满,内核退化成 SYN Cookie 模式(这会丢失 TCP 选项,性能下降)。

第二步:别急着调参数,先看 accept 是否被阻塞

队列溢出的根因往往不是队列太小,而是应用取连接取慢了。Nginx 单 worker 的事件循环如果被阻塞,accept 就会被推迟。常见阻塞源:

  • 日志写入磁盘 IO 卡顿(同步写 access_log 到机械盘);
  • 磁盘满或 inode 耗尽,写日志报错反复重试;
  • 上游 PHP-FPM 全忙,Nginx 反向代理连接堆积;
  • 开启 reuseport 后队列分散到多个 worker,单 worker 队列看起来不大但全局仍在溢出。

先确认 Nginx 侧的状态:

# 需要先开启 stub_status
location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

curl -s http://127.0.0.1/nginx_status
# Active connections: 1240
# server accepts handled requests
#  981234 981200 4823110
# Reading: 12 Writing: 45 Waiting: 1183

关键看两点:accepts 与 handled 不相等,差值就是被丢弃的连接数;Waiting 长期高于 Active 说明大量 keepalive 空闲连接占着资源,可以适当调小 keepalive_timeout,把资源让给真正需要的新连接。

第三步:调参——分层设置,别只改一个

确认是队列问题后,按下面这套值调整。假设单机要扛 5000 并发连接:

cat >> /etc/sysctl.d/99-tcp-tuning.conf <<'EOF'
# 全连接队列上限(Nginx listen backlog 不得超过它)
net.core.somaxconn = 8192

# 半连接队列 / SYN 积压
net.ipv4.tcp_max_syn_backlog = 8192

# 每个网卡设备接收队列长度
net.core.netdev_max_backlog = 8192

# 端口回收与复用:加速 TIME_WAIT 回收,允许重用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 262144

# SYN 重试与丢弃策略(防御性)
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syncookies = 1
EOF

sysctl --system
# 验证
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog

几个必须说清的坑:

  1. net.core.somaxconn 改大后必须重启 Nginx(或 reload 后重建监听套接字),否则旧进程仍用旧的 backlog。
  2. tcp_tw_recycle 已被内核移除(5.0+ 彻底删除),网上老教程还在写它,别抄。
  3. 不要盲目把 somaxconn 调到 65535。队列越大,应用已经严重落后时你越不容易发现,而且会占内核内存。压测出合适值即可。
  4. 开了 reuseport 时,ss -lnt 会看到每个 worker 都有独立的 LISTEN 套接字,队列上限要按单 worker 算。

第四步:Nginx 侧配套参数

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;   # 单 worker 最大连接数
    multi_accept on;            # 一次事件循环尽量多 accept
    use epoll;
}

http {
    # keepalive 不宜过长,否则空闲连接占满 worker_connections
    keepalive_timeout 30;

    # 允许客户端快速重试
    reset_timedout_connection on;
    client_header_timeout 15;
    client_body_timeout 15;
    send_timeout 15;

    # 上游长连接,减少 TIME_WAIT 堆积
    upstream php-fpm {
        server 127.0.0.1:9000;
        keepalive 64;
    }
}

注意 worker_connectionsworker_rlimit_nofile 的关系:每个连接占一个 fd,反向代理场景一个客户端连接还要占用一个上游连接,所以 worker_rlimit_nofile 至少要是 worker_connections × 2。改完记得核对系统级限制:

# systemd 管理的 Nginx,改 LimitNOFILE 最稳
systemctl edit nginx
# [Service]
# LimitNOFILE=65535

systemctl daemon-reload && systemctl restart nginx
# 验证进程实际拿到的 fd 上限
cat /proc/$(pidof nginx | awk '{print $1}')/limits | grep 'open files'

第五步:用压测和监控闭环验证

# 模拟 500 并发、持续 60 秒
wrk -t 8 -c 500 -d 60s --latency https://www.example.com/

# 只看连接建立能力(不跑业务逻辑)
ab -n 20000 -c 800 -k http://127.0.0.1/

压测期间同时另开一个终端跑:

watch -n1 'nstat -az | grep -E "ListenOverflows|ListenDrops|SyncookiesSent"; ss -lnt | head'

验收标准应该是:整个压测过程中 ListenOverflows 零增长ss -lnt 里 Recv-Q 不持续维持在队列上限附近,P99 延迟平稳。如果加了参数之后溢出计数还在涨,那说明瓶颈在 accept 之外——回到第二步查日志 IO 和 PHP-FPM 是否阻塞。

一张速查表:症状 → 队列 → 处理

症状可疑队列确认命令处理方向
偶发"无法建立连接"、超时半连接队列SyncookiesSent 增长调大 tcp_max_syn_backlog,开 syncookies
转圈后失败、Nginx 无 5xx全连接队列ListenOverflows 增长somaxconn + listen backlog 同步调大
accepts 与 handled 不相等全连接队列stub_status查日志 IO / accept 被阻塞
Waiting 长期远高于 Active非溢出,是 keepalive 占用stub_status调小 keepalive_timeout
带宽未满但移动端慢TCP 层重传/队列ss -s、nstat综合调参 + 上 CDN

踩坑与排查顺序总结

  1. 不要一上来改 sysctl。nstat 拿到溢出计数,有数才有证据,否则改了半天不知道有没有用。
  2. net.core.somaxconn 是所有 backlog 的天花板,listen 里写再大也会被它压回。
  3. 改内核参数后 Nginx 必须重启(不是 reload)才会用新的 somaxconn 重建监听套接字。
  4. 压测机本身也会成为瓶颈——单机 wrk 到 1000 并发就受限于本机端口和 CPU,必要时多机施压。
  5. 调参只能缓解,真正的根治是消除 accept 阻塞源:把 access_log 换成 buffer=64k flush=5s,或直接上异步日志。
  6. 如果服务器在网络边缘(小带宽 VPS),先把静态资源交给 CDN,再谈 TCP 队列调优。

常见问题(FAQ)

Q:ss -lnt 的 Recv-Q 一直显示 0,还会有溢出吗?
不会同时出现。Recv-Q 是瞬时值,溢出是累计值。你在低谷期看当然是 0,要在高峰期或压测时看,同时用 nstat 看累计计数。

Q:开了 tcp_syncookies=1 之后半连接队列还有意义吗?
有意义。Syncookies 是队列满之后的兜底手段,代价是丢失 TCP 时间戳、窗口缩放等选项,会让长肥管道(长距离高带宽连接)性能下降。所以能靠调大队列解决就别依赖 Syncookies。

Q:为什么 ab 压测很快,真实用户却慢?
ab 从本机或同机房发起,RTT 极低,且不带浏览器行为(不拉 JS/CSS/图片、不复用连接)。真实用户从移动网络接入,RTT 高、丢包多,对队列溢出和 TCP 重传极其敏感。压测要加 --latency 看分位值,别只看平均值。

Q:改完 somaxconn 需要重启应用吗?
需要。监听套接字在进程启动时创建,backlog 参数此时才被内核记录。reload 不会重建套接字。

Q:队列调大之后,服务器内存会不会被吃光?
队列本身占用很小(每个排队连接一个内核 socket 结构),真正吃内存的是连接建立后的收发缓冲区。所以调大队列是安全的,失控的是连接数本身。

写在最后

"带宽没满但很慢"这个现象,九成以上的个人站都能用本文的路径定位到根因。核心心法只有一句:nstatss -lnt 拿到内核给的证据,再决定改哪个参数。跳过取证直接抄调优参数,是运维里最常见也最浪费时间的一种做法。

把这套检查固化成一个巡检脚本,每天早上看一眼溢出计数有没有增长,比任何事后救火都划算。对个人站长来说,可观测性永远比调优技巧更值钱

Last modification:September 22nd, 2026 at 08:24 pm

Leave a Comment