深入 nf_conntrack 满载丢包排查:SNAT 端口耗尽引发的 SYN 阻断与 nftables Flowtable 旁路加速实战

高并发网关常遇 nf_conntrack: table full 导致 SYN 丢包。盲目调大 nf_conntrack_max 只会加剧内核自旋锁争用与内存开销。根本解法是排查 SNAT 端口耗尽,并从 iptables 彻底迁移至 nftables,利用 Flowtable 机制开启流量卸载(Offload),让 ESTABLISHED 状态报文旁路跳过 Netfilter 核心链,实测可降低 40% 的 sys CPU 并彻底消除连接跟踪瓶颈。

案发现场:诡异的 99 线毛刺与超时

排查过程中,某承载了上万并发连接的 K8s Egress NAT 网关节点(Kernel 5.15.0)频繁出现请求超时,监控大盘显示 TCP 99线延迟出现规律性毛刺,Load Average 中的 sys CPU 间歇性飙升到 80% 以上。

直接上机器看内核日志:

$ dmesg -T | tail -n 20 | grep conntrack
[Thu Oct 26 14:12:33 2023] nf_conntrack: nf_conntrack: table full, dropping packet
[Thu Oct 26 14:12:33 2023] nf_conntrack: nf_conntrack: table full, dropping packet

经典的连接跟踪表爆满导致丢包。看一下当前连接数与上限:

$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 262144
net.netfilter.nf_conntrack_max = 262144

为什么盲目调大 nf_conntrack_max 是一剂毒药?

遇到 table full,很多人的第一反应是无脑加大 nf_conntrack_max。在低并发场景下这确实管用,但在高吞吐 NAT 网关上,这是一剂毒药。

nf_conntrack 是基于哈希表实现的。它的核心数据结构由 Hash buckets(桶)和链表组成。当你只调大 nf_conntrack_max 而不调整 nf_conntrack_buckets 时,每个 Hash bucket 下挂载的链表会变得极长。 内核在进行包过滤或 NAT 时,需要遍历链表来匹配五元组。链表越长,查询的开销越大;加上 Hash bucket 的自旋锁(spinlock)争用,在多核高 PPS(Packet Per Second)场景下,CPU 会被 __nf_conntrack_find_get 等函数吃干抹净(表现为软中断 si 和内核态 sy CPU 极高)。

正确的临时缓解姿势必须是联动调整(保持桶大小为最大连接数的 1/4):

# 1. 调大 Hash 桶大小(立即生效,不可通过 sysctl 修改)
$ echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
# 2. 调大最大连接数
$ sysctl -w net.netfilter.nf_conntrack_max=1048576
# 3. 缩短 TIME_WAIT 和 ESTABLISHED 状态的超时时间,加速条目回收
$ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
$ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

但这只是治标。抓包发现,该节点作为 SNAT 网关,真实存在的活跃连接并没有达到 26 万,导致表满的真凶是 SNAT 端口耗尽引发的僵尸连接积压。由于 iptables 的 MASQUERADE 规则,多个内网 Pod 访问外部同一个目标 IP:Port 时,由于源端口池(默认 1024-65535)被快速消耗殆尽,新的 SYN 包在进行 NAT 转换时无法分配到 free tuple,导致连接状态卡死并在 conntrack 表中滞留。

iptables 时代的穷途末路与 nftables 破局

只要你还在用 iptables,每一个数据包都要不可避免地穿透 PREROUTING -> FORWARD -> POSTROUTING 整条链。即使是已经建立连接(ESTABLISHED)的报文,也要每次去走一遍 Rule 解析和 Conntrack 状态机。

Kernel 4.16+ 引入了 nftables 的杀手锏功能:Flowtable (Fast-path Offload)。 它的底层原理极其优雅:对于已经建立连接的 TCP/UDP 流量,Flowtable 会在网卡的 ingress hook 点(非常靠前的位置)直接进行路由转发和 NAT 替换,完全绕过传统的 Netfilter 过滤链和 Conntrack 查询

