引言

网络是分布式系统的命脉。传统的网络观测工具(tcpdump、iptables LOG、systemtap)要么性能开销大,要么需要重新编译内核。eBPF的出现改变了这一切——它在内核中安全地执行用户定义的字节码,实现零拷贝、低开销的全链路网络观测。

1. eBPF 网络观测的三层架构

Linux 网络栈为不同层次的观测提供了丰富的 eBPF hook 点:

┌──────────────────────────────────────────────────────┐
│  应用层: socket / syscall (tracepoint/kprobe)        │
├──────────────────────────────────────────────────────┤
│  协议栈层: TC, cgroup, setsockopt (cgroup/BPF)       │
├──────────────────────────────────────────────────────┤
│  驱动层: XDP (eXpress Data Path) — 最早拦截点        │
└──────────────────────────────────────────────────────┘

2. XDP:驱动层的极速包处理

XDP 在网卡驱动收到数据包后、甚至在 sk_buff 分配之前执行 eBPF 程序,实现包处理的最快路径:

  • 性能:单核可处理 2400 万 pps(比 DPDK 在某些场景更优)。
  • 典型场景:DDoS 防护、负载均衡、包过滤。
  • 程序返回码:
    • XDP_PASS:继续走正常网络栈。
    • XDP_DROP:直接丢弃。
    • XDP_TX:从原接口发回。
    • XDP_REDIRECT:转发到另一个接口或 CPU。

2.1 XDP 程序示例:简单 SYN 洪水检测

SEC("xdpDropSYN")
int xdp_drop_syn(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;
    
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;
    
    if (ip->protocol != IPPROTO_TCP)
        return XDP_PASS;
    
    struct tcphdr *tcp = (void *)(ip + 1);
    if ((void *)(tcp + 1) > data_end)
        return XDP_PASS;
    
    // 检测 SYN 包
    if (tcp->syn && !tcp->ack) {
        // 可以查询 count map 中的 SYN 计数
        return XDP_DROP;
    }
    
    return XDP_PASS;
}

3. TC eBPF:流量控制层的深度观测

TC(Traffic Control)eBPF 在协议栈中比 XDP 更早于协议处理的位置执行:

  • ingress:在包进入协议栈之前。
  • egress:在包离开协议栈之后。
  • 优势:可以看到 sk_buff、sk 结构体,做出更精细的决策。

3.1 TC BPF 程序标记 sk

TC eBPF 可以给 socket 打标记,实现跨协议栈的流量分类和策略:

SEC("tc_mark")
int tc_mark_skb(struct __sk_buff *skb)
{
    // 从 skb 中提取五元组
    __u32 src_ip = skb->remote_ip4;
    __u32 dst_ip = skb->local_ip4;
    __u16 src_port = skb->remote_port & 0xFFFF;
    __u16 dst_port = bpf_ntohs(skb->local_port);
    
    // 查找策略 BPF map
    struct policy_key key = { .dst_port = dst_port };
    struct policy_val *val = bpf_map_lookup_elem(&policy_map, &key);
    if (val) {
        // 标记该 skb,后续 eBPF 程序或 classifier 可使用
        bpf_skb_store_bytes(skb, offsetof(struct sk_buff, mark),
                            &val->mark, sizeof(val->mark), 0);
    }
    return TC_ACT_OK;
}

4. Socket 层追踪:tracepoint 与 kprobe

应用层的网络追踪主要通过三种机制:

4.1 Tracepoint 追踪

内核中预定义的稳定观测点,性能开销极低:

  • sock:inet_sock_set_state:TCP 状态机变化(ESTABLISHED、CLOSE_WAIT 等)。
  • tcp:tcp_retransmit_skb:TCP 重传事件。
  • syscalls:sys_enter_sendto / sys_exit_sendto:系统调用入口/出口。

4.2 kprobe/kretprobe 动态追踪

几乎可以挂载到任何内核函数,灵活但接口不稳定:

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u16 family = sk->sk_family;
    
    struct event evt = {0};
    evt.pid = pid;
    evt.size = size;
    evt.family = family;
    
    if (family == AF_INET) {
        BPF_CORE_READ_INTO(&evt.daddr, sk, __sk_common.skc_v4_daddr);
        BPF_CORE_READ_INTO(&evt.dport, sk, __sk_common.skc_dport);
    }
    
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt));
    return 0;
}

4.3 Socket Filter 与 cgroup BPF

  • Socket Filter:传统 BPF,用于 SO_ATTACH_FILTER。
  • cgroup/BPF:绑定到 cgroup,对该 cgroup 内所有进程生效。
  • BPF_PROG_TYPE_SK_SKB:socket 级别的包过滤和重定向。
  • BPF_PROG_TYPE_SK_MSG:socket 级别消息重定向。

5. 用户态工具链

5.1 BCC(BPF Compiler Collection)

基于 Python 的快速原型开发工具,内置丰富脚本:

# 追踪 TCP 连接建立和关闭
$ tcpconnect.py -t

# 统计 TCP 重传次数
$ tcpretrans.py

# 观测 HTTP 请求延迟
$ biolatency.py -m

5.2 bpftool

lsmod 时代的 BPF 管理和调试工具:

# 查看已加载的 BPF 程序
$ bpftool prog show

# 查看 BPF 映射
$ bpftool map show

# 导出程序 JIT 后的 x86 汇编
$ bpftool prog dump xlated id 42

# 导出 map 中的数据
$ bpftool map dump id 56

5.3 libbpf / BPF CO-RE

用户态直接使用 C 编写可移植的 BPF 程序:

// 通过 BPF CO-RE (Compile Once, Run Everywhere) 
// 无需在每台目标机器上编译,自动适配不同内核版本
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, u32);
    __type(value, struct conn_info);
} conn_map SEC(".maps");

SEC("tracepoint/sock/inet_sock_set_state")
int trace_tcp_state(struct trace_event_raw_inet_sock_set_state *ctx)
{
    // 自动处理 __vmlinux.h 中的结构体定义差异
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    bpf_map_update_elem(&conn_map, &pid, &info, BPF_ANY);
    return 0;
}

6. 生产环境最佳实践

  • 返回码语义:XDP/TC 的返回码直接决定包的处理流程,务必准确。
  • Map 大小控制:BPF Map 预分配内存,生产环境有严格上限(RLIMIT_MEMLOCK)。
  • 尾调用(Tail Call):复杂逻辑拆分多个程序,通过 bpf_tail_call 跳转,突破指令数限制。
  • Verifier 兼容:严格遵守 verifier 规则(边界检查、无死循环、有限栈空间)。
  • 性能观测:BPF 程序自身的执行时间也要监控,避免"观测系统"成为瓶颈。

7. 内核 BPF 子系统演进

Linux 内核 BPF 子系统持续快速迭代:

  • BPF trampoline:替代 kprobe 的轻量级调用,性能提升 10 倍+。
  • BPF iterator:安全遍历内核数据结构(任务、网络连接、BPF map)。
  • BPF crypto:内核 TLS(kTLS)的加密操作卸载到 BPF。
  • BPF smp:BPF 程序直接执行 smp_call_function,跨 CPU 操作。

总结

eBPF 网络观测体系覆盖了从驱动层到应用栈的全部路径。XDP 提供极速包处理,TC 实现灵活策略控制,socket 层追踪则深入到应用行为的微观细节。掌握这些工具的组合运用,可以实现生产级别的网络全链路可观测性,而无需修改内核源码或引入探针。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部