eBPF 可观测性:内核可编程追踪与性能剖析

系统性 eBPF 可观测性实战:eBPF 架构与执行模型(Verifier/JIT/maps/Helper)、BCC 与 libbpf 开发框架对比、BPF 程序类型(kprobe/tracepoint/uprobe/XDP/TC/socket)、Go eBPF 开发(cilium/ebpf 库)、常见可观测性场景(函数追踪/延迟分析/火焰图/网络丢包/文件 IO/TCP 连接)、eBPF 安全与权限(CAP_BPF)、性能开销控制、与 Prometheus/Grafana 集成。附 C/libbpf 和 Go 实战代码。

eBPF 不是在 Linux 内核中添加了一个新功能,而是给内核装上了一个「可编程的虚拟机」。 它不修改内核源码、不加载内核模块,就能安全地在 Kernel Space 执行你的代码——在包到达用户态前拦截、在磁盘写入前审计、在函数调用发生时记录。这是过去只有内核开发者才能做到的事。


一、eBPF 核心架构

1.1 执行模型

用户态程序                    内核态 BPF VM
┌─────────────┐              ┌──────────────────────────────────┐
│ clang/llvm  │ 编译         │ ┌──────────────┐                 │
│ (C → BPF)   │ ───────→     │ │ BPF Verifier │ ← 安全检查      │
└─────────────┘              │ └──────┬───────┘                 │
                             │        ↓                        │
┌─────────────┐  loaded      │ ┌──────────────┐                │
│ loader tool │ ──────→      │ │   JIT Comp   │ ← x86/aarch64  │
│ (libbpf)    │ (bpf syscall)│ └──────┬───────┘                │
└─────────────┘              │        ↓                        │
      ↑                      │ ┌──────────────┐                │
      │ read maps            │ │  BPF Program │ ← 执行业务逻辑  │
      │ (user ring buffer)   │ └──────────────┘                │
      │                      └──────────────────────────────────┘
  ┌────┴────┐
  │ Maps    │ ← BPF 和用户态共享的 KV store
  │ (ring   │   (BPF_MAP_TYPE_HASH, ARRAY, RINGBUF, PERCPU_HASH...)
  │  buffer)│
  └─────────┘

1.2 安全机制

eBPF 程序加载前的受限执行环境:

1. Verifier(验证器)
   ├── 循环展开或有限循环(max_iterations)
   ├── 禁止空指针解引用
   ├── 禁止越界访问
   ├── 指令数限制(默认 100 万条)
   ├── 调用深度限制(max 32 层)
   └── 状态空间搜索证明无死循环

2. JIT 编译
   ├── BPF 字节码 → 原生机器码
   ├── 无解释执行开销
   └── 运行效率接近原生内核代码

3. 权限控制
   ├── CAP_BPF / CAP_SYS_ADMIN(执行权限)
   ├── 特权级 vs 非特权级(unprivileged BPF)
   └── Kernel Lockdown / LSM 限制

二、BPF 程序类型

类型触发点典型场景
kprobe/kretprobe内核函数进入/返回函数追踪、延迟测量
uprobe/uretprobe用户态函数进入/返回用户态库函数追踪
tracepoint内核静态追踪点标准化事件采集(调度/网络/Syscalls)
fentry/fexit内核函数(BPF 专用,比 kprobe 快)Go 应用追踪
XDP网卡驱动层DDoS 防护、负载均衡
TC (Traffic Control)网络栈入口/出口流量整形、网络监控
socket/sockopsSocket 事件TCP 状态追踪、连接监控
LSM (Linux Security Module)安全钩点安全审计、文件访问控制
BPF Iterator/proc/bpf 文件读取内核状态导出

三、BCC 快速原型

3.1 Hello eBPF

# hello.py (BCC)
from bcc import BPF

# C 代码定义 BPF 程序
prog = """
#include <linux/sched.h>

// 定义 ring buffer 输出结构
struct data_t {
    u32 pid;
    u32 uid;
    char comm[16];
    char message[12];
};

BPF_RINGBUF_OUTPUT(events, 1 << 4);

int hello(void *ctx) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_kernel_str(&data.message, sizeof(data.message), "Hello eBPF!");

    events.ringbuf_output(&data, sizeof(data), 0);
    return 0;
}
"""

b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")

print("Tracing clone()... Ctrl+C to exit")
try:
    while True:
        event = b["events"].event()
        print(f"PID={event.pid} UID={event.uid} COMM={event.comm.decode()} MSG={event.message.decode()}")
except KeyboardInterrupt:
    pass
sudo python3 hello.py
# 输出:
# PID=1234 UID=1000 COMM=bash MSG=Hello eBPF!

3.2 延迟追踪(kprobe + kretprobe)

# tcp_connect_latency.py
from bcc import BPF

