适用场景

这篇文章适用于线上服务出现“偶发连接超时、重试后成功、服务 CPU 不高但新连接建立慢”的问题,尤其是 Nginx、网关、Java/Python Web 服务、RPC 服务或四层代理在流量突增时出现以下现象:

  • 客户端报 connection timed outconnect timeout 或偶发 502/504
  • 服务端业务日志没有对应请求记录,说明请求可能还没有进入应用层。
  • top 看 CPU 不高,free 看内存也正常,但新建连接失败或明显变慢。
  • 重启应用后短暂恢复,流量高峰一来又复现。

这类问题经常不是应用处理逻辑慢,而是 TCP 握手完成后的连接没有被应用及时 accept,导致 accept 队列被打满。

现象描述

一次线上接口高峰期,调用方反馈部分请求连接超时:

connect timeout after 3000ms
upstream timed out while connecting to upstream

服务端排查时有几个容易误导人的现象:

  • 应用进程还活着,健康检查偶尔成功。
  • CPU 使用率只有 30% 左右,没有明显打满。
  • 数据库和 Redis 没有慢查询峰值。
  • 应用日志没有记录到失败请求。

如果请求根本没有进入应用日志,优先把排查点放到连接建立、监听队列、反向代理和内核网络栈上。

可能原因

TCP 服务端新连接进入应用前,通常会经过两个关键队列:

  • SYN 队列:保存半连接,客户端发送 SYN 后等待三次握手完成。
  • accept 队列:保存已经完成三次握手、等待应用调用 accept() 取走的连接。

accept 队列溢出的常见原因包括:

  • 应用处理线程或 worker 不足,无法及时接受新连接。
  • 应用在启动、GC、阻塞调用、磁盘 IO 或锁等待中卡住。
  • listen(backlog) 设置偏小。
  • 内核参数 net.core.somaxconn 偏小,限制了实际 backlog 上限。
  • Nginx、Gunicorn、uWSGI、Tomcat 等服务自身连接队列或 worker 配置偏小。
  • 短连接流量突增,连接创建速率超过应用 accept 能力。

排查思路

排查目标是确认问题发生在“连接建立阶段”还是“请求处理阶段”。建议按下面顺序推进:

  1. 从客户端确认是连接超时还是读超时。
  2. 在服务端查看监听端口的队列堆积。
  3. 查看内核是否记录过 listen 队列溢出。
  4. 检查服务进程 accept 能力和 worker 状态。
  5. 核对应用 backlog 与系统参数。
  6. 根据根因调整服务并验证。

常用命令

1. 查看监听端口队列

假设服务监听 8080

ss -lnt sport = :8080

示例输出:

State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 128    128    0.0.0.0:8080      0.0.0.0:*

关键字段解释:

  • Recv-Q:对 LISTEN socket 来说,表示当前 accept 队列中等待应用取走的连接数。
  • Send-Q:对 LISTEN socket 来说,表示该监听 socket 的 accept 队列上限。

如果 Recv-Q 长时间接近或等于 Send-Q,说明应用 accept 新连接不及时,连接可能被丢弃或超时。

2. 查看所有监听端口的队列占用

ss -lntp

重点看高流量端口:

State  Recv-Q Send-Q Local Address:Port Process
LISTEN 0      511    0.0.0.0:80        users:(("nginx",pid=1201,fd=6))
LISTEN 256    256    127.0.0.1:8080    users:(("java",pid=2234,fd=99))

这里 127.0.0.1:8080Recv-Q 已经等于 Send-Q,表示后端服务 accept 队列满,而前面的 Nginx 仍可能正常监听。

3. 查看内核统计

netstat -s | egrep -i "listen|overflow|drop|reset"

常见输出:

12345 times the listen queue of a socket overflowed
12345 SYNs to LISTEN sockets dropped

如果这两个计数持续增长,说明内核已经观察到监听队列溢出。建议连续观察两次:

netstat -s | egrep -i "listen|overflow|drop"
sleep 10
netstat -s | egrep -i "listen|overflow|drop"

如果 10 秒内增长明显,说明问题正在发生。

4. 查看 SYN 队列和连接状态

ss -ant state syn-recv sport = :8080
ss -ant state established sport = :8080 | wc -l

判断方式:

  • SYN-RECV 很多:优先怀疑 SYN 队列、SYN flood、防火墙或网络抖动。
  • ESTABLISHED 很多且 LISTEN Recv-Q 高:优先怀疑应用 accept 或处理能力。
  • TIME-WAIT 很多:可能是短连接压力,需要结合临时端口、连接复用和 keepalive 继续排查。

5. 查看系统 backlog 上限

sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.ipv4.tcp_abort_on_overflow

