Files
MyBlog-Next/src/content/posts/ebpf-practical-notes.mdx
T
2026-08-10 14:47:34 +00:00

241 lines
8.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "eBPF 不只是可观测性:一些内核追踪和网络加速的实战笔记"
description: "跳出教科书式的 eBPF 介绍,聊点真正干活用的东西——从 XDP 到 TC,从 kprobe 到 BPF 链表,以及那些文档不会告诉你的坑。"
pubDatetime: 2026-08-10
author: "Qihan"
tags: ["Linux", "内核", "eBPF", "网络"]
---
写这篇东西的起因很简单:最近折腾了一阵 eBPF,网上教程不少,但大多数不是停留在"hello world"就是复制粘贴 kernel 文档。真正上手做点实际的事——比如用 XDP 挡 DDOS、用 TC 做透明代理——中间踩的坑,很少有文章好好讲。
这篇不打算再从头给你讲一遍 eBPF 是什么、verifier 怎么工作,那些官方文档写得够清楚了。我更想聊的是:**你在做 eBPF 程序时大概率会遇到但文档没写的东西**。
---
## XDP 的几件事,和你想的不太一样
### XDP 不一定要网卡支持 native
很多文章告诉你 XDP 需要网卡驱动支持 native mode——严格来说没错,但实战中你八成会用 generic mode`XDP_FLAGS_SKB_MODE`)做开发调试,最后再切到 native。用 `ip link set dev eth0 xdpgeneric obj xdp_prog.o` 就行,内核会把 SKB 模式的 BPF 程序挂在入口。
性能差距当然有——generic mode 走了完整协议栈再到 BPF,native 在驱动层就截住了。但开发阶段差那几微秒无所谓,先把逻辑调通。
### XDP_PASS 不是"绕过协议栈"
有同学以为 XDP_PASS 过的包直接进 TCP/IP 栈。实际上 **XDP_PASS 只是让包回到正常的内核网络路径**,该过的 netfilteriptables/nftables)、路由决策一个都不少。你在 XDP 里做了合法性过滤,PASS 出去的包该被 iptables 拦还是被拦。
### XDP_TX 的典型场景:不是回包而是 hairpin
XDP_TX 可以把包从收到它的网卡原路丢回去。很多人以为这是用来构造回包的,但更常见的落地场景是做 **hairpin load balancing**——比如在 LVS/Direct Routing 场景下,从 eth0 进来的包处理后直接从 eth0 TX 回去,不走路由表。
性能好归好,但前提是网卡驱动支持。ixgbe、mlx5 这些主流没问题,有些虚拟网卡(virtio_net 的老版本)就不行。
---
## TC BPF:比 iptables 更灵活的流量控制
### clsact qdisc 是更干净的挂载点
传统用 `tc filter add dev eth0 ingress` 挂 ingress BPF,但你得先有一个 root qdisc。从 kernel 4.12 开始,推荐用 clsact
```bash
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc_ingress
tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc_egress
```
clsact 的好处是不用操心 qdisc 链,直接挂 ingress/egress。注意参数 `da`direct-action)——没它 filter 只做分类不执行 action,新手最容易漏掉。
### 绕过 conntrack 做有状态过滤
Netfilter 的 conntrack 在大量连接场景下性能瓶颈很明显。TC BPF 里可以用 `bpf_skb_load_bytes`+`bpf_map_lookup_elem` 自己做状态表。实测 64 字节的 BPF_MAP_TYPE_HASH 存五元组,单核跑满 10G 线速比 iptables conntrack 能省 30-40% CPU。
代价是得手写超时回收逻辑。`BPF_MAP_TYPE_HASH` 不支持自动过期,可以配合 `bpf_timer`5.15+)做定时清理。
---
## BPF 链表(Linked List)是 6.6 才真正能用的
6.6 kernel 之前,`BPF_MAP_TYPE_TASK_STORAGE`、`BPF_MAP_TYPE_CPUMAP` 这类 map 用起来绑手绑脚。6.6 引入了 BPF linked list 和 `bpf_list_node`,让 eBPF 程序能维护更复杂的数据结构。
```c
struct node {
__u64 value;
struct bpf_list_node list;
};
struct array_map {
__u64 count;
struct bpf_list_head head __contains(node, list);
};
```
但有个限制很烦:**链表头只能在 map value 里定义**,不能直接在全局变量里用。而且 `bpf_list_push_front/back` 这些 helper 对指针的有效性检查非常严格——说白了还是 verifier 那套安全机制的锅。
---
## kprobe/fentry 的选择:不要惯性用 kprobe
如果你还在用 `SEC("kprobe/xxx")`,可以考虑换 fentry/fexit 了。
```c
// kprobe 方式
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
// ...
}
// fentry 方式(5.5+
SEC("fentry/tcp_v4_connect")
int BPF_PROG(tcp_v4_connect, struct sock *sk)
{
// ...
}
```
fentry 的优势:
- 性能更好:kprobe 通过断点/跳转指令模拟,fentry 是直接在函数入口插入 trampoline,延迟更低
- 参数直接可读:kprobe 用 `PT_REGS_PARM1` 这些宏间接拿参数,fentry 用签名就能直接取
- 稳定性高:不再依赖内核函数的 prologue 结构
前提是内核开了 `CONFIG_FUNCTION_TRACER` 和 `CONFIG_BPF_KPROBE_OVERRIDE`。LTS 内核基本都支持了,如果你还在用 4.x ——那就安心用 kprobe 吧。
---
## 实际踩过的坑
### 1. BPF 栈大小只有 512 字节
这是新手第一道坎。BPF 程序不能声明大的局部变量,超过 512 字节 verifier 直接拒绝。解法很简单——用 BPF map`BPF_MAP_TYPE_PERCPU_ARRAY` 做临时 buffer)。
```c
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct my_big_struct);
} tmp_buf SEC(".maps");
// 用的时候
__u32 zero = 0;
struct my_big_struct *buf = bpf_map_lookup_elem(&tmp_buf, &zero);
if (!buf) return XDP_ABORTED;
```
### 2. bpf_printk 不是 printf
`bpf_printk` 最多支持 **三个** 格式化参数,而且只认 `%d %u %x %ld %lu %lld %llu %p %s` 这几种,不认 `%f` 或 `%zu`。想打的变量太多?分多次调用或者用 `BPF_MAP_TYPE_PERF_EVENT_ARRAY` 发到用户态。
```c
bpf_printk("pid %u sent %llu bytes to port %u\\n", pid, bytes, port);
```
多一个参数都不行,verifier 会让你怀疑人生。
### 3. verifier 的复杂路径限制
BPF 程序有复杂性限制(complexity limit),循环数、分支数、指令数超标就加载失败。
最烦人的是 **verifier 并不知道你的循环真的会终止**。比如:
```c
for (i = 0; i < max; i++) {
if (arr[i] > threshold) { /* do something */ }
}
```
如果 `max` 是一个变量,verifier 会拒绝。得用 `#pragma unroll` 让编译器展开,或者用 `bpf_loop` helper5.17+)。
### 4. 尾调用有 stack 限制
`bpf_tail_call` 不增加函数调用栈,但 tail call chain 最多 **33 层**`MAX_TAIL_CALL_CNT`)。超过这个数 tail call 静默失败,返回 0。而且 tail call 之后当前 BPF 程序的栈内容全部消失——所以没法"partial execute + tail call"做分段处理。
---
## 实战:XDP 源 IP 速率限制
贴一个我在用的简化版 XDP,用 `BPF_MAP_TYPE_LRU_HASH` 做单 IP 速率限制:
```c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define RATE_LIMIT_PPS 1000
struct ip_rate_key {
__u32 src_ip;
};
struct ip_rate_val {
__u64 last_seen;
__u32 pps;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000);
__type(key, struct ip_rate_key);
__type(value, struct ip_rate_val);
} rate_map SEC(".maps");
SEC("xdp")
int xdp_rate_limit(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_ABORTED;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS;
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end)
return XDP_ABORTED;
struct ip_rate_key key = { .src_ip = ip->saddr };
struct ip_rate_val *val, new_val = {};
__u64 now = bpf_ktime_get_ns();
val = bpf_map_lookup_elem(&rate_map, &key);
if (val) {
__u64 elapsed = now - val->last_seen;
if (elapsed < 1000000000ULL) {
val->pps++;
if (val->pps > RATE_LIMIT_PPS)
return XDP_DROP;
} else {
val->last_seen = now;
val->pps = 1;
}
} else {
new_val.last_seen = now;
new_val.pps = 1;
bpf_map_update_elem(&rate_map, &key, &new_val, BPF_NOEXIST);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
```
BPF_MAP_TYPE_LRU_HASH 自带最近最少使用淘汰,不会让 map 无限膨胀。rate limit 阈值根据实际场景调就行。
---
## 几个值得留意的方向
- **BPF 可移植性**CO-RE + BTF 基本解决了跨内核版本的问题,别再写判断内核版本号的拙劣 hack 了
- **用户态 BPF 库**libbpf 比 bcc 轻量,从 libbpf-bootstrap 起步挺舒服
- **sched_ext**SCX):6.12 合入的 BPF 调度器框架,用 BPF 写 CPU 调度策略——这件事本身够开一长篇
- **BPF 安全审计**`BPF_MAP_TYPE_RINGBUF` + fentry 做内核审计日志,比传统 auditd 性能好不少
---
差不多就这些。都是实际项目里摸出来的,有人能少走点弯路就好。