在高并发网络压力测试或遭遇 DDoS 攻击时,Linux 服务器的 dmesg 或者是 /var/log/messages 经常会爆出这样一条红字警告:
nf_conntrack: table full, dropping packet
紧接着,服务器开始大面积丢包,网络请求超时,但 CPU 和内存利用率看起来却还挺空闲。
很多网上的教程会简单粗暴地告诉你:“把 nf_conntrack_max 改大就行了!”。但这往往是灾难的开始。 如果盲目将该值调得过大,而服务器内存不足,一旦并发真的冲上去,内核会瞬间吃光物理内存,触发 OOM Killer 杀掉你的核心服务(如 Nginx、Redis),甚至直接导致系统挂起。
如何在解决连接跟踪表满的同时,保证内存绝对安全?这需要一套基于数学计算的精准调优方案。
一、 为什么不能盲目放大?先算一笔系统账
nf_conntrack 是 Linux 内核连接跟踪(Connection Tracking)模块。只要启用了建设在连接跟踪之上的服务(如 iptables NAT、Firewalld、Docker 容器网络等),每一个经过服务器的 IP 包,都会在内核内存中创建一个 nf_conntrack 结构体来记录其状态。
一个 conntrack 结构体在 64 位 Linux 系统中,大约占用 320 字节(随着内核版本微调,通常在 300~380 字节之间,我们按 320 字节估算)。
如果盲目将 nf_conntrack_max 设置为 10,000,000(一千万):
$$10,000,000 \times 320 \text{ Bytes} \approx 3,200,000,000 \text{ Bytes} \approx 3 \text{ GB}$$
这意味着,一旦极端并发来临,光是内核网络连接跟踪这一个模块,就要强行吃掉 3GB 的物理内存。这部分内存属于内核 Slab 缓存,无法被轻易回收。如果你的服务器本身只有 8G 内存,且运行着高内存占用的 JVM 或 Redis,系统崩溃几乎是必然的。
二、 安全调优公式:量体裁衣
安全的做法是:根据服务器现有的空闲物理内存,倒推可以承受的最大连接跟踪数。
通常,建议用于 nf_conntrack 的内存不要超过系统总物理内存的 5% ~ 10%。我们以 5% 的保守安全边界为例,计算公式如下:
$$\text{Max_Entries} = \frac{\text{Host_RAM_Bytes} \times 0.05}{320}$$
$$\text{Hashsize} = \frac{\text{Max_Entries}}{4} \quad (\text{或 } \frac{\text{Max_Entries}}{8})$$
(注:Hashsize 是哈希表的大小。nf_conntrack 使用链地址法解决哈希冲突,通常一个哈希桶后挂 4 或 8 个结点的链表效率最高。)
实战演练:以 16GB 内存服务器为例
计算最大连接数 (Max_Entries)
$$16 \times 1024 \times 1024 \times 1024 \times 0.05 \div 320 \approx 2,684,354$$
为了取整,我们设定nf_conntrack_max为 2,000,000 (两百万)。计算哈希表大小 (Hashsize)
$$\text{Hashsize} = 2,000,000 \div 4 = 500,000$$
三、 实操:不停机动态调整
调整这两个参数时,必须同时修改 max 和 hashsize。如果只增大 max 而不增大 hashsize,会导致哈希冲突链表过长,每次查询连接状态都会消耗极高的 CPU,引发 CPU 软中断(softirq)飙升。
1. 临时生效(即时解决故障)
由于 hashsize 属于内核模块参数,它无法通过普通的 sysctl 修改,需要通过 /sys 虚拟文件系统写入:
# 1. 先修改 hashsize (必须先改,避免桶过载)
echo 500000 > /sys/module/nf_conntrack/parameters/hashsize
# 2. 再修改 max 限制
sysctl -w net.netfilter.nf_conntrack_max=2000000
2. 永久生效(防止重启失效)
将配置分别写入系统配置文件中。
修改 /etc/sysctl.conf,加入:
net.netfilter.nf_conntrack_max = 2000000
修改 /etc/modprobe.d/nf_conntrack.conf(如果文件不存在则新建),确保模块加载时自动设置 hashsize:
options nf_conntrack hashsize=500000
四、 辅助药方:缩短垃圾连接的存活期
除了单纯扩大表格容量,主动清理“占着茅坑不拉屎”的无效连接是更优雅的解法。
Linux 默认的内核参数极其保守。例如,一条已经建立的 TCP 连接(ESTABLISHED),如果没有任何数据交互,默认居然要在内核里维持 5天 (432000 秒) 的跟踪状态!这在高并发场景下简直是自杀。
将以下参数写入 /etc/sysctl.conf,大幅缩短各类状态的超时时间:
# 已经建立连接的 TCP 生命周期缩短到 30 分钟 (1800秒) 或 1 小时 (3600秒)
net.netfilter.nf_conntrack_tcp_timeout_established = 1800
# 减少等待 FIN_WAIT、CLOSE_WAIT、TIME_WAIT 等各种过渡状态的超时时间
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
执行 sysctl -p 立即生效。这一套组合拳下来,连接跟踪表的“新陈代谢”速度会提升数倍,老旧失效连接被快速清理,腾出空间给新连接。
五、 终极杀招:对特定流量禁用连接跟踪 (NOTRACK)
如果你在做高并发的 Nginx 反向代理、Redis 缓存服务或大量的 UDP DNS 解析压力测试,而这些服务并不需要复杂的防火墙安全过滤(不需要状态检测防火墙,也没有做 SNAT/DNAT),那么最彻底的办法是直接绕过 conntrack 模块。
通过 iptables 的 raw 表,我们可以对特定的端口或 IP 设置 NOTRACK:
# 比如,对流入和流出本机 80 端口的 TCP 流量,直接不进行连接跟踪
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK
一旦标记了 NOTRACK,这些连接将不会占用 nf_conntrack 表的任何空间。网络吞吐量能逼近物理网卡极限,同时 CPU 和内存开销降到最低。
(注意:使用 NOTRACK 后,依赖连接状态检测的 iptables 规则(如 -m state --state ESTABLISHED)将对这部分流量失效,请根据实际业务网络拓扑谨慎评估。)