字段含义:

  • net.core.somaxconn:限制 accept 队列 backlog 的系统上限。
  • net.ipv4.tcp_max_syn_backlog:限制 SYN 队列大小。
  • net.ipv4.tcp_abort_on_overflow:accept 队列满时是否直接 RST。通常不建议随意打开,避免把短暂抖动变成大量连接失败。

定位示例

某业务服务由 Nginx 转发到本机 Java 服务:

upstream app_backend {
    server 127.0.0.1:8080;
    keepalive 128;
}

高峰时 Nginx 错误日志出现:

upstream timed out (110: Connection timed out) while connecting to upstream

在后端机器执行:

ss -lntp sport = :8080

输出如下:

State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 100    100    127.0.0.1:8080    0.0.0.0:* users:(("java",pid=2234,fd=99))

继续查看内核统计:

netstat -s | egrep -i "listen|overflow|drop"

发现:

8432 times the listen queue of a socket overflowed
8432 SYNs to LISTEN sockets dropped

10 秒后再次执行,数字继续增加。此时可以判断:后端服务 accept 队列被打满,Nginx 到后端的新连接无法稳定建立。

再检查应用线程:

top -Hp 2234
jstack 2234 | less

发现大量业务线程阻塞在远程 HTTP 调用上,导致应用 worker 被占满,间接影响新连接接受和处理。

修复方案

1. 提升系统监听队列上限

临时调整:

sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=4096

持久化配置:

cat >/etc/sysctl.d/99-listen-backlog.conf <<'EOF'
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
EOF

sysctl --system

注意:只调大内核参数不一定生效,应用启动时传入的 backlog 也要足够大。

2. 调整应用或网关 backlog

不同服务配置位置不同,下面是常见示例。

Nginx 监听 backlog:

server {
    listen 80 backlog=4096;
}

Gunicorn:

gunicorn app:app --bind 0.0.0.0:8000 --backlog 2048 --workers 4

uWSGI:

listen = 2048
processes = 4
threads = 8

Java 服务需要结合具体容器。以 Tomcat 为例,关注 acceptCountmaxConnections 和线程池:

<Connector port="8080"
           protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="300"
           maxConnections="10000"
           acceptCount="1000" />

3. 增加应用处理能力

如果根因是 worker 被慢调用拖住,单纯调大 backlog 只是缓冲更多连接,不能真正解决延迟。需要同步治理应用层问题:

  • 给外部 HTTP、数据库、Redis 调用设置明确超时。
  • 使用连接池并限制最大并发,避免无限堆积。
  • 增加 worker、线程池或实例数量。
  • 把慢任务移到异步队列,不占用请求线程。
  • 对突发流量增加限流和排队策略。

4. 开启连接复用

如果 Nginx 到后端是短连接,高峰期会放大 accept 压力。可以启用 upstream keepalive:

upstream app_backend {
    server 127.0.0.1:8080;
    keepalive 256;
}

server {
    location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass http://app_backend;
    }
}

验证时观察后端新建连接速率是否下降:

sar -n TCP,ETCP 1 10
ss -ant sport = :8080 | awk 'NR>1 {count[$1]++} END {for (s in count) print s,count[s]}'

验证方法

修复后至少验证三类指标。

第一,监听队列不再长期打满:

watch -n 1 'ss -lnt sport = :8080'

期望结果:

Recv-Q 明显低于 Send-Q,不再长时间等于上限

第二,内核溢出计数不再增长:

netstat -s | egrep -i "listen|overflow|drop"
sleep 60
netstat -s | egrep -i "listen|overflow|drop"

第三,业务侧超时下降:

  • Nginx upstream timed out while connecting to upstream 明显减少。
  • 客户端 connect timeout 明显减少。
  • 应用 p95、p99 延迟恢复到正常范围。
  • 实例重启或发布期间不会立刻出现队列打满。

预防措施

  • 服务上线前明确 backlog、worker、连接池、超时和限流配置。
  • 监控 ss -lnt 中关键端口的 Recv-Q/Send-Q,高峰期接近上限时告警。
  • 采集 netstat -s 中 listen overflow/drop 计数,按增长率告警。
  • Nginx 到后端优先使用 keepalive,减少短连接冲击。
  • 应用依赖外部服务时必须设置连接超时和读取超时。
  • 发布、扩容、重启时使用优雅下线,避免瞬时连接集中打到少数实例。
  • 压测时覆盖连接建立速率,而不只看业务 QPS。

总结

Linux accept 队列溢出的问题很容易被误判为应用偶发慢、Nginx 502 或网络抖动。判断关键是看服务端监听端口的 Recv-Q/Send-Q,再结合 netstat -s 中 listen queue overflow/drop 计数确认。

治理时不要只调大 somaxconn。正确做法是同时检查应用 backlog、worker 数量、线程阻塞、连接复用和限流策略。backlog 是缓冲区,不是处理能力本身;如果应用线程长期被慢调用占满,队列再大也只能延后故障暴露。