prog = """
#include <net/sock.h>

BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);

int trace_connect_entry(struct pt_regs *ctx, struct sock *sk) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    return 0;
}

int trace_connect_return(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *tsp = start.lookup(&pid);
    if (tsp == 0) return 0;

    u64 delta = bpf_ktime_get_ns() - *tsp;
    dist.increment(bpf_log2l(delta / 1000000)); // ms
    start.delete(&pid);
    return 0;
}
"""

b = BPF(text=prog)
b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_entry")
b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_return")

print("Tracing TCP connect latency...")
try:
    sleep(999999)
except KeyboardInterrupt:
    b["dist"].print_log2_hist("ms")

四、libbpf + BPF CO-RE

4.1 为什么用 libbpf

维度BCClibbpf + CO-RE
依赖庞大(Clang/LLVM)轻量(只依赖 libbpf)
编译运行时编译预编译为 BPF ELF
可移植差(需内核头文件)CO-RE: Compile Once, Run Everywhere
性能中高(无运行时开销)
生产部署不便BPF ELF 直接加载

4.2 libbpf C 程序

// tcpconnect.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "GPL";

struct event {
    u32 pid;
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk) {
    struct event e = {};
    e.pid = bpf_get_current_pid_tgid() >> 32;

    BPF_CORE_READ_INTO(&e.saddr, sk, __sk_common.skc_rcv_saddr);
    BPF_CORE_READ_INTO(&e.daddr, sk, __sk_common.skc_daddr);
    BPF_CORE_READ_INTO(&e.sport, sk, __sk_common.skc_num);
    BPF_CORE_READ_INTO(&e.dport, sk, __sk_common.skc_dport);

    bpf_ringbuf_output(&events, &e, sizeof(e), 0);
    return 0;
}
# 构建(Makefile 简化)
clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 \
  -c tcpconnect.bpf.c -o tcpconnect.bpf.o

go build -o tcpconnect tcpconnect.go

五、Go eBPF 开发

5.1 cilium/ebpf(推荐)

package main

import (
    "fmt"
    "log"
    "net"
    "os"
    "os/signal"

    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang tcpconnect ./tcpconnect.bpf.c

type tcpconnectEvent struct {
    PID   uint32
    Saddr uint32
    Daddr uint32
    Sport uint16
    Dport uint16
}

func main() {
    // 解除 memlock 限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // 加载 BPF ELF
    objs := tcpconnectObjects{}
    if err := loadTcpconnectObjects(&objs, nil); err != nil {
        log.Fatalf("loading objects: %v", err)
    }
    defer objs.Close()

    // 附加 kprobe
    kp, err := link.Kprobe("tcp_v4_connect", objs.TcpV4Connect, nil)
    if err != nil {
        log.Fatalf("opening kprobe: %v", err)
    }
    defer kp.Close()

    // 读取 ring buffer
    rd, err := ringbuf.NewReader(objs.Events)
    if err != nil {
        log.Fatalf("opening ringbuf reader: %v", err)
    }
    defer rd.Close()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt)

    go func() {
        <-sig
        rd.Close()
    }()

    var event tcpconnectEvent
    for {
        record, err := rd.Read()
        if err != nil {
            if err == ringbuf.ErrClosed {
                return
            }
            log.Printf("reading from reader: %s", err)
            continue
        }

        event = tcpconnectEvent{}
        if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
            log.Printf("parsing ringbuf event: %s", err)
            continue
        }

        fmt.Printf("pid=%d %s:%d -> %s:%d\n",
            event.PID,
            intToIP(event.Saddr), event.Sport,
            intToIP(event.Daddr), ntohs(event.Dport))
    }
}

func intToIP(ip uint32) net.IP {
    b := make([]byte, 4)
    binary.LittleEndian.PutUint32(b, ip)
    return net.IP(b)
}

func ntohs(port uint16) uint16 {
    return (port>>8)&0xff | (port&0xff)<<8
}

六、可观测性场景

6.1 火焰图(Off-CPU / On-CPU)

# On-CPU 火焰图(谁在占用 CPU)
git clone https://github.com/iovisor/bcc
cd bcc/tools
sudo ./profile -F 99 -a -g -- 30 > out.stacks
# 生成火焰图
./flamegraph.pl out.stacks > oncpu.svg

# Off-CPU 火焰图(谁在等待)
sudo ./offcputime -f -p PID 30 > out.offcpu
./flamegraph.pl out.offcpu > offcpu.svg

6.2 文件 IO 追踪

# 用 BCC 追踪文件打开
sudo ./opensnoop
# PID    COMM               FD ERR PATH
# 1234   nginx               3   0 /var/log/nginx/access.log
# 1234   nginx               4   0 /etc/nginx/nginx.conf

