下面的默认值取自一台 Ubuntu 24.04、内核 6.8.0-149-generic 的机器,且确认过 /etc/sysctl.conf 和 /etc/sysctl.d/、/usr/lib/sysctl.d/、/run/sysctl.d/ 里没有覆盖,所以是内核自己的默认值:

grep -rn "tcp_tw_reuse\|rmem_max\|tcp_rmem\|somaxconn" \
  /etc/sysctl.conf /etc/sysctl.d/ /usr/lib/sysctl.d/ /run/sysctl.d/ 2>/dev/null
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.core.rmem_max net.core.wmem_max \
       net.ipv4.tcp_fin_timeout net.ipv4.tcp_tw_reuse net.ipv4.tcp_max_tw_buckets \
       net.ipv4.tcp_slow_start_after_idle net.ipv4.tcp_notsent_lowat \
       net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.core.default_qdisc
net.ipv4.tcp_rmem = 4096	131072	6291456
net.ipv4.tcp_wmem = 4096	16384	4194304
net.core.rmem_max = 212992
net.core.wmem_max = 212992
net.ipv4.tcp_fin_timeout = 60
net.ipv4.tcp_tw_reuse = 2
net.ipv4.tcp_max_tw_buckets = 65536
net.ipv4.tcp_slow_start_after_idle = 1
net.ipv4.tcp_notsent_lowat = 4294967295
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 1024
net.core.default_qdisc = fq_codel

第一条 grep 没有输出很重要:它证明上面的数字不是发行版调过的结果。

坑一:tcp_fin_timeout 不控制 TIME_WAIT

这是流传最广的一条误配:

sysctl -w net.ipv4.tcp_fin_timeout=15

配它的理由是"把 TIME_WAIT 从 60 秒缩短到 15 秒"。这个理由不成立。

tcp_fin_timeout 控制的是 FIN-WAIT-2 的超时:本端发了 FIN、对端 ACK 了,但对端还没发它自己的 FIN,本端要等多久。

TIME_WAIT 时长在 Linux 里是编译期常量 TCP_TIMEWAIT_LEN,值就是 60 秒,没有对应的 sysctl,改不了。

调小它不会减少 TIME_WAIT 连接数,但会带来真实风险:对端处理慢、FIN 发得晚时,15 秒后本端直接放弃连接,没传完的数据就丢了。表现是偶发连接被重置,而监控上 TIME_WAIT 数量没变化,于是继续往下调。

真正处理 TIME_WAIT 堆积有三个方向:连接复用(keepalive、连接池)、减少短连接数量、用 tcp_tw_reuse。

tcp_tw_reuse 本机默认是 2。内核文档里这个键的含义是:0 关闭,1 对环回和非环回都开启,2 只对环回开启。生效前提是 tcp_timestamps 打开(本机默认 1),因为它靠时间戳判断这个 TIME_WAIT socket 能否安全复用。

只对本机环回生效,意味着默认配置下它对外网连接完全不起作用,想让它对出站连接生效要显式写 1。至于它从哪个内核版本开始默认变成 2,我没翻提交历史,不确定;早期文档里写的默认值是 0。

顺便说 tcp_tw_recycle,这个键在 Linux 4.12 被移除了。它在 NAT 环境下会导致来自同一公网 IP 的不同客户端连接被丢弃,因为内核按 IP 而不是按四元组记录时间戳。现在还能搜到推荐打开它的文章,都是 2017 年之前写的。

坑二:在 app_limited 的连接上测调优效果

调完要验证,而验证通常意味着跑一次传输看指标,这一步最容易白做。

抓一个正在传数据的连接的内部状态:

# 一个终端里保持连接
( exec 3<>/dev/tcp/127.0.0.1/8080; printf 'GET / HTTP/1.1\r\nHost: x\r\n\r\n' >&3; sleep 3 ) &
sleep 0.6
ss -tin | grep -A1 '127.0.0.1:8080'
ESTAB      0      0          127.0.0.1:8080        127.0.0.1:33682
	 cubic wscale:7,7 rto:201 rtt:0.013/0.007 ato:40 mss:54464 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:9934 bytes_acked:9934 bytes_received:27 segs_out:5 segs_in:7 data_segs_out:2 data_segs_in:3 send 335163076923bps lastsnd:605 lastrcv:605 lastack:605 pacing_rate 628053333328bps delivery_rate 108928000000bps delivered:3 app_limited rcv_space:65483 rcv_ssthresh:65483 minrtt:0.004 snd_wnd:108928

