7.1 BTF与一次编译到处跑


7.1 BTF 与一次编译到处跑

本节摘要:BTF 是内嵌的类型信息,描述结构体字段与函数原型。CO-RE 在加载时用目标内核的 BTF 修正偏移,让同一份对象在字段挪动后仍能读对。它修不了字段被删、语义被改、Helper 消失。没有 BTF 的老内核,需要退回按版本构建或放弃深字段。

读前必看

阅读完本节,你应当能够:

  1. 用“类型信息 + 加载时重定位”解释 CO-RE
  2. 列出 CO-RE 做不到的三类变化
  3. 设计内核版本矩阵:哪些字段必测
  4. 在无 BTF 环境选择降级策略

第 1.2 节说模块被内核版本绑死,是因为偏移写死。eBPF 若把 task_struct 里某字段按开发机偏移写死,下场相同。BTF 把类型搬进内核和对象文件,加载器才能问:“这个字段在目标内核里现在排第几。”

一、BTF 是说明书,不是翻译机

内核镜像可以带一份 BTF,描述当前构建里的类型。你的程序对象也可以带 BTF,描述你访问了哪些字段。加载器对比两边,改写指令里的偏移和部分重定位。成功之后,验证器看到的是已经对准的字节码。

没有说明书,加载器只能猜。猜就是写死偏移,猜错就是读到隔壁字段,表现为“PID 像内存垃圾”。所以“一次编译到处跑”的前提是:到处都有 BTF。发行版裁掉 BTF 以缩小镜像时,你的到处就缩小了。

建筑图纸换了承重墙位置,传感器支架要按新图纸打孔。CO-RE 就是带新图纸打孔。图纸若把承重墙删了,支架再智能也没地方打——那是语义变更,要改程序。

二、它修什么,修不了什么

能修: 字段在结构体里挪动、大小变化但仍兼容、不同内核配置导致的布局差异(在 BTF 仍描述该字段时)。

修不了:

  • 字段改名或删除
  • 语义变了:同样叫这个名字,单位或生命周期变了
  • Helper 编号或行为变了
  • 程序类型在该内核不存在
  • 你访问的是未导出、未进 BTF 的私有细节

因此矩阵测试仍要做。CO-RE 减少的是“为每个小版本编一份”,不是取消测试。CI 里至少覆盖你们生产在用的长期支持内核各一条:加载、挂载、读一个已知字段、对一下用户态看到的 PID 是否像 PID。

变化 CO-RE 你要做的
字段偏移挪动 通常能修 仍要抽测
配置导致布局不同 常能修 用真实发行版内核测,不要只测上游
字段删除 不能 代码里可选字段,失败则降级
Helper 消失 不能 能力探测
跟踪点改名 不能 挂载点矩阵
无 BTF 不能 按版本构建或放弃该字段

⚠️ 常见坑:在开发机打开所有调试选项生成的 BTF 上测试通过,就声称支持某发行版。发行版配置不同,字段可能根本不存在。
💡 关键直觉:CO-RE 对准的是名字与类型,不是你对内核源码的记忆。源码读得再熟,也要以目标 BTF 为准。

三、工程:可选字段与降级

深字段访问写成可选:重定位失败时程序仍能加载,只是少一项观测。比“要么全有要么加载失败”更适合混合集群。

/* 概念性:字段不存在则跳过,不让整个程序死 */ if (field_exists_comm) bpf_core_read(&comm, sizeof(comm), &task->comm); else comm[0] = 0;

无 BTF 的老节点:明确不支持,而不是静默读错。对外的能力列表写“需要内核 BTF”,监控里对加载失败分类:验证拒绝 / 无 BTF / 额度不足,三类混成一个错误计数会让人改错地方。

用户态同样受益:有了 BTF,调试工具能显示 Map 值的字段名。这不改变运行,但缩短第 8 章的排障。

图:加载时对准,而不是编译时赌偏移

图:加载时对准,而不是编译时赌偏移

问题:Windows 或非 Linux 内核也能 CO-RE 吗?

那是另一套运行时与类型源。不要假设 Linux 对象文件能对上去。跨操作系统目前不是“换个 BTF”这么简单,见 7.3。

问题:自己生成 vmlinux 头文件是否还要?

生成头是为了在编译期有类型可写。真正跨版本靠 BTF 重定位。只拷一份头、关 CO-RE,等于又写死了。

四、混合集群里的能力矩阵怎么画

生产很少只有一种内核。可能有 5.10 的老池和 6.1 的新池。能力矩阵以行为程序功能、列为内核档:必有、可选、无。必有功能在任一档加载失败则该档节点不能调度开这个工作负载,或守护进程在该档关闭自己。可选功能失败只打点,不退出。无的功能不要在日志里刷错误,以免把真失败淹没。

画矩阵时用真实发行版包,不要用自己编译的“差不多版本”。配置项不同,BTF 里可能没有你要的字段。云厂商内核更是如此。每一档至少一台长期存在的测试节点,CI 通过 SSH 或远程加载接口跑冒烟:加载、挂载、读一个已知 pid 字段、卸载。冒烟失败阻断发布。

文档对外只承诺矩阵里的“必有”。工程师在群里说“我这台 6.6 能用新 Helper”,不能当成产品承诺。新 Helper 先进可选列,覆盖率够了再升必有。降级路径要先于新功能存在:没有 BTF 就只提供计数,不提供深字段。用户看到的是功能差一档,而不是守护进程崩溃。

BTF 本身也会占空间,裁剪镜像的人会动它。镜像构建检查应包含“内核 BTF 存在”这一项,和存在某个驱动同等重要。缺 BTF 的镜像不要进生产池。发现生产节点被裁掉 BTF,按事故处理,而不是让每个 eBPF 程序自己猜偏移。

