适用场景
这篇文章适用于线上服务出现“偶发连接超时、重试后成功、服务 CPU 不高但新连接建立慢”的问题,尤其是 Nginx、网关、Java/Python Web 服务、RPC 服务或四层代理在流量突增时出现以下现象:
- 客户端报
connection timed out、connect 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 能力。
排查思路
排查目标是确认问题发生在“连接建立阶段”还是“请求处理阶段”。建议按下面顺序推进:
- 从客户端确认是连接超时还是读超时。
- 在服务端查看监听端口的队列堆积。
- 查看内核是否记录过 listen 队列溢出。
- 检查服务进程 accept 能力和 worker 状态。
- 核对应用 backlog 与系统参数。
- 根据根因调整服务并验证。
常用命令
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:8080 的 Recv-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很多且 LISTENRecv-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 为例,关注 acceptCount、maxConnections 和线程池:
<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 是缓冲区,不是处理能力本身;如果应用线程长期被慢调用占满,队列再大也只能延后故障暴露。
Discussion
评论