几个关键信息:

cwnd:10——拥塞窗口停在 10 个报文段,正是 RFC 6928 定义的初始拥塞窗口(IW10),也是它的终值:这条连接总共只传了 9934 字节,两个数据段就传完了,慢启动根本没机会把窗口撑开。

app_limited——发送速率受限于应用层给数据的速度,而不是网络。内核明确告诉你:这条连接的吞吐量不能反映路径能力。

send 335163076923bps、delivery_rate 108928000000bps——这是环回,三百多 Gbps 没有任何物理意义。

在这种连接上做调优对比——改 tcp_rmem、换拥塞算法、调 tcp_notsent_lowat——得到的差值全是噪声。更糟的是它可能给出看起来显著的结果,因为环回的绝对值太大,任何百分比变化都很醒目。

要测出有意义的东西,链路必须同时满足:有真实的带宽约束、数据量足够让拥塞窗口涨过初始值、时间足够长让慢启动结束。环回三条都不满足。

我的做法是验证时只看 ss -tin 里 app_limited 有没有出现。出现了就说明这次采样无效,不管数字多好看都丢掉重测。

坑三:调了 tcp_rmem,但被应用自己绕过了

tcp_rmem 三个值是接收缓冲区的最小、默认、最大,单位字节,本机最大是 6291456,即 6MB。

net.core.rmem_max 是 212992,约 208KB。两个数字相差 30 倍,很多人会以为 rmem_max 把 tcp_rmem 限制住了,于是去调 rmem_max。这个理解反了。

这两个键管的是两条不同的路径:

  • 应用没有调用 setsockopt(SO_RCVBUF) 时,内核走自动调优,缓冲区在 tcp_rmem 三元组内按带宽时延积动态调整,rmem_max 不参与。
  • 应用显式设置了 SO_RCVBUF 时,内核关闭这条 socket 的自动调优,缓冲区就是应用要的值,而这个值被 rmem_max 封顶(非特权进程;特权进程用 SO_RCVBUFFORCE 可突破)。

真正的陷阱是第二种:一个把 SO_RCVBUF 写死在代码里的应用,你无论怎么调 tcp_rmem 都没有效果。要让它受益,得改应用代码,或者调大 rmem_max 让应用能要到更大的值。

判断走哪条路径看 ss -tim:自动调优的连接 rcv_space 随传输变化,被应用固定住的则稳定在一个数字上。

反向也成立:把 rmem_max 调到很大,所有显式设置缓冲区的应用都更容易申请到大内存,单连接占用的内核内存上升。连接数多时这是实打实的内存开销,而收益只在高带宽时延积链路上才存在。

somaxconn 和 tcp_max_syn_backlog 同理。本机分别是 4096 和 1024,但 listen() 的 backlog 取两者的较小值,而应用传进去的往往更小——很多框架默认 128 或 511。前面 nginx 监听 socket 的 Send-Q 是 511,就是它自己传的值。调 sysctl 而不调应用的 backlog,等于没调。

三条通用的判断

一、改之前先把默认值和出处记下来,确认你看到的数字是内核默认还是发行版改过的,两者回退方式不同。

二、一次只改一个键,改完立刻用 ss -tin 看连接的实际状态而不是吞吐量。吞吐量是结果,cwnd、rcv_space、app_limited、retrans 才是原因。

三、大多数"TCP 调优清单"是给长肥管道写的。服务是内网短请求或者环回上的进程间通信,那些参数一个都不该动。

我不建议在没有测量之前调任何 TCP 参数。默认的 cubic + 自动调优在绝大多数场景下已经够好,而调错的代价是偶发、难复现的连接问题——排查成本极高,而且通常在调完几个月之后才出现,那时没人记得改过什么。

分情况的一点是拥塞控制算法。本机 tcp_available_congestion_control 只有 reno cubic,BBR 要单独加载模块。在有明显丢包的高延迟链路上 BBR 和 cubic 差别很大,但那是另一套判断,不能和内网调优混为一谈。