结论先行:将 5 万条 iptables 顺序拦截规则重构为原生 nftables 的匿名集合(Set/Map)后,宿主机网卡软中断(SoftIRQ)CPU 开销从 100% 暴降至 2%,网络 P99 延迟从 200ms 恢复至 2ms。在海量 IP 拦截与路由过滤场景下,彻底抛弃 iptables 的 O(N) 线性遍历,改用 nftables 基于红黑树与哈希表的 O(1) 匹配,才是现代网关防御的唯一出路。
近期在排查某边缘网关集群性能问题时,发现部分节点在 PPS(每秒包数)仅 10 万左右时,Load Average 飙升至 40+,网络出现严重丢包。该节点主要承担流量清洗清洗和恶意 IP 封禁任务,安全组件会通过脚本动态下发拦截规则。
案发现场:ksoftirqd 满载与 perf 剖析
登录问题机器(Ubuntu 22.04, Kernel 5.15.0, iptables v1.8.7),通过 mpstat -P ALL 1 观察,发现多个 CPU 核心的 %soft 指标长期顶在 100%,而 %usr 和 %sys 极低。这说明 CPU 周期全部消耗在了内核态网络软中断处理上。
直接挂载 perf 抓取热点函数:
perf record -a -g -F 99 -- sleep 10
perf report
调用栈结果异常刺眼:
- 78.42% ksoftirqd/2 [kernel.kallsyms] [k] nft_do_chain
- 77.10% nft_do_chain
- 68.32% nft_cmp_eval
- 4.21% nft_counter_eval
排查防火墙规则,发现安全组件通过 iptables -A INPUT -s 累积下发了高达 5.2 万条黑名单规则。
为什么 iptables-nft 包装层依然无法拯救 SoftIRQ 雪崩?
很多人有个误区,认为在较新的 Linux 发行版中,iptables 已经默认切换到了 iptables-nft(即底层调用 nftables API),性能应该自动得到了优化。这是极其致命的错觉。
iptables-nft 仅仅是一个语法翻译层。当你执行 5 万次 iptables -A INPUT -s IP -j DROP 时,翻译层会在 nftables 中原样生成 5 万条独立的规则。
在内核 net/netfilter/nf_tables_core.c 的 nft_do_chain() 函数执行时,Netfilter 依然会像老版本一样,对数据包进行 O(N) 的线性遍历匹配。这意味着:
-
合法流量的深渊:正常用户的包如果不匹配任何黑名单,必须遍历完这 5.2 万条规则,直到命中链底部的默认
ACCEPT。 -
SYN 洪水的放大器:虽然通常会在链表顶部配置
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT,但对于新建连接(SYN)或恶意扫描包,Conntrack 无法命中,依然会陷入 5.2 万次nft_cmp_eval的循环对比中。
单包处理时间的微秒级膨胀,在 10 万 PPS 的乘数效应下,瞬间抽干了 ksoftirqd 的处理能力,导致网卡 Ring Buffer 溢出,触发连环丢包。
nftables 重构实战:利用 Set/Map 实现 O(1) 降维打击
要解决这个问题,必须抛弃 iptables 的扁平化规则思维,改用 nftables(推荐版本 >= 1.0.2)原生的集合(Set)数据结构。内核会自动为集合选择合适的数据结构(针对离散 IP 默认使用 rhashtable,针对网段使用 pipapo)。
1. 编写原生 nftables 配置
清空旧的 iptables 规则,并加载优化的 nftables 配置:
# file: /etc/nftables.conf
flush ruleset
table inet filter {
# 预先定义一个巨大的哈希集合用于存放黑名单 IP
set blacklist_ipv4 {
type ipv4_addr
flags interval
auto-merge
size 100000 # 预分配空间,避免频繁扩容引起的自旋锁竞争
}
chain input {
type filter hook input priority filter; policy accept;
# 1. 优先放行已建立的连接,跳过后续检查 (Conntrack 加速)
ct state established,related accept
# 2. 无效包直接丢弃
ct state invalid drop
# 3. O(1) 极速匹配黑名单,命中即丢弃
ip saddr @blacklist_ipv4 drop
# 4. 本地回环与 SSH 放行等常规规则...
iifname "lo" accept
tcp dport 22 accept
}
}
2. 动态管理海量 IP 且不重载规则
传统 iptables 增删规则时会对整个链表加锁(xtables_lock),并发极差。而在 nftables 中,向 set 添加元素是轻量级的原子操作,完全不会影响其他规则的执行:
# 批量导入 5 万个黑名单 IP(执行耗时仅需几十毫秒)
nft add element inet filter blacklist_ipv4 { 1.1.1.1, 2.2.2.0/24, 3.3.3.3 }
# 删除特定 IP
nft delete element inet filter blacklist_ipv4 { 1.1.1.1 }
3. 性能验证
重构上线后,再次压测 30 万 PPS 的新建连接风暴:
-
CPU
%soft消耗直接降至 2% ~ 3%。 -
perf top中nft_do_chain的开销跌出前十。 -
通过
nft list set inet filter blacklist_ipv4查看,五万个 IP 被压缩在一次底层的哈希查找中,耗时几近于零。
底层原理:Pipapo 与 rhashtable 的威力
在 nftables 的源码实现中(net/netfilter/nft_set_hash.c),当你定义了一个不带 interval(网段)标志的独立 IP set 时,内核默认初始化 rhashtable(可动态扩容的哈希表)。查找复杂度为绝对的 O(1)。
如果带有 interval 标志(例如混合了 CIDR 网段拦截),内核会使用 pipapo (PIle PAcket POlicies) 算法(net/netfilter/nft_set_pipapo.c)。这是一种针对多维范围匹配高度优化的算法,通过预构建位图(Bitmaps),将多级网段的匹配转换为内存连续的位运算操作,且对 CPU Cache 极其友好。这解释了为何无论黑名单是 5 千还是 5 万,单包处理时延的抖动曲线始终保持平滑。
常见问题 (FAQ)
Q1: 在向 nftables 的 Set 中大批量添加 IP 时,会阻塞现有的网络连接吗? 不会。nftables 底层通过 RCU (Read-Copy Update) 机制保障数据的并发读写。对于数据包匹配链路(读侧),RCU 提供了无锁访问能力;而在控制平面批量更新 Set 元素时(写侧),只会发生极短的锁竞争。这与传统 iptables 重载整个规则链引发的剧烈抖动有本质区别。
Q2: 既然设置了 ct state established accept,为什么之前用 iptables 时,正常流量还是会感觉到高延迟?
因为网络环境并非绝对纯净。即使正常长连接被 Conntrack 放行,但在公网环境中,机器每秒都会遭受大量的垃圾扫描包、伪造包或恶意 SYN 包。这些无状态包无法命中 established,必定会落入后续的 5 万条 iptables 规则中进行线性遍历,进而抢占大量 CPU 资源,导致正常连接的软中断处理被系统调度延后,最终体现为 P99 延迟飙升。
Q3: 如何将现有的业务脚本从 iptables 平滑迁移到 nftables?
千万不要依赖 iptables-nft 来容忍旧脚本。正确的做法是:
-
重写网关的基线规则,用
nft -f加载带有 Set 的配置结构。 -
将现有的封禁/解封脚本,从
iptables -I INPUT -s $IP -j DROP替换为nft add element inet filter blacklist_ipv4 { $IP }。 -
如果依赖 fail2ban 等第三方工具,需将 action 配置修改为
nftables-multiport或直接修改其默认的 backend 为 nftables。