调试时把目标内核 BTF 和对象 BTF 的字段对比做成工具输出。加载失败若是字段缺失,日志应直接说字段名,而不是只说 reloc 失败。人能读的失败,才能在混合集群里快速决定是升级节点还是降级功能。

现场笔记:裁掉 BTF 的精简镜像

镜像团队为了缩小体积裁掉 BTF。eBPF 守护开始读到垃圾字段,PID 不像 PID。当时怀疑业务,折腾半天。后来镜像构建把 BTF 存在性当成和网卡驱动一样的检查。缺 BTF 的镜像不许进生产池。裁剪要有清单,清单上 eBPF 依赖必须在。

混合集群里新池用上了可选深字段,老池没有。守护进程在老池崩溃退出,因为把可选当成必有。改成探测失败只关功能、不退出,调度才恢复。退出是最差的降级。降级应是少看见,不是看不见守护进程。

对外承诺写成矩阵表,而不是“支持现代 Linux”。客户问能不能上某发行版,拿表回答。表上没有的,就是不能,直到 CI 绿。群里某台机器能跑,不是承诺。

延伸讨论:存在的定义是 CI 绿

能力矩阵以真实发行版为准。新 Helper 先可选后必有。可选失败不退出。退出是最差降级。降级应是少看见。镜像构建检查 BTF 存在,缺了不许进池。裁剪体积若裁到类型说明书,字段会变成垃圾,排障会先骂业务。对外承诺就是矩阵表,表上没有就是不能。群里某台 6.6 能跑不是承诺。加载失败分类:无 BTF、验证、额度,分开计数。混成一个错误会让人改错地方。调试输出要能说出缺失字段名。人能读的失败,在混合集群里才能快速决定升级节点还是降级功能。快速决定比快速加载重要。加载到错误档上的深字段,是在用垃圾 PID 做产品决策。决策比加载失败更贵。所以失败要响,垃圾要禁。禁的手段是探测分支默认关。默认开是在赌发行版。赌输的是用户。

对照清单

  1. CO-RE 修布局不修语义,字段删了或 Helper 没了要对准也救不了。
  2. 到处跑的前提是到处都有 BTF,裁掉 BTF 的精简镜像会让字段变成垃圾。
  3. 矩阵用真实发行版包,不要用自己编译的差不多版本。
  4. 深字段写成可选,失败则降级,不要全有或全无导致守护退出。
  5. 失败分类:无 BTF、验证、额度,分开计数以免改错地方。
  6. 新 Helper 先可选后必有,可选失败只打点不退出。
  7. 对外承诺就是矩阵表,群里某台新内核能跑不是承诺。
  8. 存在的定义是 CI 绿、有所有者、有预算,博客不是存在。
  9. 生成头文件是为了编译期有类型,只拷头关 CO-RE 等于又写死偏移。
  10. 跨操作系统不是换个 BTF 那么简单,挂钩集合往往不像。
  11. 镜像构建把 BTF 存在性当成和驱动一样的检查。
  12. 人能读的 reloc 失败应指出字段名,才能决定升级节点还是降级功能。

一次编译到处跑是有边界的句子。边界是 BTF 还在、字段还在、语义没变、Helper 还在。四条里坏一条,句子就要改成一次编译、多处探测、失败降级。降级写进加载器,比写进事后解释体面。体面的意思是用户看到功能差一档,而不是看到守护进程消失。消失会被当成全面故障。全面故障会触发全面回滚。全面回滚可能把仍然能工作的计数功能也撤掉。撤掉计数,缺口表又空了。空了的晚上,你会后悔没有把可选做成可选。可选是工程,不是谦虚。

\n\n## 课堂补充\n\n四处跑有四条边界。缺一条就改成探测降级。守护退出是最差降级。矩阵用真发行版。可选失败只打点。承诺是表不是群消息。CI绿才叫存在。关CO-RE等于写死。跨OS不是换说明书。镜像检查BTF。失败要说出字段名。垃圾PID比加载失败更贵。\n\n\n\n## 生产验收条\n\n1. 围绕「BTF与一次编译到处跑」,生产验收只认能关掉、能计数、能对账,不认口头保证。\n2. 围绕「BTF与一次编译到处跑」,把所有者、版本、卸载方式写成清单三件套,缺一视为幽灵。\n3. 围绕「BTF与一次编译到处跑」,对照实验必须能回答开关前后业务指标动了没有。\n4. 围绕「BTF与一次编译到处跑」,失败要分类到验证、额度、挂载、未触发、丢失,禁止只丢一句笼统错误。\n5. 围绕「BTF与一次编译到处跑」,热路径默认克制,阈值之后才出栈,出栈之前先证明钩子活着。\n6. 围绕「BTF与一次编译到处跑」,和平台已有挂钩点冲突时先登记再加载,禁止手工抢挂。\n7. 围绕「BTF与一次编译到处跑」,内核版本矩阵没跑绿就不能把功能写成必成功路径。\n8. 围绕「BTF与一次编译到处跑」,回滚必须碰到内核对象,心跳停止才算撤回成功。\n\n## 要点串联

  • BTF 是类型说明书,CO-RE 在加载时按说明书改偏移。
  • 前提是目标内核有 BTF;裁掉就没有“到处跑”。
  • 修布局不修语义;删除与 Helper 变化要降级分支。
  • 矩阵测试仍必要,测真实发行版配置。
  • 可选字段优于全有或全无。
  • 失败分类:无 BTF、验证、额度,分开计数。

下一节在已经能加载的前提下,处理程序太大、循环太复杂:尾调用、子程序、有界循环。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U