深入 Netfilter 陷阱排查:高并发 SNAT 引发的 nf_conntrack 满载丢包与 nftables Flowtable 加速实战

核心结论:高并发网关出现 nf_conntrack: table full, dropping packet 导致 502 和延迟雪崩时,盲目调大 nf_conntrack_max 只会加剧 CPU SoftIRQ 负载。治本之策是针对内网信任流量在 raw 表下发 NOTRACK 规则,并全面向 nftables 迁移,利用 Flowtable 实现 Fast-path 连接绕过,能将网络栈包转发的 CPU 消耗降低 40% 以上。

案发现场:网关节点的 99 线雪崩

排查过程中,某 Kubernetes 生产集群的 Ingress/NAT 网关节点(Ubuntu 22.04, Kernel 5.15)遭遇突发性高延迟。业务侧反馈 API 接口 99 线延迟从 20ms 飙升至 3s 以上,并伴随大量 502 错误。

登录节点,top 显示部分 CPU 核心的 si(SoftIRQ)长期被打满至 100%。敲下 dmesg -T | tail,系统日志里被一行致命报错刷屏:

[Fri ...] nf_conntrack: nf_conntrack: table full, dropping packet
[Fri ...] nf_conntrack: nf_conntrack: table full, dropping packet

通过 sysctl net.netfilter.nf_conntrack_count 确认,当前连接跟踪表中的条目数已经逼近配置的 262144 上限。这是一个极其典型的 Netfilter 性能瓶颈。

抓取 conntrack 现场状态进行分析:

conntrack -L -o extended | awk '{print $4}' | sort | uniq -c | sort -nr

输出显示,绝大多数记录处于 TIME_WAIT 和 UDP 的 UNREPLIED 状态。由于业务采用短连接模式,并发极高,NAT 模块强依赖 conntrack 表进行 Tuple 映射。一旦表满,内核网络栈别无选择,只能将新到达的 SYN 包直接 Drop,引发应用层的重传与超时风暴。

为什么单纯调大 nf_conntrack_max 无法根治丢包问题?

遇到这个报错,很多人的第一反应是无脑修改 /etc/sysctl.conf,将 nf_conntrack_max 调到 100 万甚至 200 万。这是极其危险的做法,等于饮鸩止渴。

Netfilter 在内核中维护连接状态使用的是哈希表。这个哈希表的大小由 nf_conntrack_buckets 决定。如果你只放大了 max(最大连接数),而不按比例放大 buckets(哈希桶数量),会导致单个哈希桶下的链表变得极长。

对于每一个经过网络栈的包,内核都需要遍历这条链表来查找匹配的五元组。当链表过长时,查找算法的时间复杂度从 O(1) 退化为 O(N)。更致命的是,修改 conntrack 记录需要加自旋锁(Spinlock)。高并发下,漫长的遍历加上激烈的锁竞争,会直接导致内核消耗大量 CPU 在软中断(ksoftirqd)上,系统 Load Average 飙升,网卡丢包率进一步恶化。

合理的应急配置比例应当是 nf_conntrack_max = nf_conntrack_buckets * 4。 应急修复命令如下:

# 1. 调整哈希桶大小 (必须先调桶,再调 Max)
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize

# 2. 调大最大连接数和降低超时时间
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

但这仅仅是续命,并不能解决 CPU 开销问题。

防御性降级:Raw 表的 NOTRACK 机制

网关节点并非所有流量都需要进行 NAT。比如同网段的 Pod 到 Pod 通信,或者内部 Consul/Prometheus 的探测流量,完全可以跳过连接跟踪。

在 iptables 时代,我们在 raw 表下发 NOTRACK 规则,将这部分信任流量提前短路:

# 对于源和目的都在内网 10.0.0.0/8 网段的包,不进入 conntrack
iptables -t raw -I PREROUTING -s 10.0.0.0/8 -d 10.0.0.0/8 -j CT --notrack
iptables -t raw -I OUTPUT -s 10.0.0.0/8 -d 10.0.0.0/8 -j CT --notrack

避坑指南:一旦给数据包打了 NOTRACK 标签,它在后续的 filter 表中就不属于 ESTABLISHED 或 RELATED 状态,而是 UNTRACKED。如果你的 INPUT 链默认策略是 DROP,且只有 -m state --state ESTABLISHED,RELATED -j ACCEPT,那么这些内网包会被统统丢弃。必须补上一条:

iptables -I INPUT -m state --state UNTRACKED -j ACCEPT

架构演进:全面向 nftables 与 Flowtable Fast-path 迁移

