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