登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  业界新闻

eBPF 可观测工具怎样减少内核改动

来源:17golang原创

时间:2026-11-11 22:48:14 356浏览 收藏

我第一次把 eBPF 可观测工具接到一台不方便重编译内核的机器上时,最明显的变化不是多了一段内核代码,而是多了一个可以加载、验证、卸载的用户态观测程序。eBPF 的价值正在这里:把观测逻辑放在运行时加载的程序里,挂到内核已经提供的 tracepoint、kprobe 或 fentry/fexit 等位置,再通过 BPF Map 或 Ring Buffer 把摘要事件交给用户态。

官方资料:https://docs.ebpf.io/

eBPF 可观测工具减少的是“为采集数据而修改、编译、安装内核源码”的工作量,不是把内核接口变成永远稳定,也不是完全不需要内核能力。真正的判断标准是:观测目标是否已有合适挂载点,加载程序是否通过 verifier,以及目标内核是否提供所需的 BTF、权限和接口。
  • 源码改动少:观测程序由用户态 loader 运行时加载,不必为每个指标新增一套内核模块。
  • 数据路径清楚:内核侧只做必要的采集和聚合,用户态负责展示、存储和告警。
  • 兼容性有边界:CO-RE 可以降低结构体布局差异的适配成本,但挂载点和 kfunc 仍需要版本与能力判断。

先把“少改内核”拆成三个动作

传统做法往往是改内核源码、重新编译或安装模块,再把采集逻辑放进修改后的路径。eBPF 把这个过程拆开:用户态 Loader 负责加载 ELF 对象;内核 verifier 检查程序是否满足安全规则;通过检查后,程序附着到已有的内核事件或函数入口。Linux tracing 文档把 tracepoint、ftrace、kprobe 等列为内核已有的追踪框架,这些位置就是 eBPF 可以借力的“接口面”。

这里的“已有”很重要。eBPF 不是凭空创造事件,而是利用内核已经暴露的上下文和挂载点。观测需求如果能落到现有事件上,就可以把新增逻辑放在用户态可管理的 BPF 对象里;如果根本没有合适事件,仍可能需要内核侧增加 tracepoint 或调整现有实现。

用户态 Loader、eBPF 程序、内核挂载点和 BPF 数据通道的结构说明图
图1:eBPF 可观测层结构说明图,展示运行时挂载如何减少内核源码改动。

为什么观测逻辑可以放在内核源码之外

eBPF 程序不像普通内核模块那样拥有任意调用权限。它通常由用户态程序加载,经过 verifier 检查后才能运行;程序能访问什么上下文、能调用哪些 helper,也由程序类型决定。对观测工具来说,这个约束反而形成了清晰边界:内核侧负责在事件发生时读取少量字段、做必要过滤或聚合,用户态再把结果变成指标、日志或追踪数据。

BPF Map 是两侧的共享数据结构,Ring Buffer 则适合把事件记录送回用户态。下面的伪代码只展示数据职责,不绑定某个工具或内核版本。

struct event {
    __u32 pid;
    __u64 latency_ns;
};

// 中文注释:内核侧只提交必要字段,避免把大块上下文复制到用户态。
static __always_inline int record_event(struct event *e)
{
    // 中文注释:真实程序还需按挂载点和 verifier 允许的访问方式取值。
    return bpf_ringbuf_output(&events, e, sizeof(*e), 0);
}

// 中文注释:用户态负责读取事件、聚合指标并写入观测系统。
void consume_events(void) {
    // 中文注释:此处代表 libbpf 或其他 loader 的 Ring Buffer 消费循环。
}

这段结构带来的收益不是“所有工作都在内核外完成”,而是让内核侧新增逻辑保持小而受限。采样比例、字段映射、存储格式和告警规则可以在用户态迭代,通常不需要重新编译内核。

tracepoint、kprobe 和 fentry 应该怎么理解

选择挂载点时,我会先问“我需要的是稳定事件,还是某个具体函数的细节”。tracepoint 是内核代码中预先定义的事件接口,适合围绕明确事件采集;kprobe 可以探测许多内核函数位置,但内核官方文档和 BPF 设计问答都提醒,kprobe 的挂载位置属于内部实现细节,函数变化后可能需要调整。fentry/fexit 使用 BPF trampoline 进入函数入口或返回点,能提供函数参数和返回值等更直接的观测上下文。