实战:将 iptables NAT 迁移至 nftables Flowtable

不要再用 iptables-nft 这种套壳工具了,直接写原生的 nftables 配置。以下是我们在网关节点上的落地配置,实现内网到外网的 SNAT 并开启 Flowtable 硬件/软件卸载。

清除老旧规则:

$ iptables -F && iptables -t nat -F
$ systemctl stop iptables

编写 /etc/nftables.conf

flush ruleset

table inet filter {
    # 定义 Flowtable 开启卸载
    flowtable f {
        # 挂载在非常靠前的 ingress 钩子,优先级 0
        hook ingress priority 0;
        # 绑定内外网网卡(根据实际情况修改)
        devices = { eth0, eth1 };
    }

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

        # 核心逻辑:允许 ESTABLISHED 流量,并将新流量加入 flowtable 'f'
        ip protocol { tcp, udp } flow add @f

        # 允许内网 (10.0.0.0/8) 到外网的初始包通过
        iifname "eth0" oifname "eth1" ip saddr 10.0.0.0/8 accept

        # 允许已建立连接的回包
        ct state established,related accept
    }
}

table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        # 传统 SNAT/Masquerade,只对首包生效
        oifname "eth1" ip saddr 10.0.0.0/8 masquerade random
    }
}

应用配置并验证:

$ nft -f /etc/nftables.conf
$ nft list ruleset

注意:masquerade random 的加入是为了缓解 SNAT 端口分配的哈希碰撞冲突,配合 Flowtable 能最大程度压榨网关性能。

性能表现对比

迁移至 nftables Flowtable 后,使用 perf top 观察内核函数调用:

  • 迁移前ipt_do_tablenf_conntrack_in 长年霸占 Top 3,软中断消耗极大。

  • 迁移后:由于首包建立连接后,后续几十个甚至成百上千个数据包直接从网卡 ingress 进入 nft_flow_offload_eval 后被路由发出,ipt_do_table 直接消失,sys CPU 占用率暴降 40% 以上,dmesg 中再无 table full 报错。

常见问题 (FAQ)

Q1:为什么我明明清空了 iptables,用 iptables -L 还能看到一些莫名其妙的规则? 因为较新的 OS(如 Debian 11+, RHEL 8+)默认将 iptables 软链接到了 iptables-nft。这是兼容层,你在 iptables 敲的命令,其实被转换成了 nftables 的内置表。要查看纯正的 iptables 规则,请使用 iptables-legacy -L。在系统层面彻底向 nftables 演进时,强烈建议干掉所有 legacy 和兼容层,统一用 nft 命令行管理。

Q2:开启 nftables Flowtable 之后,为什么 tcpdump 抓不到部分数据包了? 这是预期行为。Flowtable 提供了 Software Offload 和 Hardware Offload (NIC HW offload)。如果是 Hardware offload(需要网卡驱动支持 tc 卸载),数据包在物理网卡层面就被转发了,根本不会进入内核网络栈,挂在 AF_PACKET 上的 tcpdump 自然抓不到。即使是 Software offload,由于绕过了常规的 Netfilter RX 路径,抓包结果也会呈现“只看到 SYN 包,看不到后续数据流”的现象。排查网络问题时,需要临时禁用 flowtable 规则。

Q3:在 K8s 中使用 IPVS 模式的 kube-proxy,也会受 nf_conntrack 限制吗? 会。虽然 IPVS 维护了自己的连接管理哈希表,但它仍然深度依赖 Netfilter 框架做底层的包拦截和 NAT 协调(尤其是 nf_conntrack)。K8s 场景下大量短连接(如探针、微服务间 RPC)极易打满 conntrack。除文中提到的调优手段外,建议通过 kube-proxy 启动参数 --conntrack-max-per-core 来合理规划容量,而非手动修改 sysctl,防止被 Kubelet 重置。