大家好,我是内核喵。
前几天线上有一台高并发服务突发偶发性连接超时,业务层监控看到一堆 connection reset by peer 和超时重试。业务同学跑来问我:“是不是网络抖动,或者连接池被打爆了?”
我连上去看了一下网络指标,直接把结论甩回去了:TCP 全连接队列(Accept Queue)被打满了,ACK 包被内核静默丢弃。
很多人一看到连接超时就习惯性地调 tcp_max_syn_backlog,甚至盲目开启 tcp_tw_recycle(这货在 Linux 4.12 之后都被废弃了还抄呢)。今天我们较真一把,把 Linux 内核的三次握手队列、溢出排查路径以及内核参数调优从头到尾扒干净。
一、 两个队列:内核里的三次握手究竟怎么走?
在 TCP 三次握手期间,Linux 内核为每一个处于 LISTEN 状态的 socket 维护了两个队列:
- 半连接队列(SYN Queue):
服务端收到客户端的
SYN包,回复SYN+ACK后,连接进入SYN_RECV状态,此时连接暂存在半连接队列中。 - 全连接队列(Accept Queue):
客户端回复最后的
ACK包,三次握手完成,连接状态变为ESTABLISHED,此时连接从半连接队列移出,进入全连接队列,等待用户态进程调用accept()将其取走。
用内核源码来说话,关键逻辑在 net/ipv4/tcp_ipv4.c 的 tcp_v4_conn_request() 和 tcp_v4_syn_recv_sock() 里:
- 当全连接队列满时,不仅最后的
ACK会出问题,甚至连最初的SYN都可能直接被 drop 掉(为了防止半连接状态堆积后无法转正)。
二、 现场取证:全连接队列溢出排查
遇到问题别猜,先看指标。
1. 查看全连接队列当前状态 (ss)
注意,ss 命令在不同 socket 状态下,Recv-Q 和 Send-Q 的含义完全不同!这是很多新手的坑。
$ ss -lnt '( sport = :8080 )'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 129 128 0.0.0.0:8080 0.0.0.0:*
LISTEN状态下:Send-Q:全连接队列的最大容量(Listen backlog),由min(backlog, net.core.somaxconn)决定。Recv-Q:当前正在等待应用层accept()的连接数量。
- 现象判断:
上面的输出中,
Recv-Q (129) > Send-Q (128)!这说明当前全连接队列已经满了,新的连接根本进不来。
2. 检查内核统计计数器 (nstat / netstat)
执行以下命令查看溢出历史:
$ nstat -az TcpExtListenOverflows TcpExtListenDrops
#kernel
TcpExtListenOverflows 12405 0.0
TcpExtListenDrops 12405 0.0
TcpExtListenOverflows:全连接队列满了,导致无法容纳新握手完成的连接的次数。TcpExtListenDrops:所有因为资源问题导致监听 socket 丢弃包的次数(包含了全连接队列满的情况,也包括分配内存失败等)。
如果这两个值隔几秒看一次还在不断递增,说明此时此刻正在发生丢包!
3. 动态追踪:bpftrace 抓现场
如果想看具体是哪个进程在慢腾腾地 accept(),或者到底是谁丢的包,直接祭出 bpftrace 挂载到内核函数 tcp_v4_syn_recv_sock:
// trace_overflow.bt
#include <net/sock.h>
#include <net/tcp.h>
kprobe:tcp_v4_syn_recv_sock
{
$sk = (struct sock *)arg0;
$ack_backlog = $sk->sk_ack_backlog;
$max_ack_backlog = $sk->sk_max_ack_backlog;
if ($ack_backlog > $max_ack_backlog) {
time("%H:%M:%S ");
printf("comm:%s drop connection, backlog:%d, max:%d\n",
comm, $ack_backlog, $max_ack_backlog);
}
}
运行输出:
# bpftrace trace_overflow.bt
Attaching 1 probe...
14:23:01 comm:gunicorn drop connection, backlog:129, max:128
14:23:01 comm:gunicorn drop connection, backlog:129, max:128
很清晰:gunicorn 工作进程处理不过来,accept() 太慢,队列塞满后直接触发内核丢包。
三、 全连接队列满了,内核会怎么对待客户端?
这就涉及到一个极其关键的参数:net.ipv4.tcp_abort_on_overflow。
tcp_abort_on_overflow = 0(默认值): 内核很“温柔”,它会直接把客户端发来的第三次握手ACK丢弃,假装没收到。- 后果:服务端处于
SYN_RECV,客户端以为握手成功处于ESTABLISHED。客户端发数据时,服务端会重传SYN+ACK。客户端如果超时没有收到响应,会反复重传数据,直到超时报错。
- 后果:服务端处于
tcp_abort_on_overflow = 1: 内核很“暴躁”,既然队列满了,直接回复一个RST包给客户端。- 后果:客户端立即收到
connection reset by peer,快速失败。
- 后果:客户端立即收到
调优建议: 绝大多数情况下保持默认值
0,给服务端应用一个短暂抖动缓冲(重试)的机会;但在一些内部 RPC、有完善快速熔断/重试机制的微服务架构中,设置为1可以避免客户端无谓的重传阻塞。
四、 半连接队列(SYN Queue)溢出与 SYN Flood
如果 SYN 包发得太多,半连接队列爆了怎么办?
半连接队列大小由以下公式共同决定:
backlog = min(somaxconn, backlog)
nr_table_entries = max(backlog, 8)
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1)
sysctl_max_syn_backlog = net.ipv4.tcp_max_syn_backlog
// 最终上限大致受限于 tcp_max_syn_backlog
排查半连接队列溢出:
$ netstat -s | grep "SYNs to LISTEN sockets dropped"
5123 SYNs to LISTEN sockets dropped
调优方案:
- 扩大半连接队列:
bash
sysctl -w net.ipv4.tcp_max_syn_backlog=4096 - 开启 SYN Cookies:
bash较真点:
sysctl -w net.ipv4.tcp_syncookies=1tcp_syncookies并不是“队列满了才启用”,而是在半连接队列达到阈值后,跳过半连接队列的分配,直接把连接元信息(时间戳、MSS 等)编码进SYN+ACK的ISN(初始序列号)里。只有客户端返回合法的 ACK 时才解包建连。这能免疫 SYN Flood 攻击,但可能会牺牲一部分 TCP 扩展选项(如特定窗口缩放)。
五、 参数调优实战:正确姿势
调整连接队列,必须内核参数与应用层配置协同配合,单改一方等于没改。
1. 调整内核全局限制
编辑 /etc/sysctl.conf:
# 全连接队列全局上限(默认一般只有 128 或 4096,建议提升)
net.core.somaxconn = 8192
# 半连接队列全局上限
net.ipv4.tcp_max_syn_backlog = 8192
# 开启 SYN Cookies 防御
net.ipv4.tcp_syncookies = 1
# 适度增加全连接队列满时的行为考量(视业务场景而定,通常保持 0)
net.ipv4.tcp_abort_on_overflow = 0
执行 sysctl -p 立即生效。
2. 调整应用层 listen() backlog
系统参数调大了,应用层如果不调,也是白搭。因为内核取的是: $$\text{Queue Size} = \min(\text{backlog}, \text{somaxconn})$$
- Nginx:
nginx
server { # 显式指定 backlog 大小 listen 8080 default_server backlog=8192; } - Golang:
Go 的
net.Listen默认会读取/proc/sys/net/core/somaxconn的值填入底层listen(fd, backlog),所以 Go 服务只需要把系统somaxconn调大即可。 - Tomcat / Netty:
xml
<Connector port="8080" protocol="HTTP/1.1" acceptCount="8192" ... /> - Redis:
在
redis.conf中修改tcp-backlog 8192。
总结
连接队列溢出不是玄学,一套组合拳就能准确定位:
ss -lnt看实时(LISTEN状态下的Recv-Q和Send-Q)。nstat -az TcpExtListenOverflows看历史丢包趋势。bpftrace定位到底是哪个进程拖累了accept()。- 调优必须双管齐下:改高内核
somaxconn+ 改大应用配置里的backlog。 - 永远记住:队列只是缓冲池,真正治本的方法是优化后端处理延迟,让应用更快地调用
accept()处理连接。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


