一个监听中的 TCP 套接字有两个队列,不是一个;而本文标题里那个 sysctl 管的,恰恰是几乎从不丢东西的那一个。传给 listen(2) 的 backlog 是 accept 队列,装的是已经完成握手、正等应用来取走的连接;SYN 队列装的才是半开请求。把两者混为一谈,就会开错药方。
这件事值得掰开,因为那句著名的建议——把 tcp_max_syn_backlog 调大——基本已经过时。tcp_syncookies 默认就是 1,而按 listen(2) 的说法,一旦启用 syncookies,SYN 队列就没有逻辑上的长度上限,这个设置会被忽略。真正会卡住你的是 accept 队列,以及应用调用 accept() 的速度。
backlog 参数管的是 accept 队列
listen(2) 明确写着,这个行为在 Linux 2.2 就变了:backlog 现在指的是「已完成建立、正等待被 accept 的套接字队列长度」,而不是半开请求的数量。半开队列由 /proc/sys/net/ipv4/tcp_max_syn_backlog 单独限制。而如果 backlog 参数超过 /proc/sys/net/core/somaxconn,它会被静默截断到那个值——Linux 5.4 之后是 4096,之前是 128。所以在默认配置的机器上写 listen(8192),实际拿到的是 4096,而且不会有任何日志告诉你。
tcp(7) 把 tcp_max_syn_backlog 描述为「尚未收到对端确认的排队连接请求上限」:默认 256,内存达到 128 MB 及以上时升到 1024,低于 32 MB 时降到 128。ip-sysctl 补上了真正重要的限定:它是 per-listener 的限制,每个半开请求套接字大约占 304 字节,而 tcp_syncookies 默认为 1——队列溢出时发一个 cookie,而不是把请求丢掉。
syncookies 开着的时候,SYN 队列就不再是瓶颈
ip-sysctl 把 tcp_syncookies 记录为一种兜底手段,并明确说不能用它去帮一台高负载服务器扛合法的连接吞吐;它还指出 syncookies 严重违反 TCP 协议、无法使用 TCP 扩展,并可能让某些服务明显劣化。listen(2) 更进一步:启用 syncookies 时没有逻辑上的长度上限,tcp_max_syn_backlog 被忽略。而 tcp(7) 仍然把同一个旋钮描述为「超过就开始丢弃请求」——那是 syncookies 关掉时的行为。两页手册各自都说对了,只是说的是不同配置。另外 ip-sysctl 注明,syncookies 只在编译时启用了 CONFIG_SYN_COOKIES 的内核上有效,所以它并不是一个必然存在的兜底。
所以诚实的结论是:让 syncookies 保持 1,别再折腾 SYN 队列。代价是这条兜底路径救回来的连接会失去窗口缩放、SACK 这类 TCP 扩展,属于吞吐问题而不是连通性问题。如果你在合法流量下看到 SYN flood 告警,ip-sysctl 自己的建议是去调别的旋钮,而不是关掉 syncookies。
压力真正落在 accept 队列上
SYN 队列被中和之后,会满的是 accept 队列,它的容量是 min(listen(backlog), somaxconn)。溢出时的行为取决于 tcp_abort_on_overflow,默认是 FALSE:ip-sysctl 说默认值意味着「如果溢出只是突发造成的,连接会恢复」,并警告打开这个开关会伤害你的客户端。机制才是意外之处:默认情况下内核会静默丢弃最后一个 ACK,于是客户端的 connect() 成功返回,故障要等到第一个请求卡住或被重置时才浮现。之所以说默认值「能恢复」,是因为服务端随后还会按自己的节奏重传 SYN-ACK:只要在它放弃之前队列里腾出位置,这条连接就能被救回来。这也正是它无声的原因——客户端那边从头到尾都认为自己已经连上了。
更大的 backlog 是延迟线,不是容量。在模型里,listen(128) 和 listen(65535) 一秒内 accept 的连接数完全一样,都是 20,000;更大的队列只是把溢出的时刻往后推,因为 accept 速率由 worker 数量决定。
坏掉的样子
第一种故障是无声的那种,而且它就是默认行为。accept 循环太慢的服务会把 accept 队列挤满,内核丢掉最后的 ACK,客户端看到的是「连接建立了,然后就挂着」。没有任何东西被拒绝,于是客户端的连接池里堆满死套接字,报错最终出现在一个看起来无关的层面。盯住 TcpExtListenOverflows 和 TcpExtListenDrops:这两个计数器上涨而 accept 速率平稳,说明瓶颈在处理线程。
第二种故障是被丢弃的 SYN,只有在 syncookies 关掉时才会发生。SYN 队列满了就丢请求,客户端按重传时间表重试,文档给出的两个端点是「最后一次重传到 67 秒、最终超时到 131 秒」,所以用户看到的现象是一次挂了两分多钟才失败的连接。在这种配置下调大 tcp_max_syn_backlog 是真正的修法,而把 syncookies 重新打开是更好的修法——前提是你接受 TCP 扩展的损失。
快速验证
ss -lnt 'sport = :8443'
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPReqQFullDrop TcpExtTCPReqQFullDoCookies
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_syncookies net.ipv4.tcp_abort_on_overflow
在 ss -lnt 里,Send-Q 是这个套接字创建时的 backlog,Recv-Q 是已经建立但还没被 accept 的连接数;Recv-Q 顶到 Send-Q 就是队列满了。nstat 的几个计数器应该为零或接近零:TcpExtListenOverflows 和 TcpExtListenDrops 属于 accept 队列事件,TcpExtTCPReqQFullDrop 是被丢掉的 SYN,TcpExtTCPReqQFullDoCookies 则是兜底机制在正常工作。sysctl 那一行显示内核真正会认的数字——如果 somaxconn 比你的 listen() 参数小,那么那个更小的值才是你真实的 backlog。
来源
- 内核 ip-sysctl 文档,somaxconn、tcp_max_syn_backlog、tcp_abort_on_overflow 以及 syncookies 的警告,已核验 2026-09-16。
- Linux listen(2),Linux 2.2 起的 accept 队列、somaxconn 的静默截断,以及 syncookies 如何取消 SYN 队列上限,已核验 2026-09-16。
- Linux tcp(7),tcp_max_syn_backlog 按内存大小的默认值,以及 syncookies 的注意事项,已核验 2026-09-16。