iptables/netfilter 作为 Linux 网络的”瑞士军刀”服务了 Linux 二十余年,但在云原生时代,它的局限性日益明显:规则链线性匹配导致 O(n) 复杂度、连接跟踪表膨胀、kube-proxy 在大规模 Service 下性能急剧下降。eBPF 提供了一种全新的网络范式——用可编程的数据面替代固定的规则匹配,用 Map 查找替代线性遍历,用 XDP/TC 替代 Netfilter 钩子。
本章从宏观视角展示 eBPF 网络的全景,理解 eBPF 如何一步步替代传统网络组件。
一、传统网络栈的瓶颈
1.1 iptables 的性能问题
iptables 的核心问题:
| 问题 | 原因 | 影响 |
|---|---|---|
| O(n) 规则匹配 | 线性遍历规则链 | 规则数增加,延迟线性增长 |
| 连接跟踪表膨胀 | 每个连接一条记录 | 内存占用大,GC 开销高 |
| 规则更新慢 | iptables-restore 原子替换 | 大规模更新耗时秒级 |
| 无增量更新 | 替换是全量操作 | 无法高效添加/删除单条规则 |
| 锁竞争 | 全局 xt_table 锁 | 多核并发性能差 |
1.2 kube-proxy 的性能问题
kube-proxy 的 iptables 模式在 K8s 大规模集群中的问题:
- 每个 Service 产生 4-8 条 iptables 规则
- 10000 个 Service → 40000-80000 条规则
- 规则匹配延迟从微秒级升至毫秒级
- 规则更新(Service 变更)耗时可达秒级
二、eBPF 连接跟踪
2.1 传统 conntrack vs eBPF conntrack
| 维度 | Netfilter conntrack | eBPF conntrack |
|---|---|---|
| 数据结构 | 全局哈希表 + RCU | eBPF Map(LRU Hash) |
| 查找复杂度 | O(1) 但锁竞争 | O(1) 无锁(Per-CPU) |
| 内存管理 | 内核 slab 分配器 | Map 预分配 |
| 超时清理 | GC 定时器 | LRU 自动淘汰 |
| 可编程性 | 固定逻辑 | 完全可编程 |
| 可观测性 | /proc/net/nf_conntrack | Map 直接读取 |
2.2 TCP 连接跟踪状态机
eBPF conntrack 需要正确跟踪 TCP 连接的状态变迁,才能区分新连接、已建立连接和即将关闭的连接,从而决定是否放行或做 NAT:
在 eBPF conntrack 实现中,TCP 状态存储在 ct_value.state 字段,ingress 和 egress 程序根据 TCP 标志位(SYN/ACK/FIN/RST)更新状态。已进入 ESTABLISHED 状态的连接走快速路径——直接查 Map 获取后端信息,跳过策略匹配。
2.3 eBPF 连接跟踪实现
// 连接跟踪表struct ct_key { __u32 src_ip; __u32 dst_ip; __u16 src_port; __u16 dst_port; __u8 proto; __u8 pad[3];};
struct ct_value { __u64 packets; __u64 bytes; __u64 last_seen; // 用于超时判断 __u32 backend_ip; // 后端 IP(用于负载均衡) __u16 backend_port; __u8 state; // TCP 状态 __u8 flags;};
struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 1000000); __type(key, struct ct_key); __type(value, struct ct_value);} conntrack SEC(".maps");
SEC("tc")int tc_conntrack(struct __sk_buff *skb){ struct ct_key key = {}; // 解析数据包填充 key... if (parse_packet(skb, &key) < 0) return TC_ACT_OK;
struct ct_value *ct = bpf_map_lookup_elem(&conntrack, &key); if (ct) { // 已有连接:更新统计 __sync_fetch_and_add(&ct->packets, 1); ct->last_seen = bpf_ktime_get_ns(); } else { // 新连接:创建记录 struct ct_value new_ct = { .packets = 1, .last_seen = bpf_ktime_get_ns(), }; bpf_map_update_elem(&conntrack, &key, &new_ct, BPF_ANY); }
return TC_ACT_OK;}三、eBPF NAT
3.1 DNAT:入方向目标地址转换
// DNAT 规则表struct dnat_rule { __u32 new_dst_ip; __u16 new_dst_port;};
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); __type(key, __u16); // 服务端口 __type(value, struct dnat_rule);} dnat_rules SEC(".maps");
SEC("tc")int tc_dnat(struct __sk_buff *skb){ void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end;
// 解析数据包获取目标端口 __u16 dst_port = get_dst_port(data, data_end); if (dst_port == 0) return TC_ACT_OK;
// 查找 DNAT 规则 struct dnat_rule *rule = bpf_map_lookup_elem(&dnat_rules, &dst_port); if (!rule) return TC_ACT_OK;
// 修改目标 IP 和端口 struct iphdr *iph = data + sizeof(struct ethhdr); iph->daddr = rule->new_dst_ip;
struct tcphdr *tcp = (void *)(iph + 1); tcp->dest = rule->new_dst_port;
// 更新校验和 bpf_l3_csum_replace(skb, IP_CSUM_OFFSET, 0, 0, sizeof(__u32)); bpf_l4_csum_replace(skb, TCP_CSUM_OFFSET, 0, 0, sizeof(__u32) + sizeof(__u16));
return TC_ACT_OK;}3.2 SNAT:出方向源地址转换
SNAT 的核心难点在于校验和更新——修改源 IP 后,IP 头和 TCP/UDP 头的校验和都必须同步修正。bpf_csum_diff() 提供了增量校验和计算能力,避免重新计算整个校验和:
SEC("tc")int tc_snat(struct __sk_buff *skb){ void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return TC_ACT_OK; if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK;
struct iphdr *iph = (void *)(eth + 1); if ((void *)(iph + 1) > data_end) return TC_ACT_OK;
__u32 orig_src = iph->saddr; __u32 *new_src = bpf_map_lookup_elem(&snat_map, &orig_src); if (!new_src) return TC_ACT_OK;
// 增量更新 IP 校验和 __u32 old_csum = ~iph->check; old_csum = bpf_csum_diff(&orig_src, 4, new_src, 4, old_csum); iph->check = ~old_csum;
// 修改源 IP iph->saddr = *new_src;
// 增量更新 TCP/UDP 校验和(伪首部包含源 IP) if (iph->protocol == IPPROTO_TCP) { struct tcphdr *tcp = (void *)(iph + 1); if ((void *)(tcp + 1) > data_end) return TC_ACT_OK; tcp->check = bpf_csum_diff(&orig_src, 4, new_src, 4, tcp->check); } else if (iph->protocol == IPPROTO_UDP) { struct udphdr *udp = (void *)(iph + 1); if ((void *)(udp + 1) > data_end) return TC_ACT_OK; udp->check = bpf_csum_diff(&orig_src, 4, new_src, 4, udp->check); }
return TC_ACT_OK;}conntrack 表的容量规划直接影响网络稳定性。LRU Hash Map 满时,最早未使用的连接记录会被淘汰,导致已建立连接的后续数据包被当作新连接处理,可能触发错误的 NAT 或策略拒绝。生产环境建议按 峰值并发连接数 × 1.5 预分配,并监控 bpf_map_lookup_elem 失败率。
四、eBPF 替代 kube-proxy
4.1 kube-proxy 的工作模式
4.2 性能对比
| 指标 | kube-proxy iptables | eBPF (Cilium) |
|---|---|---|
| 规则匹配 | O(n) 线性 | O(1) Map 查找 |
| 连接建立延迟 | ~50-200μs | ~5-20μs |
| 规则更新 | 秒级(全量替换) | 毫秒级(增量 Map 更新) |
| 内存占用 | 与规则数成正比 | 与连接数成正比 |
| 可扩展性 | 1000 Service 后性能下降 | 10000+ Service 仍稳定 |
4.3 eBPF kube-proxy 替代的实现
// Service 表:Service ClusterIP → 后端列表struct svc_key { __u32 cluster_ip; __u16 port; __u8 proto; __u8 pad;};
struct svc_value { __u32 backend_ips[16]; // 后端 IP 列表 __u16 backend_ports[16]; __u8 count; // 后端数量 __u8 pad[3];};
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); __type(key, struct svc_key); __type(value, struct svc_value);} services SEC(".maps");
SEC("tc")int tc_kube_proxy(struct __sk_buff *skb){ struct svc_key key = {}; // 从 skb 提取 ClusterIP 和端口 key.cluster_ip = skb->remote_ip4; key.port = skb->remote_port;
// O(1) 查找 Service struct svc_value *svc = bpf_map_lookup_elem(&services, &key); if (!svc) return TC_ACT_OK;
// 选择后端(一致性哈希) __u32 hash = jhash_3words(key.cluster_ip, key.port, skb->mark, 0); __u32 idx = hash % svc->count;
// DNAT 到后端 // 修改目标 IP 和端口... return TC_ACT_OK;}4.4 一致性哈希后端选择
简单取模哈希(hash % count)在后端扩缩容时会导致大量连接重新分配,引发请求风暴。一致性哈希(Consistent Hashing)通过哈希环解决这一问题——后端增减时只有 1/N 的连接受影响:
// 一致性哈希环:虚拟节点 → 后端 IDstruct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); // 每个后端 128 个虚拟节点 __type(key, __u32); // 虚拟节点哈希值 __type(value, __u16); // 后端索引} hash_ring SEC(".maps");
static __always_inline int select_backend(__u32 hash){ // 顺时针查找第一个虚拟节点 for (__u32 i = 0; i < 256; i++) { __u32 key = hash + i; __u16 *backend = bpf_map_lookup_elem(&hash_ring, &key); if (backend) return *backend; } // 兜底:回到环首 __u32 first = 0; __u16 *backend = bpf_map_lookup_elem(&hash_ring, &first); return backend ? *backend : 0;}Cilium 的实现更精巧——使用 BPF_MAP_TYPE_LPM_TRIE 或数组模拟哈希环,后端变更时用户态 Agent 更新 Map,数据面无感知切换。
五、Socket 层 eBPF
5.1 BPF_PROG_TYPE_SOCKET_FILTER
Socket Filter 在 Socket 层过滤数据包,早于数据到达用户态:
SEC("socket")int socket_filter(struct __sk_buff *skb){ // 只允许 TCP 流量通过此 Socket if (skb->protocol == bpf_htons(ETH_P_IP)) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; struct iphdr *iph = data; if ((void *)(iph + 1) <= data_end && iph->protocol == IPPROTO_TCP) return 1; // 允许 } return 0; // 丢弃}5.2 BPF_PROG_TYPE_SOCK_OPS
Sock_ops 程序在 Socket 事件(建立、关闭、状态变化)时触发:
SEC("sockops")int bpf_sockops(struct bpf_sock_ops *skops){ switch (skops->op) { case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // 被动连接建立 break; case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: // 主动连接建立 break; case BPF_SOCK_OPS_STATE_CB: // TCP 状态变化 break; }
return 0;}5.3 BPF_PROG_TYPE_SK_MSG
Sk_msg 程序在 Socket 发送数据时触发,可以实现 Socket 消息重定向:
struct { __uint(type, BPF_MAP_TYPE_SOCKHASH); __uint(max_entries, 65536); __type(key, struct sock_key); __type(value, u32);} sock_map SEC(".maps");
SEC("sk_msg")int bpf_skmsg(struct sk_msg_md *msg){ // 将消息重定向到目标 Socket // 绕过内核协议栈,实现本地 Socket 直通 struct sock_key key = { .sip = msg->remote_ip4, .dip = msg->local_ip4, .sport = msg->remote_port, .dport = msg->local_port, }; bpf_msg_redirect_hash(msg, &sock_map, &key, BPF_F_INGRESS); return SK_PASS;}5.4 Socket 短路加速性能
Sk_msg + Sock_ops 组合实现的 Socket 短路(short-circuit)是 eBPF 网络中最容易被忽视却收益巨大的优化——同一节点上的两个 Pod 通信时,数据直接在内核 Socket 间传递,完全跳过 TCP/IP 协议栈:
| 路径 | 延迟 | 吞吐量 | 说明 |
|---|---|---|---|
| 传统路径(经协议栈) | ~45μs | ~12 Gbps | Socket → TCP → IP → TC egress → TC ingress → IP → TCP → Socket |
| Socket 短路(sk_msg) | ~8μs | ~40 Gbps | Socket → sk_msg redirect → Socket |
| 纯回环(loopback) | ~5μs | ~50 Gbps | 内核内部优化路径 |
Cilium 在同一节点 Pod 间通信时自动启用 Socket 短路,Sidecar 间通信(如 Istio 的 Envoy→Envoy)延迟可降低 60% 以上。
六、eBPF 与 nftables 对比
nftables 作为 iptables 的继任者,在语法和性能上都有改进,但与 eBPF 方案仍有本质差异:
| 维度 | nftables | eBPF (TC/XDP) |
|---|---|---|
| 规则匹配 | 集合/字典查找,O(1) | Map 查找,O(1) |
| 可编程性 | 声明式规则 + 简单表达式 | 完全可编程(C/Go/Rust) |
| 连接跟踪 | 依赖内核 conntrack | 自定义 LRU Map |
| 卸载能力 | 部分硬件卸载(tc flower) | XDP 卸载到网卡(SmartNIC) |
| 规则更新 | 原子替换,毫秒级 | Map 增量更新,微秒级 |
| 状态管理 | 无状态(依赖 conntrack) | 可在 Map 中维护任意状态 |
| L7 处理 | 不支持 | 可解析 HTTP/gRPC/Kafka 等 |
| 学习曲线 | 中等(nft 语法) | 陡峭(需 eBPF 开发能力) |
| 生态成熟度 | 高(主流发行版默认) | 中(Cilium/Katran 等项目) |
nftables 适合传统网络运维场景——防火墙、NAT、简单包过滤;eBPF 适合云原生场景——大规模 Service 负载均衡、L7 策略、零信任网络。两者并非互斥,可以共存:Cilium 在 kubeProxyReplacement=partial 模式下,仅用 eBPF 处理 K8s Service 流量,其余流量仍走 nftables。
七、eBPF 网络全景图
八、动手实践
8.1 对比 iptables 和 eBPF 的连接跟踪
# 查看传统 conntracksudo conntrack -Lsudo cat /proc/net/nf_conntrack
# 使用 bpftrace 追踪连接建立sudo bpftrace -e 'tracepoint:tcp:tcp_probe { @conn[pid, comm] = count();}'8.2 使用 eBPF 实现 Service 负载均衡
# 使用 Cilium 替代 kube-proxyhelm install cilium cilium/cilium \ --set kubeProxyReplacement=strict \ --set hubble.enabled=true
# 验证 eBPF 替代了 iptablessudo iptables -t nat -L KUBE-SERVICES# (应该为空或显示 Cilium 管理的规则)8.3 Socket 层 eBPF 加速
# 使用 bpftrace 追踪 Socket 操作sudo bpftrace -e 'tracepoint:sock:sock_recvmsg { @recv[comm] = count();}tracepoint:sock:sock_sendmsg { @send[comm] = count();}'conntrack 表的大小直接影响网络性能。默认的 conntrack 表大小(nf_conntrack_max)通常为 65536,在容器化环境中可能不够。可以通过 sysctl -w net.netfilter.nf_conntrack_max=262144 调大,但要注意内存消耗——每个 conntrack 条目约占用 336 字节。
九、本章小结
上一章深入解读了TC 流量控制与 eBPF的内部机制。
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 传统瓶颈 | iptables O(n) 匹配、conntrack 膨胀、kube-proxy 性能下降 | 传统瓶颈 |
| eBPF conntrack | LRU Hash Map 实现,O(1) 查找,自动淘汰 | eBPF conntrack |
| eBPF NAT | TC 层 DNAT/SNAT,Map 查找规则,增量更新 | eBPF NAT |
| kube-proxy 替代 | eBPF Map 查找替代 iptables 线性匹配,性能提升 5-10 倍 | kube-proxy 替代 |
| Socket 层 eBPF | Socket Filter、Sock_ops、Sk_msg 重定向 | Socket 层 eBPF |
| 全景图 | XDP → TC ingress → 路由 → Socket → TC egress → 发送 | 全景图 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






