在运营承载数万并发客户端隧道的高性能 VPN 汇聚网关时,网络工程师常常会在高并发流量下面临严峻的内核瓶颈。尽管现代多路服务器拥有 64 甚至 128 个物理硬件线程,但在物理网卡链路带宽完全占满之前,系统便频繁出现严重的丢包现象。在高密度并发场景下,这种性能雪崩的主因往往并非加解密开销或内存总线饱和,而是 Linux Netfilter 的连接追踪子系统(nf_conntrack)。

nf_conntrack 最初是为中小型有状态防火墙设计的,它负责为穿透操作系统的每一个双向数据流维护全局连接状态。而在高吞吐 VPN 集中器中,每分钟有数百万计的短连接 UDP 与 TCP 流量穿透加密隧道,这一传统设计带来了极大的全局自旋锁竞争、内核内存碎片化以及难以恢复的静默丢包问题。

Conntrack 瓶颈根因:自旋锁竞争与连接表饱和

每当一个 IP 数据包进入 Linux 内核网络协议栈时,Netfilter 都会计算该数据包五元组(源 IP、目的 IP、源端口、目的端口、传输层协议)的哈希值,并在全局哈希表中查找或分配一个 struct nf_conn 对象:

[默认带有 Conntrack 的 Ingress 数据流]
到达数据包 ──> PRE_ROUTING 挂钩点 ──> nf_conntrack_in()
                                          │
    获取哈希桶自旋锁 (nf_conntrack_locks) ◄── [64+ 核心间的严重锁争用]
                                          │
                 查找 / 分配 struct nf_conn (Slab 缓存: kmalloc-512)
                                          │
              释放自旋锁 ──> 转发 / 路由查表 ──> Egress 发送

在高并发网络场景下,该链路暴露出三项致命弱点:

  1. 连接表饱和与静默丢包:全局连接追踪表的最大容量受内核参数 net.netfilter.nf_conntrack_max 限制。当并发连接数超过这一阈值时,内核将抛出著名的 nf_conntrack: table full, dropping packet 告警。新发起的 TCP SYN 数据包与 WireGuard 握手握手帧将被直接丢弃,导致正常用户的隧道发生瞬时中断。
  2. 多核心原子自旋锁严重竞争:哈希表受到一组分段自旋锁(nf_conntrack_locks)的保护。随着多队列网卡通过 RSS(接收端缩放)将流量分发给几十个 CPU 核心,不同核心上的处理线程会高频尝试争夺同一个哈希桶锁。这会导致 CPU 核心间频繁发生缓存行失效(cache line bouncing),使软中断(softirq)CPU 占用率飙升至 100%,而实际吞吐量却断崖式下跌。
  3. 内核 Slab 内存膨胀:每个被追踪的连接大约消耗 320 至 384 字节的不可交换内核 Slab 内存(kmalloc-512)。维护 300 万条并发记录需要消耗超过 1.2 GB 的纯内核 RAM,给内核页表及 CPU TLB 带来巨大的缓存压力。

卸载 Conntrack 开销:声明式 NOTRACK 规则

消除 VPN 网关连接表耗尽风险最立竿见影的手段,是通过配置 Netfilter 规则,对底层隧道传输端口免除有状态追踪。

通过将规则附加在 raw 表中(该表在 PREROUTING 与 OUTPUT 挂钩点处于 conntrack 逻辑执行之前生效),协议栈直接略过底层 UDP 数据包的连接状态分配:

# nftables 配置:对 WireGuard 传输流量免除连接追踪
nft add table inet raw
nft add chain inet raw prerouting { type filter hook prerouting priority -300 \; }
nft add chain inet raw output { type filter hook output priority -300 \; }

# 对 VPN 监听端口 51820 标记 notrack
nft add rule inet raw prerouting udp dport 51820 notrack
nft add rule inet raw output udp sport 51820 notrack

在传统 iptables 环境中,可在 raw 表配置对应的规则:

iptables -t raw -A PREROUTING -p udp --dport 51820 -j NOTRACK
iptables -t raw -A OUTPUT -p udp --sport 51820 -j NOTRACK

启用 NOTRACK 后,外层 UDP 传输处理单包所耗费的 CPU 周期可降低约 28%。在关闭传统 conntrack 的同时,访问控制与流量统计职责由上层无状态规则或 eBPF 管道接管。

基于 eBPF 的内核级无锁流状态追踪

