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),程序可移植

参考与延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「infra」更多文章

  1. 可观测性数据存储选型:TSDB、列式存储、对象存储与成本优化
  2. 云原生 APM 与性能剖析:Continuous Profiling 与火焰图
  3. Kubernetes 可观测性实战:集群、Pod、网络、存储全链路监控