目标优先考虑需要留意
围绕内核已定义事件采集Tracepoint事件字段与版本支持仍需对照目标内核
快速观察某个函数Kprobe/Kretprobe函数名、参数和挂载位置不是稳定 ABI
需要函数入口或返回值Fentry/Fexit依赖 BTF、内核能力与程序类型支持

因此,eBPF 减少的是“为获得观测点而改内核”的成本,不是让所有挂载点都拥有长期兼容承诺。文章标题中的“减少”必须和接口选择同时理解。

CO-RE 解决的是适配成本,不是所有兼容性

不同内核版本的结构体可能改字段、改布局,直接依赖固定内核头文件的 eBPF 程序容易因此失配。CO-RE 的思路是:编译产物携带 BTF 类型信息与重定位描述,libbpf 加载时读取运行中内核的 BTF,再把程序访问的类型和字段调整到当前内核。这样,一份 eBPF ELF 对象可以更容易适配多个内核版本。

运行中内核 BTF、CO-RE、libbpf 与 eBPF 接口稳定性边界的说明图
图2:BTF 与 CO-RE 兼容性说明图,强调少改内核源码不等于零适配。

但不能把 CO-RE 当成万能兼容层。内核 BPF 设计问答明确区分了不同接口:BPF 指令、helper 及参数属于 ABI;tracepoint 和 kprobe 挂载位置不是稳定 ABI;kfunc 也有自己的生命周期和兼容性预期。eBPF 文档同样建议对 kfunc 做防御式处理,不能默认它在所有内核上都存在。

把“减少改动”落成一张采用清单

  1. 先找现成事件:优先检查目标内核的 tracepoint、BTF 和可用程序类型,确认观测目标是否有直接入口。
  2. 再选数据边界:只在内核侧采集定位问题所需字段,把聚合、采样策略和存储交给用户态。
  3. 使用 CO-RE:通过 BTF 与 libbpf 处理结构体布局差异,避免为每台机器维护一份固定头文件版本。
  4. 记录降级路径:当 fentry、kfunc 或某个 tracepoint 不可用时,明确回退到更稳定事件、减少字段或关闭该观测项。
  5. 控制权限与开销:根据程序类型准备所需能力,关注采样频率、Map/Ring Buffer 容量和事件丢失策略。

如果只是要统计系统调用、网络连接或进程生命周期,成熟工具通常已经把这些判断封装好;如果要观察内部函数,则应把内核版本、BTF 完整性和挂载点稳定性纳入发布清单。

常见误区

  • 误区一:eBPF 等于修改内核。可观测程序通常是运行时加载的 BPF 对象,和直接改内核源码不是一回事。
  • 误区二:CO-RE 让程序永远兼容。它主要降低类型和字段布局的适配成本,不能替换对挂载点、kfunc 和权限的检查。
  • 误区三:kprobe 比 tracepoint 更通用就更适合生产。它能快速接近函数实现,但函数内部细节变化也会提高维护成本。
  • 误区四:内核侧采集越多越好。事件越大、频率越高,Ring Buffer 压力和观测开销越明显,应先做字段收敛和采样。

相关问题

eBPF 工具完全不需要重新编译内核吗?

通常不需要为了每个观测指标重新编译内核,但前提是目标内核已经提供所需挂载点、BTF 和程序能力。缺少事件或能力时,仍需改变观测方案,极端情况下要补充内核支持。

为什么不直接使用 kprobe?

kprobe 适合快速观察函数,但挂载位置和函数原型属于内部实现细节。对长期运行的工具,应优先考虑稳定事件,或用 CO-RE、能力探测和回退路径降低维护风险。

CO-RE 和 BTF 的关系是什么?

BTF 提供运行中内核的类型与布局信息,CO-RE 利用这些信息完成重定位,libbpf 则在加载阶段应用适配结果。三者共同降低跨内核版本部署成本。

最后可以把答案压缩成一句话:eBPF 可观测工具把新增观测逻辑从内核源码、编译和安装流程中抽离出来,借助已有挂载点、受限程序模型、BPF 数据结构和 CO-RE 完成运行时适配;但是否稳定,仍取决于挂载点、BTF、权限和接口生命周期。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>