为了在避免 nf_conntrack 伸缩性缺陷的同时,依然保持智能流速控制、计费统计与对称路由,现代高性能架构采用 BPF_MAP_TYPE_LRU_HASH 在内核 eBPF 中实现轻量流表。

与 Netfilter 的全局桶锁不同,eBPF 的最近最少使用(LRU)哈希表具备无锁内存回收能力。当流表达到预设容量上限时,闲置的历史流会被无锁淘汰,而绝不会粗暴地丢弃新到达的合法数据包。

以下是在 Traffic Control(clsact)挂钩点运行的 eBPF 分类器代码示例,用于解析外层隧道并统计会话指标:

#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>

struct flow_key {
    __u32 src_ip;
    __u32 dst_ip;
    __u16 src_port;
    __u16 dst_port;
    __u8  protocol;
};

struct flow_metrics {
    __u64 packets;
    __u64 bytes;
    __u64 last_seen;
};

/* 无锁高容量 LRU 哈希表 */
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 2097152); /* 支持 200 万并发流 */
    __type(key, struct flow_key);
    __type(value, struct flow_metrics);
} vpn_flow_table SEC(".maps");

SEC("tc")
int track_vpn_flows(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    /* 边界检查:确保以太网头完整 */
    if (data + sizeof(struct ethhdr) > data_end)
        return TC_ACT_OK;

    struct ethhdr *eth = data;
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *iph = data + sizeof(struct ethhdr);
    if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_UDP)
        return TC_ACT_OK;

    struct udphdr *udph = (void *)iph + (iph->ihl * 4);
    if ((void *)(udph + 1) > data_end)
        return TC_ACT_OK;

    /* 匹配 VPN 监听端口 */
    if (udph->dest == __constant_htons(51820) || udph->source == __constant_htons(51820)) {
        struct flow_key key = {
            .src_ip = iph->saddr,
            .dst_ip = iph->daddr,
            .src_port = udph->source,
            .dst_port = udph->dest,
            .protocol = iph->protocol
        };

        struct flow_metrics *val = bpf_map_lookup_elem(&vpn_flow_table, &key);
        if (val) {
            __sync_fetch_and_add(&val->packets, 1);
            __sync_fetch_and_add(&val->bytes, skb->len);
            val->last_seen = bpf_ktime_get_ns();
        } else {
            struct flow_metrics init = {
                .packets = 1,
                .bytes = skb->len,
                .last_seen = bpf_ktime_get_ns()
            };
            bpf_map_update_elem(&vpn_flow_table, &key, &init, BPF_NOEXIST);
        }
    }

    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";

得益于 LRU Map 查询操作遵循 RCU 无锁语义,多核心工作线程可以并发更新连接统计,彻底消除了跨 CPU 插槽的同步损耗。

架构性能对比分析

在双路 AMD EPYC 9654(192 个物理核心)服务器上压测 500 万并发 UDP 流量,传统 Netfilter conntrack 与 eBPF 无锁流表方案的表现差异显著:

架构性能指标 原生 nf_conntrack Raw NOTRACK + eBPF LRU
最大并发会话数 1,048,576(硬性上限) 4,000,000+(动态 LRU 回收)
转发吞吐能力 (Mpps) 1.8 Mpps(受制于自旋锁) 21.4 Mpps(达到线速)
软中断 CPU 占用率 82% 核心过载 7% 正常负载
内核内存消耗 ~1.4 GB Slab 动态分配 ~256 MB 预分配连续内存
流表填满时的表现 静默丢包(服务异常) 平滑淘汰最早闲置记录

RiceGuard Anycast 边缘架构的实战落地

RiceGuard 在其全球分布式 Anycast 汇聚节点中全面推行了 connection tracking 卸载机制:

  • 免疫状态耗尽型 DDoS 攻击:分布式拒绝服务攻击常通过海量伪造源端口刷爆 conntrack 连接表。RiceGuard 边缘入口在完成内存中的 Noise 加密身份验证之前,先通过 eBPF 执行无状态流过滤,使状态耗尽类攻击彻底失效。
  • 微秒级转发时延:通过省去 Netfilter 冗长流水线和锁开销,高带宽隧道数据包的节点穿透时延稳定保持在 1.5 微秒以内。
  • 与原生系统生态共存:底层路由栈与 BGP Anycast 守护进程无需改造,在保持全栈可观测性的同时,释放了网卡的物理线速处理能力。

通过结合 NOTRACK 规则与 eBPF 无锁流表,高并发 VPN 架构能够充分发挥现代多核服务器的硬件潜能,为海量并发业务提供确定性的高吞吐网络支撑。