iptables 最大的架构缺陷在于其规则的线性遍历机制(Linear evaluation)。哪怕你有一万条匹配规则,数据包也要从第一条匹配到最后一条。而 nftables 原生支持 Set 和 Map(类似字典),能在 O(1) 复杂度内完成匹配。

更重要的是,从 Linux Kernel 4.16 开始,nftables 引入了革命性的 Flowtable 机制(软件 Fast-path),并在 Kernel 5.3 引入了硬件卸载(Hardware Offload)。

在传统的 Netfilter 路径中,每一个包都要经历完整的 PREROUTING -> FORWARD -> POSTROUTING 链条。 启用 Flowtable 后,对于建立好的双向连接(ESTABLISHED),nftables 会直接在 Ingress 钩子处截获数据包,将其直接推送到对应的出口网卡(Egress),完全绕过 Netfilter 核心网络栈和复杂的路由查找。

落地配置:构建基于 nftables 的加速网关

首先,清空老旧的 iptables 规则,确保模块未被深度占用(此处需极度谨慎,确保断网可恢复)。编写 /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    # 1. 定义 flowtable,挂载在 ingress 钩子,监听内网 eth0 和外网 eth1
    flowtable fastpath {
        hook ingress priority 0;
        devices = { eth0, eth1 };
    }

    chain forward {
        type filter hook forward priority 0; policy accept;

        # 2. 只有处于 established 状态的连接,才卸载到 flowtable 加速
        ip protocol { tcp, udp } flow add @fastpath

        # 基础的连接跟踪允许
        ct state established,related accept
        ct state invalid drop
    }
}

table ip nat {
    chain prerouting {
        type nat hook prerouting priority 0; policy accept;
    }

    chain postrouting {
        type nat hook postrouting priority 100; policy accept;

        # 3. 对出公网的流量执行伪装 (SNAT)
        oifname "eth1" masquerade
    }
}

应用规则:

nft -f /etc/nftables.conf

性能验证: 应用 nftables Flowtable 后,再次在晚高峰压测。

  1. 查看 CPU si 开销,从之前的单核满载 100% 断崖式下降至 15% 左右。

  2. 观察 conntrack_count,虽然总条目仍然在 10 万级别,但大量的 established 包走的是 Fast-path,不再反复冲击 Netfilter 锁。

  3. P99 延迟从数百毫秒稳定回落至 5ms 以内,丢包彻底消失。

常见问题 (FAQ)

Q1:如何平滑从 iptables 迁移到 nftables,不导致生产环境长连接中断? A: 生产环境切忌直接 apt-get purge iptables。Linux 提供了兼容层 iptables-nft。首先替换底层后端,让 iptables 命令实际生成 nftables 规则。跑通后,使用 iptables-save > rules.txt,再通过 iptables-restore-translate -f rules.txt > rules.nft 转换为原生 nftables 语法。最后,在维护窗口 nft -f rules.nft 覆盖并原子化应用新规则集,连接跟踪状态在内核态是共享的,ESTABLISHED 状态的连接不会因此断开。

Q2:启用 nftables Flowtable 后,为什么用 tcpdump 抓包,部分数据包神秘消失了? A: 这是正常现象。Flowtable 在 Ingress 阶段(网卡驱动收到包后极早期的阶段)就将包转发出去了,直接绕过了协议栈的大部分拦截点(包括 AF_PACKET)。这意味着处于 Fast-path 的数据包可能无法被 tcpdump 捕获到。如果你必须抓包排障,需要临时在 nft 规则中注释掉 flow add @fastpath,让流量回退到慢速路径(Slow-path)。

Q3:masquerade 和直接写 snat to 性能上有差别吗? A: 有明显差别。masquerade 是专门针对动态 IP 设计的,每次处理 NAT 映射时,它都会去动态查询出口网卡当前的 IP 地址。如果你的网关是静态 IP,务必在 nftables/iptables 中使用 snat to 192.168.x.x 这样的明确指令。静态 SNAT 免去了查路由表获取本机 IP 的开销,在高并发场景下性能表现更好。

Q4:为什么 UDP 流量也会引发 conntrack 爆满,UDP 不是无连接的吗? A: UDP 在协议栈确实无连接,但 Netfilter 是状态防火墙,它必须为 UDP 维持一个伪连接状态(包含五元组),否则无法进行返回包的 NAT 映射。DNS 解析(短 UDP 请求)是最常见的导火索。如果业务侧不复用 DNS 连接,海量的源端口会导致 conntrack 表被大量状态为 UNREPLIED (默认超时 30 秒) 的 UDP 条目塞满。解决方案是优化业务代码,或在内核调低 net.netfilter.nf_conntrack_udp_timeout_stream 和 timeout 参数。