一、eBPF 技术概述

1.1 什么是 eBPF

eBPF(Extended Berkeley Packet Filter)是 Linux 内核中的一项革命性技术,它允许用户在不修改内核源码、不加载内核模块的情况下,安全地在内核中运行自定义程序。eBPF 最初用于网络数据包过滤,如今已扩展为通用的内核观测和编程框架,成为云原生时代可观测性、安全和网络领域的基石技术。

1.2 eBPF 核心架构

eBPF 程序的生命周期包含以下关键步骤:用户空间编写 eBPF 程序(C 语言子集) → 通过 LLVM/Clang 编译为 eBPF 字节码 → 系统调用 bpf() 加载到内核 → 内核验证器(Verifier)进行安全校验 → JIT 编译为原生机器码 → 挂载到内核钩子点执行。验证器确保程序不会崩溃内核、不会无限循环、内存访问安全,这是 eBPF 区别于内核模块的核心安全保障。

1.3 eBPF 的三大应用场景

  • 可观测性(Observability): 系统调用追踪、函数调用栈分析、网络请求延迟测量、容器级别资源监控
  • 网络(Networking): XDP 高速数据包处理、负载均衡(Cilium)、服务网格数据平面加速
  • 安全(Security): 运行时安全监控(Falco)、系统调用过滤、文件访问审计

二、开发工具链实战

2.1 BCC(BPF Compiler Collection)

BCC 是最流行的 eBPF 开发框架,提供 Python/Lua 前端和 C 后端,内含大量开箱即用的观测工具:

# 安装 BCC(Ubuntu/Debian)
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

# 追踪所有 open() 系统调用
sudo opensnoop-bpfcc

# 统计块 I/O 延迟分布
sudo biosnoop-bpfcc

# 追踪 TCP 连接和接受
sudo tcpconnect-bpfcc
sudo tcpaccept-bpfcc

# 生成 CPU Flame Graph 所需的栈信息
sudo profile-bpfcc -F 99 -af 30 > out.stacks
FlameGraph/flamegraph.pl out.stacks > flame.svg

2.2 bpftrace 单行脚本

bpftrace 提供类似 awk 的高级语言,适合快速编写一行式追踪脚本:

# 统计每个进程的系统调用次数
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[comm] = count(); }'

# 追踪所有 malloc 调用及大小(用户态)
sudo bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { printf("malloc %d bytes\n", arg0); }'

# 监控超过 100ms 的磁盘 I/O
sudo bpftrace -e 'kprobe:blk_account_io_start { @start[tid] = nsecs; } kprobe:blk_account_io_done /@start[tid]/ { $duration = nsecs - @start[tid]; if ($duration > 100000000) { printf("slow I/O: %s %d ms\n", comm, $duration/1000000); } delete(@start[tid]); }'

2.3 libbpf 和 CO-RE

libbpf 是 C 语言原生 eBPF 库,配合 CO-RE(Compile Once, Run Everywhere)技术,通过 BTF(BPF Type Format)实现跨内核版本兼容:

// bootstrap.bpf.c - 最简单的 libbpf 程序
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, u64);
} exec_start SEC(".maps");

SEC("tp/sched/sched_process_exec")
int tracepoint__sched__sched_process_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
    bpf_printk("Process %d started at %llu\n", pid, ts);
    return 0;
}

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

三、系统调用追踪深度实战

3.1 进程Exec行为监控

基于 tracepoint 追踪进程创建,审计所有新启动的进程:

#!/usr/bin/env python3
from bcc import BPF
import time

prog = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u32 ppid;
    char comm[TASK_COMM_LEN];
    char filename[256];
};

BPF_PERF_OUTPUT(events);

