nf_conntrack 连接数上限:VPS 高并发下 TIME_WAIT 与端口耗尽的坑

解释 Linux 连接跟踪表 nf_conntrack 满表、TIME_WAIT 堆积、本地端口耗尽的成因与症状(Nginx/代理突然连不上),给出调大 conntrack_max、开启 tcp_tw_reuse、调优端口范围的实操,以及 DDoS 下的防护副作用。

结论先说在前面:Linux 上相当一部分"明明机器没满、网络却时好时坏、代理和 Nginx 偶尔连不上"的怪毛病,根子不在 CPU,也不在带宽,而在一个你平时看不见的内核表——nf_conntrack 连接跟踪表。它满了会直接丢包,而它旁边的 TIME_WAIT 堆积又会悄悄吃掉本地端口,让你在出站连接时报"Cannot assign requested address"。这篇文章把这两层坑一次讲清,并给出能直接抄的调优命令。

延伸阅读

更多相关攻略推荐:Ollama AI系列(2):VPS上的AI推理与API应用Ollama AI系列(2):VPS上的AI推理与API应用【CPU选型 01】搭载 AMD Ryzen 9950X 的 VPSARM / Ampere 席卷 VPS:性价比真香还是兼容陷阱?Ollama AI系列(2):VPS上的AI推理与API应用

一、连接跟踪是什么:每个 TCP 连接都要占一条 conntrack 表项

Netfilter 是 Linux 内核里 iptables 与 nftables 共同的底层引擎,它维护着一张叫做 conntrack(connection tracking,连接跟踪)的哈希表,用来记录"此刻有哪些网络连接正在进行"。只要数据包经过这台机器的网络栈,无论你是跑 Nginx 做网站、做反向代理,还是跑着 Docker、Kubernetes 容器,内核都会尝试为每条连接建立一条表项。一次 TCP 三次握手完成,表里就多一条记录;即便连接已经关闭,这条记录也不会立刻消失,而是要等对应状态的超时(比如 ESTABLISHED 状态默认要等 5 天,TIME_WAIT 状态默认 120 秒)才会被回收。

很多人误以为只有做 NAT 转发才需要 conntrack,其实完全不是。只要你的防火墙规则里用到了 state 或 conntrack 匹配(iptables 的 -m state、-m conntrack,或者 nftables 的 ct state),又或者你启用了 Docker、Kubernetes,这张表就在后台默默工作。哪怕你只是托管一个静态博客,只要用的是带状态防火墙的主流 Linux 发行版,conntrack 模块几乎一定被加载着。

每条表项都要消耗内存,量级在 300 字节上下,而且是不可被换出到磁盘的内核内存。表的大小有一个硬性上限,由参数 nf_conntrack_max 控制。较新的内核默认值一般是 262144 条,更老的内核只有 65536 条。对一台只跑小流量博客的 VPS 来说绰绰有余,但对高并发的代理、API 网关,或者被搜索引擎爬虫、恶意扫描频繁光顾的机器,这点余量可能在几分钟甚至几十秒内就被耗尽。

二、症状:dmesg 报 conntrack table full、代理随机连不上、新建连接超时

最典型、也最容易让人摸不着头脑的信号,来自内核日志。执行下面任意一条都能看到:

journalctl -k | grep conntrack

dmesg | grep 'table full'

日志里会反复刷出这样一行:

nf_conntrack: table full, dropping packet

请盯住 dropping packet 这半句——意思是内核直接把新到达的数据包丢弃了,而不是给你返回一个错误。于是你会观察到一类非常诡异的现象:机器 CPU、内存、带宽统统没满,Nginx 进程也好好活着,可就是有一部分新连接建立不起来。具体表现可能是浏览器偶尔转圈、curl 偶发超时、代理到后端的连接随机失败、负载均衡器冷不丁飘出 502。更折磨人的是这种丢包是概率性的,压测时可能十次里掉一两回,日志里还常常跟着 net_ratelimit: N callbacks suppressed,表示同样的错误被内核限流、没全部打印出来。

