eBPF 是什么
eBPF(extended Berkeley Packet Filter)让你可以在不修改内核源码、不加载内核模块的前提下,把一小段经过安全校验的程序注入内核执行。
它已经成为云原生可观测性、网络与安全的事实标准:
- 网络:Cilium 用它替代 kube-proxy 与 iptables;
- 可观测性:Pixie、Parca、Pyroscope 用它采集指标与火焰图;
- 安全:Falco、Tetragon 用它做运行时行为审计。
一、最快的上手方式:bpftrace
# 安装
apt install -y bpftrace
# 统计所有进程的系统调用次数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 查看慢于 10ms 的文件读取
bpftrace -e '
tracepoint:syscalls:sys_enter_read /args->count > 0/ {
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read /@start[tid]/ {
$d = (nsecs - @start[tid]) / 1_000_000;
if ($d > 10) { printf("%s %dms\n", comm, $d); }
delete(@start[tid]);
}'一行命令就能看到内核视角的行为,这是传统 APM 做不到的。
二、用 Go 写一个真正的探针(Cilium eBPF 库)
先写 C 侧的 eBPF 程序:
// counter.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, __u32); // pid
__type(value, __u64); // 次数
} open_count SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")
int count_open(void *ctx) {
__u64 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *val = bpf_map_lookup_elem(&open_count, &pid);
if (val) {
__sync_fetch_and_add(val, 1);
} else {
__u64 init = 1;
bpf_map_update_elem(&open_count, &pid, &init, BPF_ANY);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";Go 侧加载与读取:
//go:build linux
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
func main() {
// 移除内存锁定限制(5.11+ 内核不需要)
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
spec, err := ebpf.LoadCollectionSpec("counter.bpf.o")
if err != nil {
log.Fatal(err)
}
coll, err := ebpf.NewCollection(spec)
if err != nil {
log.Fatalf("加载失败: %v", err)
}
defer coll.Close()
l, err := link.Tracepoint("syscalls", "sys_enter_openat", coll.Programs["count_open"], nil)
if err != nil {
log.Fatal(err)
}
defer l.Close()
log.Println("探针已挂载,Ctrl+C 退出")
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
// 读取结果
m := coll.Maps["open_count"].(*ebpf.Map)
var key uint32
var val uint64
iter := m.Iterate()
for iter.Next(&key, &val) {
log.Printf("pid=%d openat=%d\n", key, val)
}
}编译:
clang -O2 -target bpf -D__TARGET_ARCH_x86 -c counter.bpf.c -o counter.bpf.o
# 用 bpf2go 可以把 .o 直接嵌进 Go 二进制
GOOS=linux go generate ./...
go build -o probe三、CO-RE:一次编译,到处运行
早期 eBPF 最大的痛点是内核结构体布局差异。BPF CO-RE(Compile Once, Run Everywhere)配合 BTF 解决了这个问题:
#include <vmlinux.h>
#include <bpf/bpf_core_read.h>
SEC("kprobe/__x64_sys_execve")
int trace_exec(struct pt_regs *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// CO-RE 重定位:字段偏移在加载时按目标内核修正
pid_t pid = BPF_CORE_READ(task, pid);
char comm[16];
bpf_core_read_str(&comm, sizeof(comm), &task->comm);
bpf_printk("exec: pid=%d comm=%s\n", pid, comm);
return 0;
}只要目标内核带 BTF(/sys/kernel/btf/vmlinux,主流发行版默认开启),同一个 .o 文件就能跨内核版本运行。
四、生产落地注意点
- 验证器限制:eBPF 程序必须通过校验(无越界、无死循环)。早期内核指令数上限较低,复杂逻辑建议拆成多程序 + tail call。
- 开销可控:tracepoint/kprobe 每次事件都会执行,高频事件(如
sys_enter_*)务必在内核态聚合,不要每个事件都bpf_perf_event_output到用户态。 - 权限与安全:加载 eBPF 需要
CAP_BPF或 root;Kubernetes 里建议用 DaemonSet 集中采集,而不是给业务容器加特权。 - 可移植性:优先用稳定的 tracepoint 而非 kprobe;kprobe 符号名可能随版本变化。
五、工具选型速查
| 场景 | 推荐 |
|---|---|
| 临时排查 | bpftrace、BCC 工具集 |
| Go 长期服务 | cilium/ebpf + bpf2go |
| Rust | aya |
| Kubernetes 网络 | Cilium |
| 持续 profiling | Parca / Pyroscope |
小结
eBPF 把“观察系统”这件事从应用层下沉到了内核层,且成本极低——不改代码、不重启服务、开销可控。掌握它,等于拿到了一把能解释任何性能问题的万能钥匙。
评论 0
还没有评论,来说两句吧。