TRACEPOINT_PROBE(sched, sched_process_exec) {
    struct data_t data = {};
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.ppid = task->real_parent->tgid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_str(&data.filename, sizeof(data.filename), (void *)args->filename);
    
    events.perf_submit(args, &data, sizeof(data));
    return 0;
}
"""

b = BPF(text=prog)
print("%-10s %-8s %-8s %-30s %s" % ("TIME(s)", "PID", "PPID", "COMMAND", "FILENAME"))

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("%-10.3f %-8d %-8d %-30s %s" % (
        time.time(), event.pid, event.ppid, 
        event.comm.decode(), event.filename.decode()))

b["events"].open_perf_buffer(print_event)
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

3.2 文件访问异常检测

监控敏感目录的异常文件访问行为:

#!/usr/bin/env python3
from bcc import BPF

prog = """
struct data_t {
    u32 pid;
    u32 uid;
    char comm[TASK_COMM_LEN];
    char fname[256];
    int flags;
};

BPF_PERF_OUTPUT(events);

int trace_do_sys_openat2(struct pt_regs *ctx, int dfd, 
                          const char __user *filename, 
                          struct open_how *how) {
    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_user_str(&data_fname, sizeof(data.fname), filename);
    data.flags = how->flags;
    
    if (data.fname[0] == '/' && data.fname[1] == 'e' && 
        data.fname[2] == 't' && data.fname[3] == 'c') {
        events.perf_submit(ctx, &data, sizeof(data));
    }
    return 0;
}
"""

b = BPF(text=prog)
b.attach_kprobe(event="do_sys_openat2", fn_name="trace_do_sys_openat2")

四、网络性能分析实战

4.1 TCP 连接全生命周期追踪

追踪 TCP 三次握手到四次挥手的完整过程,定位连接延迟瓶颈:

// tcp_lifecycle.bpf.c
struct sock_info {
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
    u32 pid;
    u64 timestamp;
};

BPF_HASH(tcp_state, struct sock *, struct sock_info);

SEC("tracepoint/tcp/tcp_set_state")
int trace_tcp_set_state(struct trace_event_raw_tcp_event_sk *ctx) {
    struct sock *sk = ctx->skaddr;
    u16 new_state = ctx->newstate;
    struct sock_info info = {};
    
    bpf_probe_read(&info.saddr, sizeof(info.saddr), &sk->__sk_common.skc_rcv_saddr);
    bpf_probe_read(&info.daddr, sizeof(info.daddr), &sk->__sk_common.skc_daddr);
    bpf_probe_read(&info.sport, sizeof(info.sport), &sk->__sk_common.skc_num);
    bpf_probe_read(&info.dport, sizeof(info.dport), &sk->__sk_common.skc_dport);
    info.pid = bpf_get_current_pid_tgid() >> 32;
    info.timestamp = bpf_ktime_get_ns();
    
    bpf_map_update_elem(&tcp_state, &sk, &info, BPF_ANY);
    return 0;
}

4.2 HTTP 请求延迟测量

通过 uprobe 追踪 nginx/Go HTTP 框架的请求处理延迟:

# 追踪 Go HTTP 请求处理延迟
sudo bpftrace -e '
uprobe:./app:http.HandlerFunc.ServeHTTP {
    @start[tid] = nsecs;
}
uretprobe:./app:http.HandlerFunc.ServeHTTP /@start[tid]/ {
    $latency = (nsecs - @start[tid]) / 1000000;
    @latency_ms = hist($latency);
    if ($latency > 100) {
        printf("SLOW: %s took %d ms\n", comm, $latency);
    }
    delete(@start[tid]);
}'

# 统计各路径的延迟分布
sudo bpftrace -e '
uprobe:./app:github.com/gin-gonic/gin.(*Context).Next {
    @path_start[tid] = nsecs;
}
uretprobe:./app:github.com/gin-gonic/gin.(*Context).Next /@path_start[tid]/ {
    $ms = (nsecs - @path_start[tid]) / 1000000;
    @path_latency[str(arg0)] = hist($ms);
    delete(@path_start[tid]);
}'

五、CPU 性能分析与火焰图

5.1 生成火焰图的完整流程

# 步骤1: 使用 profile-bpfcc 采集栈采样数据
sudo profile-bpfcc -F 99 -f 60 > out.stacks 2>&1

# 步骤2: 折叠栈格式转换生成 SVG
cat out.stacks | ./flamegraph.pl --color=java --title="CPU Flame Graph" > flame.svg

# 步骤3: 针对特定进程采集
sudo profile-bpfcc -F 99 -p $(pgrep -n myapp) 30 > myapp.stacks

# 步骤4: 包含用户态+内核态+OFF-CPU栈
sudo profile-bpfcc -F 99 30 -U > full.stacks

# 步骤5: Off-CPU 分析(阻塞时间热力图)
sudo offcputime-bpfcc -df -p $(pgrep -n myapp) 10 > offcpu.stacks
./flamegraph.pl --color=io --title="Off-CPU Time" offcpu.stacks > offcpu.svg

5.2 调度器延迟分析

追踪进程调度延迟,识别 CPU 争用问题:

# 统计调度运行队列延迟
sudo runqlat-bpfcc 1 10

# 输出示例:
#     usecs               : count     distribution
#         0 -> 1          : 0        |                                        |
#         2 -> 3          : 0        |                                        |
#         4 -> 7          : 1        |                                        |
#         8 -> 15         : 12       |**                                      |
#        16 -> 31         : 45       |********                                 |
#        32 -> 63         : 187      |*****************************************|
#        64 -> 127        : 98       |*********************                    |
#       128 -> 255        : 23       |****                                     |
#       256 -> 511        : 5        |*                                        |

# 追踪调度延迟直方图(按进程分组)
sudo runqlen-bpfcc -m 1 5

六、容器与云原生环境监控

6.1 容器级别资源监控

cgroup 级别的 eBPF 程序,为每个容器独立统计资源使用:

// container_monitor.bpf.c
struct container_stat {
    u64 cpu_time_ns;
    u64 io_read_bytes;
    u64 io_write_bytes;
    u64 net_rx_bytes;
    u64 net_tx_bytes;
    u64 mem_usage;
    u32 pid_count;
};

BPF_HASH(cgroup_stats, u64, struct container_stat);

SEC("cgroup/skb")
int cgroup_skb_monitor(struct __sk_buff *skb) {
    u64 cgroup_id = bpf_skb_cgroup_id(skb);
    struct container_stat *stat = cgroup_stats.lookup(&cgroup_id);
    if (!stat) {
        struct container_stat new_stat = {};
        cgroup_stats.update(&cgroup_id, &new_stat);
        stat = cgroup_stats.lookup(&cgroup_id);
    }
    if (skb->ingress_ifindex) {
        stat->net_rx_bytes += skb->len;
    } else {
        stat->net_tx_bytes += skb->len;
    }
    return 1;
}

6.2 Kubernetes Pod 网络拓扑

自动发现 Kubernetes Pod 间的网络流量关系,生成实时拓扑:

# 使用 Pixie(基于 eBPF 的 K8s 可观测平台)
px deploy
px script px/service_stats  # 自动获取所有服务间流量

# 使用 Hubble(Cilium 的 eBPF 可观测层)
hubble observe --namespace default --protocol http
hubble observe --namespace kube-system --verdict DROPPED

# 自定义 bpftrace 脚本追踪 DNS 请求
sudo bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo {
    printf("PID %d (%s) DNS lookup: %s\n", pid, comm, str(arg0));
}'

七、高级 eBPF 技术模式

7.1 BPF Maps 数据交换机制

BPF Maps 是内核态 eBPF 程序与用户态程序之间的数据桥梁:

// 丰富的 Map 类型满足不同场景

// 1. BPF_MAP_TYPE_HASH - 通用键值存储
BPF_HASH(connectsock, struct sock *, u64);

// 2. BPF_MAP_TYPE_PERF_OUTPUT - 高性能事件流
BPF_PERF_OUTPUT(events);

// 3. BPF_MAP_TYPE_RING_BUFFER - 新版高效环形缓冲区(推荐)
BPF_RINGBUF_OUTPUT(events, 1 << 12);

// 4. BPF_MAP_TYPE_LRU_HASH - 自动淘汰的有限容量哈希表
BPF_LRU_HASH(cache, u64, struct data, 1024);

// 5. BPF_MAP_TYPE_STACK_TRACE - 内核栈采样存储
BPF_STACK_TRACE(stack_traces, 1024);

// 6. BPF_MAP_TYPE_LPM_TRIE - 最长前缀匹配(路由/防火墙)
BPF_LPM_TRIE(subnets, struct subnet_key, u32, 10000);

// 7. BPF_MAP_TYPE_SOCKMAP / BPF_MAP_TYPE_DEVMAP - 网络转发加速
BPF_SOCKMAP(sock_map, 65535);

// 环形缓冲区示例
SEC("tp/sched/sched_process_exit")
int trace_process_exit(struct trace_event_raw_sched_process_template *ctx) {
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    e->exit_code = ctx->exit_code;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);
    return 0;
}

7.2 Tail Call 尾调用链

通过尾调用将复杂的 eBPF 程序拆分为多个独立模块:

// 尾调用映射表
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __type(key, u32);
    __type(value, u32);
    __uint(max_entries, 8);
} prog_array SEC(".maps");

// 主入口函数
SEC("xdp")
int xdp_main(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    
    // IPv4 → 跳转到路由模块(index=0)
    if (eth->h_proto == bpf_htons(ETH_P_IP))
        bpf_tail_call(ctx, &prog_array, 0);
    // IPv6 → 跳转到 IPv6 处理模块(index=1)
    else if (eth->h_proto == bpf_htons(ETH_P_IPV6))
        bpf_tail_call(ctx, &prog_array, 1);
    // ARP → 跳转到 ARP 处理模块(index=2)
    else if (eth->h_proto == bpf_htons(ETH_P_ARP))
        bpf_tail_call(ctx, &prog_array, 2);
    
    return XDP_PASS;
}

// 子模块:IPv4 路由
SEC("xdp")
int xdp_ipv4_route(struct xdp_md *ctx) { return XDP_PASS; }

// 子模块:IPv6 处理
SEC("xdp")
int xdp_ipv6_route(struct xdp_md *ctx) { return XDP_PASS; }

八、生产环境部署最佳实践

8.1 性能开销控制

eBPF 程序引入的性能开销极低,但生产中仍需注意采样频率和程序复杂度:

  • 采样频率: CPU profiling 推荐 99Hz(而非更高频率),足以捕获热点同时开销可控
  • 程序复杂度: 验证器限制程序不超过 100 万指令,避免在热路径上执行复杂计算
  • 批量化输出: 优先使用 Ring Buffer 替代 Perf Buffer,减少事件传输的 per-event 开销
  • 条件过滤: 尽早过滤无关事件,减少向用户态推送的数据量

8.2 安全权限管理

# eBPF 系统调用需要的能力
sudo setcap cap_sys_admin+ep /usr/sbin/bpftrace

# 或者(Linux 5.8+)拆分细粒度能力
sudo setcap cap_bpf,cap_perfmon,cap_net_admin,cap_sys_resource+ep /path/to/binary

# 通过 sysctl 控制 eBPF 对非特权用户的访问
sysctl -w kernel.unprivileged_bpf_disabled=1

# 查看当前 eBPF 程序加载情况
sudo bpftool prog show
sudo bpftool map show
sudo bpftool net show

九、总结

eBPF 正在重塑 Linux 内核可观测性的格局。相比传统工具(strace、tcpdump、perf),eBPF 提供以下核心优势:极致低开销(纳秒级执行、可安全用于生产环境)、全栈可见性(同时观测用户态+内核态+硬件事件)、动态性(无需重启即可部署观测策略)。从 BCC 快速原型到 libbpf CO-RE 生产部署,eBPF 工具链已覆盖了从开发调试到生产监控的完整生命周期。在云原生时代,eBPF 已成为 Cilium、Pixie、Falco 等明星项目的底层基石,是每一位 Linux 工程师必须掌握的核心技术。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.357928s