eBPF 内核编程的 observability
bash uname -r
安装必要的软件包(以 Ubuntu 为例):
```bash
sudo apt update
sudo apt install -y bpfcc-tools linux-headers-$(uname -r) bpftrace
BCC(BPF Compiler Collection)提供了 Python 和 Lua 的前端库,适合快速开发;bpftrace 是更高层的脚本语言,适合临时分析。
开发环境配置
安装 LLVM 和 Clang 用于编译 eBPF 字节码:
sudo apt install -y clang llvm libbpf-dev
现代 eBPF 开发推荐使用 libbpf 库配合 CO-RE(一次编译到处运行)技术,但入门阶段我们先使用 BCC 降低门槛。
第一个 eBPF 可观测性程序:跟踪系统调用
我们将编写一个简单的 BCC 程序,统计每秒 execve 系统调用的次数。这能直观展示 eBPF 如何无侵入地监控内核行为。
代码实现
创建 execsnoop.py:
from bcc import BPF
# eBPF 程序:C 代码嵌入字符串
bpf_text = """
#include <uapi/linux/ptrace.h>
BPF_HASH(last, u64); // 哈希表用于缓存时间戳
BPF_PERF_OUTPUT(events); // 性能事件输出通道
int do_trace(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u64 key = 0;
char comm[16] = {};
bpf_get_current_comm(&comm, sizeof(comm));
events.perf_submit(ctx, &comm, sizeof(comm));
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_tracepoint(tp="syscalls:sys_enter_execve", fn_name="do_trace")
print("Tracing execve... Ctrl-C to end.")
def print_event(cpu, data, size):
comm = b["events"].event(data)
print(f"execve called by {comm.comm.decode()}")
b["events"].open_perf_buffer(print_event)
while True:
b.perf_buffer_poll()
运行与观察
sudo python3 execsnoop.py
打开另一个终端执行任意命令(如 ls),你将看到程序输出哪个进程调用了 execve。这个微型程序展示了 eBPF 的三个核心组件:Map(哈希表和性能输出)、挂载点(tracepoint)、数据处理(Python 端)。
深入 eBPF 映射(Maps):存储与聚合观测数据
Maps 是 eBPF 程序与用户空间之间共享数据的唯一机制。上面的例子使用了 BPF_PERF_OUTPUT 来实时流式传输事件,但当需要聚合统计时,应使用哈希或数组类型的 Map。
统计系统调用的发起用户
修改程序,使用 BPF_HASH 统计每个用户名发起的 execve 次数:
BPF_HASH(counts, u32, u64); // key=uid, value=count
int count_by_uid(struct pt_regs *ctx) {
u32 uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
u64 *count = counts.lookup(&uid);
u64 value = (count) ? *count + 1 : 1;
counts.update(&uid, &value);
return 0;
}
在 Python 端通过 b["counts"].items() 遍历 Map 并打印结果。Map 类型决定了观测数据的组织方式,如 BPF_HISTOGRAM 可用于记录延迟分布。
监控内核网络延迟:从请求到响应
网络延迟是分布式系统最关键的指标之一。eBPF 可以关联 TCP 请求与响应包,精确测量内核层级的耗时。
利用 kprobe 挂载内核函数
我们可以在 tcp_sendmsg(发送)和 inet_csk_accept 或 tcp_cleanup_rbuf(接收)处挂载探测点,通过记录包四元组和时间戳来计算延迟。
关键代码片段(BCC BPF 程序内):
struct conn_key {
u32 saddr; u16 sport; u32 daddr; u16 dport;
};
BPF_HASH(start, struct conn_key, u64);
int trace_send(struct pt_regs *ctx, struct sock *sk) {
// 从 sk 中提取地址信息构造 key,记录当前时间戳
...
return 0;
}
int trace_recv(struct pt_regs *ctx, struct sock *sk) {
// 查找 key,计算耗时,存入直方图
...
}
这种技术允许你精确区分网络延迟是由网络传输还是内核处理队列引起的。
高级场景:用户态与内核态联合观测
eBPF 的 uprobe 允许动态跟踪用户态进程。结合内核态的 tracepoint,可以构建端到端的请求追踪。例如,监控 Go 应用的 HTTP 处理函数,同时关联内核的 TCP 事件:
- 使用 uprobe 挂载
net/http.HandlerFunc.ServeHTTP。 - 在 eBPF 程序中提取请求 ID(可从上下文追踪)。
- 在内核
tcp_sendmsg中携带该 ID。 - 用户空间聚合器将两个事件关联,形成完整的延迟剖面。
BCC 的 USDT 支持进一步简化了与运行时探针的交互。
bpftrace:一键式内核观测
对于临时诊断,bpftrace 提供了媲美 DTrace 的便利性。一行命令即可统计系统调用耗时:
sudo bpftrace -e 'kprobe:__x64_sys_read { @start[tid] = nsecs; } kretprobe:__x64_sys_read /@start[tid]/ { @usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'