mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1854 字
5 分钟
eBPF 网络全景
2026-03-22

iptables/netfilter 作为 Linux 网络的”瑞士军刀”服务了 Linux 二十余年,但在云原生时代,它的局限性日益明显:规则链线性匹配导致 O(n) 复杂度、连接跟踪表膨胀、kube-proxy 在大规模 Service 下性能急剧下降。eBPF 提供了一种全新的网络范式——用可编程的数据面替代固定的规则匹配,用 Map 查找替代线性遍历,用 XDP/TC 替代 Netfilter 钩子。

本章从宏观视角展示 eBPF 网络的全景,理解 eBPF 如何一步步替代传统网络组件。

一、传统网络栈的瓶颈#

1.1 iptables 的性能问题#

flowchart LR PKT["数据包"] --> R1["Rule 1<br/>匹配?"] -->|"否"| R2["Rule 2<br/>匹配?"] R2 -->|"否"| R3["Rule 3<br/>匹配?"] R3 -->|"否"| RN["Rule N<br/>匹配?"] RN -->|"否"| DEFAULT["默认策略"] R1 -->|"是"| A1["执行动作 1"] R2 -->|"是"| A2["执行动作 2"] R3 -->|"是"| A3["执行动作 3"] RN -->|"是"| AN["执行动作 N"] style R1 fill:#ffcdd2,stroke:#c62828 style R2 fill:#ffcdd2,stroke:#c62828 style R3 fill:#ffcdd2,stroke:#c62828 style RN fill:#ffcdd2,stroke:#c62828

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 conntrackeBPF conntrack
数据结构全局哈希表 + RCUeBPF Map(LRU Hash)
查找复杂度O(1) 但锁竞争O(1) 无锁(Per-CPU)
内存管理内核 slab 分配器Map 预分配
超时清理GC 定时器LRU 自动淘汰
可编程性固定逻辑完全可编程
可观测性/proc/net/nf_conntrackMap 直接读取

2.2 TCP 连接跟踪状态机#

eBPF conntrack 需要正确跟踪 TCP 连接的状态变迁,才能区分新连接、已建立连接和即将关闭的连接,从而决定是否放行或做 NAT:

stateDiagram-v2 [*] --> SYN_SENT : 客户端 SYN SYN_SENT --> ESTABLISHED : 服务端 SYN+ACK → 客户端 ACK ESTABLISHED --> FIN_WAIT_1 : 主动关闭 FIN FIN_WAIT_1 --> FIN_WAIT_2 : 对端 ACK FIN_WAIT_2 --> TIME_WAIT : 对端 FIN TIME_WAIT --> [*] : 超时 2MSL ESTABLISHED --> CLOSE_WAIT : 对端 FIN CLOSE_WAIT --> LAST_ACK : 本端 FIN LAST_ACK --> [*] : 对端 ACK

在 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;
}
Warning

conntrack 表的容量规划直接影响网络稳定性。LRU Hash Map 满时,最早未使用的连接记录会被淘汰,导致已建立连接的后续数据包被当作新连接处理,可能触发错误的 NAT 或策略拒绝。生产环境建议按 峰值并发连接数 × 1.5 预分配,并监控 bpf_map_lookup_elem 失败率。

四、eBPF 替代 kube-proxy#

4.1 kube-proxy 的工作模式#

flowchart TB subgraph iptables模式["iptables 模式"] PKT1["数据包"] --> PREROUTING["PREROUTING<br/>DNAT 规则"] PREROUTING -->|"KUBE-SERVICES"| SVC["Service 链<br/>线性匹配"] SVC -->|"KUBE-SVC-XXX"| EP["Endpoint 链<br/>随机选择"] EP --> DNAT["DNAT 到后端 Pod"] end subgraph eBPF模式["eBPF 模式"] PKT2["数据包"] --> TC_BPF["TC eBPF 程序<br/>Map 查找"] TC_BPF -->|"O(1) 查找"| CT["连接跟踪表<br/>已有连接?"] CT -->|"是"| DIRECT["直接转发<br/>到已知后端"] CT -->|"否"| SELECT["后端选择<br/>一致性哈希"] SELECT --> DNAT2["DNAT 到后端 Pod"] end style iptables模式 fill:#ffcdd2,stroke:#c62828 style eBPF模式 fill:#c8e6c9,stroke:#2e7d32

4.2 性能对比#