# 追踪文件系统同步
sudo ./syncsnoop

# 追踪磁盘 IO
sudo ./biosnoop
# TIME(s)  COMM           PID    DISK    T    SECTOR    BYTES   LAT(ms)
# 0.000001 wget           4567   sda     W    1234567   4096    0.85

6.3 网络丢包分析

# 追踪 TCP 丢包原因
sudo ./tcpretrans
# TIME     PID    IP LADDR:LPORT          T> RADDR:RPORT          STATE
# 14:32:01 2345   4  10.0.1.2:443         R> 172.16.0.1:54321    ESTABLISHED

# 追踪 TCP 生命周期
sudo ./tcptracer
# T  PID    COMM             IP SADDR            DADDR            SPORT  DPORT
# A  1234   curl             4  10.0.1.2         172.217.0.1      54321  443
# X  1234   curl             4  10.0.1.2         172.217.0.1      54321  443

七、与 Prometheus/Grafana 集成

// 将 eBPF 采集的数据导出为 Prometheus metrics
// ebpf_exporter(开源项目)

// 配置:
// ebpf_exporter.yml
programs:
  - name: tcpconnect
    metrics:
      counters:
        - name: tcp_connections_total
          help: Total TCP connections by destination
          table: events
          labels:
            - name: daddr
              size: 4
              decoders:
                - name: inet_ip
# 查询 eBPF 导出的指标
sum by (daddr) (rate(tcp_connections_total[5m]))

# 与现有指标关联
sum by (pod) (
  rate(container_network_transmit_packets_total[5m])
) * on (pod) group_left (daddr)
sum by (pod, daddr) (rate(tcp_connections_total[5m]))

八、eBPF Checklist

检查项说明
内核版本≥ 4.18(CO-RE 需要 ≥ 5.2)
权限CAP_BPF 或 root
安全策略SELinux/AppArmor 可能阻止加载
不修改生产先在测试环境验证 Verifier 通过
性能开销每个 kprobe ~50-200ns,高频率注意
资源清理defer 确保 program/link/map 关闭
CO-RE使用 BTF(BPF Type Format),程序可移植

九、进阶主题与生产实践

9.1 BTF 与 CO-RE 可移植性详解

BTF(BPF Type Format)是内核数据结构类型信息的二进制序列化格式。没有 BTF 时,BPF 程序在不同内核版本上运行会因为结构体偏移变化而崩溃。CO-RE(Compile Once, Run Everywhere)通过 BTF 解决这一问题:

// 传统方式:直接访问 sk->__sk_common.skc_rcv_saddr
// 不同内核版本字段偏移可能不同 → 崩溃

// CO-RE 方式:使用 BPF_CORE_READ_INTO
BPF_CORE_READ_INTO(&e.saddr, sk, __sk_common, skc_rcv_saddr);
// 编译器在加载时根据目标内核的 BTF 重定位字段偏移
机制作用要求
BTF描述内核结构体类型和布局内核编译时开启 CONFIG_DEBUG_INFO_BTF
vmlinux.h从 BTF 自动生成的头文件包含所有结构体定义bpftool btf dump file /sys/kernel/btf/vmlinux format c
bpf_core_read()运行时根据 BTF 动态计算偏移libbpf ≥ 0.3

检查当前系统是否支持 BTF:

cat /boot/config-$(uname -r) | grep CONFIG_DEBUG_INFO_BTF
# CONFIG_DEBUG_INFO_BTF=y → 支持 BTF

ls /sys/kernel/btf/vmlinux
# 文件存在 → 运行时 BTF 可用

若内核不支持 BTF,可使用 pahole 工具手动生成 BTF 并加载到系统,或退回到编译时携带特定内核头文件的传统方式(牺牲可移植性)。

9.2 容器与 Namespace 追踪

容器内的进程与宿主机共享内核,eBPF 天然适合跨 Namespace 的细粒度监控。追踪容器需要理解 PID Namespace 的层级关系:

// 获取进程所在 Namespace 的 ID(用于关联到容器)
SEC("tracepoint/sched/sched_process_fork")
int trace_fork(void *ctx) {
    u64 cgroup_id = bpf_get_current_cgroup_id();
    u32 pid = bpf_get_current_pid_tgid() >> 32;

    // cgroup_id 可映射到容器运行时(containerd/docker)的容器 ID
    struct event e = {.pid = pid, .cgroup = cgroup_id};
    bpf_ringbuf_output(&events, &e, sizeof(e), 0);
    return 0;
}

配合 cgroupfs 或 containerd API 将 cgroup_id 映射到容器名/Pod 名,即可在 Grafana 中按容器维度聚合 eBPF 指标。Kubernetes 场景下,典型的关联链路为:

eBPF cgroup_id → /sys/fs/cgroup/.../cgroup.procs → containerd/containerd.sock
→ Pod Name + Namespace → Prometheus labels (pod, namespace, container)

9.3 基于 eBPF 的网络安全

eBPF 可在内核网络栈的多个节点拦截流量,实现高效的 L3~L7 安全策略:

挂载点位置安全能力
XDP网卡驱动层(最早)DDoS 缓解、MAC/IP 黑名单
TC网络设备 qdisc速率限制、连接追踪、协议合规
socketsocket 层进程级流量审计、加密检测
cgroup/skbcgroup 网络出口容器级防火墙策略

Cilium 是基于 eBPF 的 Kubernetes 网络策略实现代表,它用 eBPF 替代了传统 kube-proxy 的 iptables 规则:

对比维度iptablesCilium eBPF
规则匹配复杂度O(n)O(1)(哈希表查找)
连接追踪状态conntrackeBPF map(无 GC 开销)
服务发现iptables 规则更新eBPF map 原地更新(无需重算全量规则)
L7 策略不支持支持 HTTP/Header 级策略

9.4 LSM 安全检查点

Linux Security Module(LSM)提供内核安全钩点,eBPF 从内核 5.7 开始支持挂载 LSM 程序,实现运行时安全策略:

SEC("lsm/file_open")
int BPF_PROG(restrict_sensitive_files, struct file *file, int ret) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    // 禁止非白名单进程访问 /etc/shadow
    if (bpf_strncmp(file->f_path.dentry->d_name.name, 11, "/etc/shadow") == 0) {
        if (!is_allowed_process(pid)) {
            return -EPERM;  // 拒绝访问
        }
    }
    return ret;  // 放行
}

相比内核模块编写的 LSM(如 SELinux/AppArmor),eBPF LSM 的优势在于:

  • 可动态加载/卸载策略,无需重启或重编内核
  • Verifier 保证安全,不会出现内核模块的空指针或越界问题
  • 与可观测性共享基础设施,同一份 map 数据可用于审计日志

9.5 性能开销基准测试

eBPF 的开销取决于程序类型和触发频率。以下为典型基准(x86_64):

程序类型单次触发开销百万次/秒总开销备注
kprobe50~200 ns5~20% CPU可被 kretprobe 栈展开放大
tracepoint30~100 ns3~10% CPU静态点,无栈展开
fentry/fexit10~50 ns1~5% CPUBPF 专用,最快
XDP0.5~2 μs与包大小相关高端网卡可卸载到硬件
cgroup/skb20~80 ns2~8% CPUper-cgroup map 查找

控制开销的关键实践:

  1. 采样而非全量:高频率事件(如每个 syscall)使用统计采样,每 1000 次记录一次。
  2. 尽量使用 tracepoint/fentry:避免 kprobe 的栈展开开销。
  3. 批量读取 ringbuf:用户态消费者批量读取事件,减少系统调用次数。
  4. 限制 map 大小:使用 per-CPU map 避免跨核锁竞争。
  5. BPF 程序应短小精悍:复杂逻辑推送到用户态处理,内核态只做过滤和聚合。
// 优化示例:过滤后再写入 ringbuf,减少用户态负担
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx) {
    const char *filename = (const char *)ctx->args[1];
    char buf[32];
    bpf_probe_read_user_str(buf, sizeof(buf), filename);

    // 仅对敏感路径感兴趣
    if (bpf_strncmp(buf, 4, "/etc") != 0 &&
        bpf_strncmp(buf, 4, "/var") != 0) {
        return 0;
    }

    // ... 只有少量事件到达 ringbuf
    bpf_ringbuf_output(&events, &e, sizeof(e), 0);
    return 0;
}

9.6 内核版本兼容性速查

内核版本关键特性生产建议
4.18eBPF 基础支持、BPF_PROG_TYPE_KPROBE最低运行门槛,不建议新部署
5.2BPF CO-RE、BPF_MAP_TYPE_SK_STORAGECO-RE 最低版本,推荐最低标准
5.7BPF LSM 安全检查点运行时安全审计可用
5.8fentry/fexit、BPF iteratorGo 应用追踪首选
5.10BPF 程序可以 sleep(如 bpf_copy_from_user)复杂追踪场景
6.1+BPF struct ops、更完善 verifier最新稳定,长期支持

生产部署建议:目标内核 ≥ 5.4(Ubuntu 20.04 LTS),理想 ≥ 5.10(RHEL 9 / Ubuntu 22.04 LTS)。低于 5.2 的系统应优先升级操作系统而非妥协降级 eBPF 功能。


参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 仪表盘设计与可读性工程:信息层级、图表选型与避免误读
  2. 日志采样、去重与降噪:从每天 TB 级日志里捞出信号
  3. OpenMetrics 与指标规范治理:暴露格式、命名单位与兼容性