怎么确认就是它在作祟?把当前用量和上限摆在一起比一比就清楚了:

sysctl net.netfilter.nf_conntrack_count

sysctl net.netfilter.nf_conntrack_max

如果 count 已经逼近甚至等于 max(经验上到 90% 以上就开始掉包),基本就坐实了。也可以用 conntrack -S 观察 insert_failed 和 drop 计数是否还在往上走。顺带一提,/proc/net/nf_conntrack 这个文件里能看到当前所有表项,行数就是表的占用情况,wc -l 一下就知道有多满。

三、TIME_WAIT 堆积与本地端口耗尽(ip_local_port_range 太小)

conntrack 表满只是问题的一面,另一面叫"本地端口耗尽",它和 TIME_WAIT 状态紧紧缠在一起,而且症状长得几乎一样,很容易被混淆。

当你的机器作为客户端去连别人时——比如 Nginx 反代到上游、应用去连数据库、微服务之间互相调用——每一次出站连接都要占用一个"本地临时端口"。Linux 默认只把 32768 到 60999 这段约 28000 个端口开放给临时连接。而 TCP 协议里,主动关闭的一方会进入 TIME_WAIT 状态,默认要停留约 60 秒(conntrack 层面则跟踪 120 秒)才会释放。

这时候一个很残酷的算术就出现了:假设你的服务每秒新建 1000 个短连接,每个 TIME_WAIT 占 60 秒,那么同一时刻堆积的 TIME_WAIT 就有大约 60000 个,已经把那 28000 的端口池彻底撑爆。下一个想出站的连接就会直接报错:

Cannot assign requested address

这是端口耗尽最直白的表达。与此同时,内核日志里还可能出现:

TCP: time wait bucket table overflow

它在告诉你 tcp_max_tw_buckets(TIME_WAIT 桶的上限,默认大约 65536)也已经到了。注意这里的问题出在"出站 + 短连接 + 回收慢",而不是 conntrack 表本身,但两者经常结伴出现。

排查时强烈建议用 ss 而不是老旧的 netstat,后者在几万条连接下慢得离谱:

ss -s

ss -ant | awk '{print $1}' | sort | uniq -c

如果 TIME_WAIT 的数量长期贴着端口上限跑,那根因就是短连接太多、回收太慢,调 conntrack 是治标,得从连接复用上动刀。

四、调优:nf_conntrack_max、tcp_tw_reuse、tcp_max_tw_buckets、端口范围

对症有四板斧,全部通过 sysctl 完成:临时改用 sysctl -w 立刻生效,长期则要写进 /etc/sysctl.conf 或 /etc/sysctl.d/ 下的文件,再 sysctl -p 让配置持久化。

第一斧,把 conntrack 表放大。可以按内存粗略估算:64 位系统上 CONNTRACK_MAX 约等于 内存字节数 / 16384 / 2,一台 64GB 内存的机器算下来约 2097152 条。但小 VPS 千万别盲目拉满,前面说过每条约 300 字节,100 万条就要吃掉近 300MB 不可换出内存,一台 1GB 内存的小鸡根本扛不住。常规做法先翻个倍:

sysctl -w net.netfilter.nf_conntrack_max=262144

同时建议把哈希桶数量维持在 max 的四分之一左右,否则单个桶里的链表过长、查找效率骤降:

sysctl -w net.netfilter.nf_conntrack_buckets=65536

第二斧,缩短超时,让死掉的连接快点腾位置。ESTABLISHED 默认 5 天对绝大多数 Web 场景纯属浪费,压到 6 小时(21600 秒)足够;TIME_WAIT 的跟踪超时从 120 秒降到 30 到 60 秒更见效:

sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=21600

sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=60

下面用一张表对比几个关键超时的默认值与建议值:

参数默认值建议值作用
nf_conntrack_tcp_timeout_established432000 秒(5 天)21600 秒(6 小时)已建立连接跟踪存活时长
nf_conntrack_tcp_timeout_time_wait120 秒30 至 60 秒TIME_WAIT 状态跟踪时长
ip_local_port_range32768 609991024 65535本地临时端口池
tcp_max_tw_buckets约 65536200000 左右TIME_WAIT 桶上限