指标kube-proxy iptableseBPF (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 的连接受影响:

// 一致性哈希环:虚拟节点 → 后端 ID
struct {
__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 GbpsSocket → TCP → IP → TC egress → TC ingress → IP → TCP → Socket
Socket 短路(sk_msg)~8μs~40 GbpsSocket → sk_msg redirect → Socket
纯回环(loopback)~5μs~50 Gbps内核内部优化路径

Cilium 在同一节点 Pod 间通信时自动启用 Socket 短路,Sidecar 间通信(如 Istio 的 Envoy→Envoy)延迟可降低 60% 以上。

六、eBPF 与 nftables 对比#

nftables 作为 iptables 的继任者,在语法和性能上都有改进,但与 eBPF 方案仍有本质差异:

维度nftableseBPF (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 网络全景图#

flowchart TB NIC["网卡"] --> XDP["XDP<br/>DDoS 防护<br/>L4 负载均衡<br/>NodePort 加速"] XDP -->|"XDP_PASS"| TC_IN["TC ingress<br/>连接跟踪<br/>DNAT<br/>策略执行"] TC_IN --> ROUTING["路由查找<br/>ip_route_input()"] ROUTING --> LOCAL["本地交付<br/>或转发"] LOCAL --> SOCKET["Socket 层<br/>Socket Filter<br/>Sock_ops<br/>Sk_msg 重定向"] SOCKET --> APP["用户态应用"] APP -->|"发送"| SK_EGRESS["Socket egress<br/>Sk_msg / Sk_skb"] SK_EGRESS --> ROUTING_OUT["路由查找<br/>ip_route_output()"] ROUTING_OUT --> TC_OUT["TC egress<br/>SNAT<br/>策略执行"] TC_OUT --> SEND["网卡发送"] style XDP fill:#c8e6c9,stroke:#2e7d32 style TC_IN fill:#bbdefb,stroke:#1565c0 style TC_OUT fill:#bbdefb,stroke:#1565c0 style SOCKET fill:#fff9c4,stroke:#f9a825

八、动手实践#

8.1 对比 iptables 和 eBPF 的连接跟踪#

# 查看传统 conntrack
sudo conntrack -L
sudo cat /proc/net/nf_conntrack
# 使用 bpftrace 追踪连接建立
sudo bpftrace -e '
tracepoint:tcp:tcp_probe {
@conn[pid, comm] = count();
}'

8.2 使用 eBPF 实现 Service 负载均衡#

# 使用 Cilium 替代 kube-proxy
helm install cilium cilium/cilium \
--set kubeProxyReplacement=strict \
--set hubble.enabled=true
# 验证 eBPF 替代了 iptables
sudo 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();
}'
Note

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 conntrackLRU Hash Map 实现,O(1) 查找,自动淘汰eBPF conntrack
eBPF NATTC 层 DNAT/SNAT,Map 查找规则,增量更新eBPF NAT
kube-proxy 替代eBPF Map 查找替代 iptables 线性匹配,性能提升 5-10 倍kube-proxy 替代
Socket 层 eBPFSocket Filter、Sock_ops、Sk_msg 重定向Socket 层 eBPF
全景图XDP → TC ingress → 路由 → Socket → TC egress → 发送全景图

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

eBPF 网络全景
https://blog.souloss.com/posts/ebpf/ebpf-networking/
作者
Souloss
发布于
2026-03-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
eBPF 可观测性
eBPF eBPF 最大的应用场景是可观测性——零侵入、低开销、内核级的全链路追踪。本章详解三大可观测性工具链——bpftrace(一行命令追踪内核)、BCC(Python 前端 + 丰富工具集)、Beyla(零侵入应用性能监控),并通过实战展示性能分析、分布式追踪、应用性能监控的完整工作流。
2
eBPF 与 WebAssembly
eBPF eBPF 提供内核可编程能力,WebAssembly 提供跨平台可移植性——两者的融合会带来什么?本章详解 Wasm-eBPF 项目、用户态 eBPF 运行时、eBPF 程序的 Wasm 封装,以及 eBPF + Wasm 在边缘计算、插件系统、跨平台可观测性中的应用前景。
3
TC:流量控制与 eBPF
eBPF TC(Traffic Control)是 Linux 内核的流量控制子系统,通过 cls_bpf 分类器可以在 TC 层挂载 eBPF 程序,实现灵活的数据包分类、修改和重定向。本章详解 TC eBPF 的架构、ingress/egress 双向处理、direct action 模式、sk_buff 操作,以及 TC 与 XDP 的选择策略。
4
eBPF Hook 点:kprobe/tracepoint/uprobe
eBPF eBPF 程序的价值在于它能挂载到内核的各种检查点——Hook 点。本章详解三大 Hook 机制——kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT 静态用户态追踪点,并通过实战代码展示每种 Hook 的使用方式与适用场景。
5
eBPF 生产部署
eBPF eBPF 生产部署实践——性能开销分析、调试技巧、版本兼容、内核版本要求与升级策略。