作者视角:10 年运维,长期盯线上稳定性和网络排障。这篇不讲「怎么配 nginx」,而是讲一个反复被当成「防火墙 / 抖动」的坑——内核的连接队列溢出了,应用根本没收到那个连接。全文给可复现的排查命令、参数取值和一张反直觉清单。
一、现象与背景
去年 11 月,我们一个 Go 写的高频 API 网关(监听 :8080,单机常驻 QPS 约 2000)在一次大促预热压测里开始出问题:
- 压到 QPS ≈ 8000 时,客户端开始零星报
502和connect timeout,占比约 0.7%; - P99 延迟从平时的 80ms 飙到 3s;
- 诡异的是:故障是偶发的,重启进程立刻消失,扩容一台机器也压不住,监控上 CPU、内存、GC 全绿。
一开始全员以为是防火墙丢包、或者对端网络抖动。抓了两天包也没看到明显异常。真正定位到根因,靠的是一个平时没人看的计数器。
二、原理 / 根因:内核其实有两队,不是你以为的一队
很多人以为「三次握手 → 应用 accept」是一条流水线,其实内核维护着两个独立的队列:
- 半连接队列(SYN queue):收到 SYN、回完 SYN-ACK、但还没收全 ACK 的连接。容量受
net.ipv4.tcp_max_syn_backlog约束。 - 全连接队列(accept queue / backlog):三次握手已完成、但应用还没调 accept() 取走的连接。容量由
listen(fd, backlog)决定,且上限被net.core.somaxconn截断。
关键机制:当全连接队列满了,新完成握手的连接会被内核直接丢弃(默认行为),或者触发一个 RST;客户端于是重试、超时,最终就是你看到的偶发 502 / connect timeout。而应用侧的线程池此时往往并不满——问题不是「应用慢」,而是应用 accept 的速度跟不上握手完成的速度。
为什么偶发?因为队列是弹性缓冲:平时流量小,队列几乎空着;瞬时尖峰一来,队列瞬间灌满,然后被 accept 慢慢吃掉。你抓到包的那几秒恰好是队列已排空的时刻,所以「抓包看不出问题」。
ss -lnt 里的两列就是答案钥匙:
- Send-Q = 该监听 socket 的 backlog 上限(
min(代码里的backlog, somaxconn)); - Recv-Q = 全连接队列当前长度。
只要看到 Recv-Q 逼近甚至等于 Send-Q,就说明应用 accept 追不上握手,队列在打满。
三、具体做法(可复现)
1. 看队列水位(最直接的信号)
ss -lnt | grep :8080
# 正常:Recv-Q 很小(0~几十),Send-Q 是上限
# 危险:Recv-Q 逼近 Send-Q,例如 Recv-Q=1024 Send-Q=1024
2. 看内核到底丢了多少(这是「抓包看不到」的真相源)
netstat -s | grep -iE 'listen|overflow|reset'
# 重点看:
# "times the listen queue of a socket overflowed" ← 全连接队列溢出次数
# "SYNs to LISTEN sockets dropped" ← 半连接队列溢出
# "TCP: connection request dropped"
压测前后各跑一次,对比这两个计数器的增量,比抓包可靠得多。
3. 抓包看客户端侧的重传(佐证)
tcpdump -i any -nn 'tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0'
# 若同一目的 IP:port 连续出现多条 SYN(重传),说明对端没收到 SYN-ACK / 连接没被接走
4. 调参(治标,但常常立刻见效)
# 看当前上限
sysctl net.core.somaxconn
# 老内核(≈4.0 及以前)默认 somaxconn=128 —— 这基本就是「Send-Q=128」的来源
# 内核 4.1+ 默认已是 4096
# 抬高上限(应用 listen 的 backlog 也要同步改大,否则 min() 还是取小值)
sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
5. 治本:让应用 accept 得更快
队列溢出本质是「应用取连接太慢」。常见元凶:
- Go:
net层没问题,多半是上游 handler 阻塞占住 goroutine 调度或同步锁; - Java/Netty:accept 线程被慢任务拖住,或 epoll 事件循环里有同步阻塞调用;
- 通用:慢 SQL / 慢依赖让请求处理变慢 → accept 节奏被拖慢 → 队列堆积。
四、踩过的坑 / 反直觉之处
- backlog 不是越大越好:它受
somaxconn截断,也占内核内存;somaxconn=128的老内核上,你把代码 backlog 写成 65535 也白搭,Send-Q还是 128。改代码 backlog 却没改 somaxconn,是「参数改了不生效」最常见的原因。 tcp_abort_on_overflow=1的副作用:开它会让队列溢出时直接回 RST,客户端立刻失败而不是重试超时——看起来「变快了」,其实是把「超时」换成了「连接被 reset」,治标且对短连接不友好,生产默认保持 0。- 锅甩给防火墙/网络,其实不是:0.7% 的偶发失败 + 全绿监控,90% 概率是队列溢出,而不是对端网络。先
netstat -s看 overflow 计数,再怀疑网络。 - 重启就好的「玄学」假象:重启清空了队列、也重置了某些慢状态,掩盖了 accept 变慢的真实原因,导致你以为扩容/重启能解决。
Send-Q不等于「配置值」:它是min(代码backlog, somaxconn),两个都要对得上才算数。
五、结论与可复用清单
排查「偶发 502 / connect timeout」时,按这个顺序走:
ss -lnt | grep :端口→ 看Recv-Q是否逼近Send-Q;netstat -s | grep -iE 'listen|overflow'→ 看队列溢出计数器的增量;- 若溢出:先
sysctl net.core.somaxconn确认上限(老内核警惕 128),再同步调大代码 backlog 与somaxconn; - 治本:查应用 accept 为何变慢(慢 SQL / 阻塞 handler / 事件循环被占);
- 不要乱开
tcp_abort_on_overflow;别在没看 overflow 计数前就把锅甩给网络。
一句话:连接队列溢出是「应用没收到连接」,不是「网络没送到」——Recv-Q 贴近 Send-Q 就是它的指纹。
【想听大家聊的】你们线上遇到过「监控全绿、接口偶发超时」的情况吗?最后根因是队列溢出,还是别的东西?你们 somaxconn 现在设的是多少、为什么?