第三斧,在 TCP 层复用 TIME_WAIT。开启 tcp_tw_reuse = 1,允许内核把空闲超过 1 秒的 TIME_WAIT socket 拿去服务新的出站连接,前提是 tcp_timestamps = 1(现代内核默认已开):

sysctl -w net.ipv4.tcp_tw_reuse=1

sysctl -w net.ipv4.tcp_timestamps=1

要特别说明:tcp_tw_reuse 只对客户端出站连接有效,对服务端监听侧帮不上忙;更重要的是,千万别去开 tcp_tw_recycle——这个参数早在 Linux 4.12 就被删除,而且在任何带 NAT 的环境(家庭路由器、云厂商 SLB、K8s Service)下会让对端的连接莫名其妙失败。

第四斧,扩大端口池、抬高桶上限、顺手收紧 FIN 超时:

sysctl -w net.ipv4.ip_local_port_range='1024 65535'

sysctl -w net.ipv4.tcp_max_tw_buckets=200000

sysctl -w net.ipv4.tcp_fin_timeout=30

把上述参数一并写进 /etc/sysctl.d/99-conntrack.conf,再执行 sysctl -p 加载,重启后依然生效。如果你明确知道某些流量既不做 NAT 也不需要状态防火墙(比如到内网可信 DNS 的查询、机器之间的备份流量),还可以在 raw 表用 NOTRACK 把它排除出跟踪表,进一步给真正需要的连接腾地方。

五、DDoS/扫描场景下 conntrack 成为瓶颈与取舍(关跟踪的风险)

当机器遭遇 DDoS 或端口扫描时,conntrack 会从"默默干活的小透明"变成实打实的瓶颈。原因在于连接跟踪的插入和删除要走内核锁,半开连接(SYN_RECV)在 SYN Flood 下会疯狂产生新表项,瞬间把表撑爆——这恰恰是攻击者想要的:用最少的成本打满你的状态表,让正常流量被 dropping packet。

社区里常给两个方向的缓解。一个是防御侧:开启 SYN Cookies(net.ipv4.tcp_syncookies = 1)来吸收半开连接,把 nf_conntrack_tcp_timeout_syn_recv 调小,并在防火墙层对新建 SYN 做限速;再把 nf_conntrack_tcp_loose 设为 0,配合丢弃 INVALID 状态包,能把抗 ACK 泛洪的吞吐从每秒十几万包提升到几百万包量级。另一个方向是"釜底抽薪":既然表会满,那干脆不用表。

确实,对于纯粹做反向代理、又不依赖 NAT 和状态防火墙的机器,可以整体关掉 conntrack(比如在 /etc/modprobe.d/ 里写 install nf_conntrack /bin/true 阻止模块加载)。但这一步的副作用你必须心里有数:

  • 状态防火墙会失效:绝大多数 iptables / nftables 的状态匹配规则依赖 conntrack,关掉后这些规则要么不工作,要么你得把整套管规则改写成无状态形式。
  • NAT 与容器网络会断:NAT、IP 伪装(MASQUERADE)重度依赖 conntrack;Docker、Kubernetes(kube-proxy、网络策略)同样离不开它,关掉基本等于让容器网络瘫痪。
  • 丢弃不等于安全:把表调大或关掉跟踪,解决的是"正常流量被误伤",并不减少攻击流量本身,真正的清洗仍要靠上游防护。

所以现实里的取舍是:绝大多数 VPS 应该走"调大 max + 缩短超时 + 筛选 NOTRACK + 开 SYN Cookies"这条稳妥路线;只有在你百分之百确认不需要 NAT、不需要状态防火墙、也不是 K8s 节点的前提下,才考虑彻底关闭 conntrack。

#nf_conntrack #连接数限制 #TIME_WAIT #端口耗尽 #VPS调优