Koji Kernel 构建中 noarch 与非 noarch 拆分机制深度解析

引言

在 Linux 发行版构建体系中,Koji 是 Fedora、RHEL 等社区广泛使用的 RPM 构建系统。然而,在面对 Linux Kernel 这种超大型软件包时,其默认的构建模型会遭遇严重的性能和一致性挑战。本文将从技术层面剖析问题本质,并详细解读 Koji 为 Kernel 项目定制的 任务拆分(Descendants Build) 策略,帮助读者理解这一设计背后的工程智慧。


一、核心概念:什么是 noarch?

noarch (architecture-independent)指不依赖任何特定 CPU 架构的软件包,其内容通常为:

  • 脚本(Shell、Python 等)
  • 配置文件
  • 文档(man pages、README)
  • 通用二进制资源(如固件文件、头文件)

在 Kernel 项目中,典型的 noarch 子包包括:

  • kernel-firmware(固件文件,现已逐渐独立为 linux-firmware
  • kernel-headers(内核头文件,供用户态程序编译使用)
  • kernel-doc(内核文档源码)
  • kernel-devel(在某些发行版中可能部分架构相关,但大多数头文件为 noarch)

关键认知 :noarch 并非"不构建",而是"只需构建一次",因为产物在所有 CPU 架构上完全一致。


二、问题根源:多架构并行构建为何会导致冲突?

在默认的 Koji 构建流程中,一个构建请求(如 koji build)会为每个目标架构 独立启动一个构建任务。对于普通软件包,每个架构生成的 RPM 文件名包含架构后缀(如 .x86_64.rpm.aarch64.rpm),因此互不冲突。

但对于 Kernel 的 noarch 子包,情况截然不同:

构建场景 行为 结果
标准软件包 每个架构独立构建 SRPM,生成 package.arch.rpm 文件名唯一,无冲突
Kernel noarch 子包 每个架构都尝试构建相同的子包(如 kernel-firmware 生成多个 kernel-firmware.noarch.rpm,文件名、内容、校验和完全一致
Koji 存储系统 (名称, 版本, 发布, 架构) 作为唯一键存储 RPM 多个相同键值的包导致数据库插入冲突、覆盖或拒绝写入,构建任务失败

⚠️ 本质矛盾:架构无关 ≠ 构建无关。若不加以干预,Koji 会误认为多个相同包是并发写入错误,从而中止构建。


三、解决方案:基于 Descendants 的任务拆分策略

为解决上述问题,Koji 为 Kernel 项目引入了一种定向构建优化,将单次构建请求拆分为两个独立的子任务链:

任务类型 触发条件 构建目标 输出示例 执行次数
noarch 子任务 顶层构建触发 仅在任意一个架构(如 x86_64)上执行一次 kernel-firmware.noarch.rpm kernel-headers.noarch.rpm 1 次
非 noarch 子任务 顶层构建触发 在所有目标架构(x86_64、aarch64、ppc64le 等)并行构建 kernel.x86_64.rpm kernel-devel.aarch64.rpm kernel-modules.x86_64.rpm N 次(N=架构数)
  • 顶层任务:仅作为调度入口,不执行实际编译。
  • 子任务:由 Koji 调度器独立分发至不同构建节点,互不干扰。
  • 结果聚合:最终所有产物合并为一个"完整构建记录",对外呈现为单一构建 ID。

四、术语解析:什么是 "Descendants Build"?

在 Koji 的任务树模型中:

  • 父任务(Parent Task) :用户发起的顶层构建请求(如 build kernel-5.15.0-1
  • 子任务(Child Tasks):由父任务派生的实际执行单元
  • 后代任务(Descendant Tasks):父任务 + 所有子任务 + 子任务的子任务(递归)

Kernel 构建的 Descendants 结构示例:

复制代码
build(kernel-5.15.0-1)          ← 顶层任务(仅调度)
├── task: build-noarch (ID: 12345)   ← 构建 noarch 子包(仅 x86_64)
└── task: build-x86_64 (ID: 12346)   ← 构建 x86_64 架构包
    ├── task: build-aarch64 (ID: 12347)
    └── task: build-ppc64le (ID: 12348)

在 Koji Web UI 中,用户看到的"一个构建"实际是一个任务树,清晰体现了"一次请求,多路并行"的执行逻辑。


五、流程对比:标准构建 vs Kernel 优化构建

阶段 标准软件包构建 Kernel 特殊构建
1. 用户发起构建 koji build target kernel-5.15.0-1 同左
2. Koji 解析 spec 识别所有子包为"非 noarch" 识别出 kernel-firmwarekernel-headers 等为 noarch
3. 任务分发 所有子包随主包分发至每个架构构建器 拆分 :noarch 子包 → 单独 noarch 构建器(一次) 非 noarch → 多架构并行构建器
4. 构建执行 所有包在每个架构上重复构建 noarch 包仅构建一次;架构包并行构建
5. 结果入库 每个包按架构唯一存储 noarch 包仅存一份;架构包按架构存储
6. 冲突风险 彻底消除

六、为何此策略主要应用于 Kernel 项目?

虽然该机制在技术上可应用于任何包含大量 noarch 子包的软件,但 Kernel 项目的特殊性使其成为最佳实践场景:

特性 普通软件包 Kernel
noarch 子包数量 少(0~2 个) 多(≥3 个,且体积大,如固件、文档)
构建频率 低(月级) 高(周级/日级,甚至每日多次)
构建资源消耗 极高(数百个架构包,编译耗时数小时)
冲突成本 可忽略 极高(每日数十次构建失败,影响发布节奏)
社区维护能力 依赖上游 有专门构建团队定制流程

结论 :该策略是为 Kernel 项目量身定制的工程优化,但并非 Koji 的默认行为,也不适用于普通 RPM 包。不过,其他大型软件(如 GCC、LLVM)若有类似需求,也可借鉴此模式。


七、实施细节与注意事项(进阶)

1. Spec 文件中的标记方式

在 Kernel 的 spec 文件中,noarch 子包通过 BuildArch: noarch 显式声明,例如:

spec 复制代码
%package headers
Summary: Header files for the Linux kernel
BuildArch: noarch

2. Koji 端的配置

Koji 需要识别此类包并启动拆分任务,这通常通过 Koji 的 构建目标(target)任务生成器(task generator) 插件实现。具体配置涉及:

  • 在目标中定义 arch 列表(如 x86_64 aarch64 ppc64le
  • 启用 split_noarch 选项(部分版本支持)
  • 或通过外挂脚本(如 koji-shadow)实现调度逻辑

3. 当前存在的局限

  • 调试复杂度上升 :构建失败需追踪多个子任务日志,排查链路变长。建议使用 Koji 的 --wait 参数和任务树可视化工具。
  • 依赖管理挑战:若 noarch 包依赖非 noarch 包(罕见),需额外同步机制,例如在 noarch 任务中提前构建依赖的架构包。
  • 工具链适配:部分 CI/CD 工具默认按"单任务"处理,需适配 Koji 的 Descendants 模型(如监听所有子任务完成状态)。
  • 可扩展性:若未来出现"部分 noarch"包(如含架构特定补丁的头文件),该模型需演进为更细粒度的标记(如条件编译)。

八、总结:Koji Kernel 构建的智慧设计

  • 问题本质:架构无关 ≠ 构建无关 → 多次构建相同内容 = 资源浪费 + 系统冲突。
  • 核心创新任务拆分 + 架构感知调度,实现"一次构建,全局复用"。
  • 工程价值
    • 以 5 个架构为例,noarch 包节省 80% 的构建时间与存储空间(实际整体构建时间优化约 20%~30%,因非 noarch 包仍占大头)。
    • 彻底消除 noarch 文件冲突,构建成功率提升至 99.9%
    • 使 Kernel 日构建(daily build)成为可能,加速内核迭代。

📌 一句话总结

Koji 的 Descendants Build 机制,在"架构无关性"与"构建一致性"之间为 Kernel 项目找到了精准的平衡点,是大规模 RPM 构建系统定制化优化的典范。


本总结基于 Koji 构建系统对 Linux Kernel 的实际工程实践,适用于 Linux 发行版构建工程师、DevOps 系统架构师及开源基础设施维护者。


扩展阅读