作者 | 赵振
Linux 发行版的自主维护工作一直面临着巨大的挑战,软件包规模巨大,涉及多个领域,要进行有效的自主维护,对人力、能力都有极高的要求。本文根据腾讯工程师、OpenCloudOS 社区技术专家赵振在 2024 年第十一届开源操作系统年度会议(OS2ATC)上的分享整理,重点探讨为打造全链路自研操作系统,如何实现对 3000+ 大规模软件包的全链路自主研发与自主维护。
一、整体介绍
一个 Linux 发行版标准镜像包含 3000 余个软件包,具体涉及库、工具、服务、语言运行时、图形、音视频等各个方面,再加上各种场景应用,比如云原生、数据库、AI 等,涉及软件包数以万计,如何维护如此大规模的软件包,对团队的人力、人员能力都是巨大的挑战。
操作系统团队在对软件包分类、分层,按照领域分类、重要性等进行差异化维护之外,更构建了一套全流程自动化的基础设施和工具平台,以提升维护效率和质量,让软件包的维护者有更多的精力投入到重要包的掌握和能力建设中。
该工具平台从上游跟踪到代码同步,各个流程环节尽可能自动化,主要包括以下 5 个部分及对应的工具。
第一个工具 rpm-upgrade 用来跟踪上游社区的发布情况,包括获取新版本的 changelog 了解社区的动态。第二个工具 rpm-tracker 用来跟踪重要包的 commits,扒取 bugfix、cve 相关的 patch。通过这两个工具可以及时获取上游最新的动态、修复,按需同步到自主维护的版本,软件包维护者就不用人肉跟踪上游社区;获取到上游的更新、修复后,会尝试自动提交 PR。
对于提交成功的PR,会通过第三个工具 rpm-check 进行变更识别和兼容性检查,如果发现兼容性变化,会自动通过第四个工具 rpm-dep 来查找受影响的软件包来进行重编、执行受影响的包的用例,最后通过第五个工具 rpm-sync 来同步到其他分支。
以上是软件包维护全过程,通过 CI 串联起来实现全流程自动化。
二、具体实现
1.rpm-upgrade:上游新 Release 跟踪查询
问题:软件包的上游社区形式多样,有 Git、svn、hg 等不同的协议,github/gitlab、pypi、metacpan、Sourceforge 等不同接口,且软件包最新的 Release 发布时间不定,有的发布频繁,有的长期无新 Release,不同系列的软件包选型的间隔也存在差异,如果无差异地对所有包执行升级查询,浪费资源和人力。
解决方案:团队设计的 rpm-upgrade 工具,可通过多种查询接口,覆盖不同类型的上游平台;通过综合发布时间、频率、选型时间、选型间隔内的版本数等,多维度评估该版本是否需要自动升级。
效果:当前主流平台 Git/svn/pypi/perl 等都已覆盖,3200+ 软件包中的 98.5% 都能实现自动化查询升级,基本不再需要人工跟踪上游。
2.rpm-upgrade:上游新 Release 自动升级
问题:上游新 Release 查询到后,符合条件的软件包就可以进行自动升级。但在升级过程中,还存在多 source 源,tarball 需自行生成、上游未提供完整 tarball,补丁冲突等复杂情况导致自动化困难。
解决方案:工具先修改version、Release、chaneglog,再根据修改后的内容获取源码包。如果是多源码包情况,则根据宏解析自动下载;如果是 tarball 无法直接获取等情况,则支持维护者自定义脚本处理。
对于升级过程中的补丁冲突,则根据补丁来源和补丁编号规则处理。上游补丁冲突时,视为上游已合入,移除该补丁并记录; 发行版补丁冲突时,报警提示并记录,由 maintainer 介入分析。
效果:升级软件包平均节省 10 分钟以上,并且自动解决补丁冲突可达 85% 以上,软件包升级效率提升 80%+。
3.rpm-tracker:上游 commits 跟踪扒取
问题:软件分支、commit 信息多,传统 git clone 方式耗时长;主要关注 bugfix、cve,需要对 commtis 进行分类,人工费力、关键词匹配方式不准确。
解决方案:rpm-tracker工具通过 Python 的 GraphQL API 爬取上游 commits 信息,支持 GitHub、GitLab 等主流平台;并且选择专门基于代码训练的大模型,结合微调,对 commits 进行分类,把 bug 修复、cve 修复等类型的 commits 识别出来,backport 到代码中。爬取后自动提交 PR。
效果:当前工具可以一次性查询上百个软件,模型在普通台式机上无量化的情况下可以稳定运行,输出结果准确率 80% 以上,大大减少人工分析、回合的工作量。
4.rpm-check:兼容性检查,软件包变更的守门人
问题:上游新 Release、commits 扒取回合,提交 PR、完成编译后,进行兼容性检查。当前业界已有的兼容性检查开源工具主要是对 C/C++ 程序、Java 程序进行检查,同时存在需要人工指定包以及库、无法处理库中部分特殊字符、无法判断符号是否对外、结果可读性差、速度较慢等情况。
解决方案:rpm-check 在 abicc 社区工具的基础上解决了上述几个问题,同时基于Python AST 模块自研了 Python 兼容性检查工具。
工具扫描目录下所有 rpm 包及其 debuginfo 包、devel 包进行匹配成对后以筛选出需要检查的 rpm 包,然后通过多进程方式对每对包进行处理,同时会在文件粒度上也进行并发操作,最大限度缩短检查时间。
检查项包括几个方面:
- 子包列表:检查子包是否有增删
- rpm 的能力:(requires/provides/..),判断是否有能力发生变化
- 文件列表:检查重点位置的文件是否有增删,同时排除无关信息(如版本号)以及无影响文件
- 动态库的 ABI/API:根据代码变化定位影响的结构体、函数等
- 二进制可执行程序比较:比较软件包中存在的可执行文件(工具、脚本等)的选项、参数是否发生变化
- 配置文件:支持多种配置文件格式,精确找到具体的变化项
多进程检查结束后,会对检查结果进行分析评估:比如 ABI/API 的变化,会先经过内外部符号判定,判断该变化为内部变化还是外部变化,然后经过评估算法确定其影响等级,影响等级用来判断该次变化的严重程度以及是否需要进一步判断其影响范围。
最后根据评估结果等级确认影响范围,这一步通过在依赖包中进行符号搜索来完成,同时,搜索也会通过本地缓存进行加速。经过以上检查和精确查找,得到此次的变化影响的符号、软件包以及受到影响的库。
效果:支持 C、C++、Python、Java 等主流语言,特殊场景基本覆盖;从符号粒度确认影响范围,精确度 90% 以上;包及文件粒度并发,本地缓存缩短检查以及符号搜索耗时 50% 以上。
5.rpm-dep: 查询包依赖与排序
问题:受兼容性变化影响的包,通过 rpm-dep 工具获取。当前 DNF 工具无法快速获取发生变化的包所影响到的包,包括依赖当前变化的包进行编译的包,依赖当前变化的包进行安装的包,即反向依赖。
同时,获取到反向依赖包列表后,列表中的包之间也存在层级关系,进行构建时,需要先构建底层的,后处理高层级的,这就需要排序。
解决方案:rpm-dep 工具初始化时,解析 repo 源的 repodata 文件,构造出依赖关系表,将依赖关系表存入 Redis,提高查询速度。
- 依赖查询时,通过 key - value 查询,以及 bfs 多层的检索,即可获取指定层数的依赖关系树。
- 依赖排序时,首先建立包的依赖图,对于存在循环依赖的情况,会统计循环链上的所有包的被引用情况,从被引用最少的节点拆开循环链条。
然后得到一个有向无环图,接下来使用拓扑排序的思想,每一轮循环都取出无前向依赖的节点,即可对同层的 RPM 包排出优先级。
效果:多种依赖场景秒级查询多层依赖树;包排序指导按依赖层级进行构建。
6.自动 Release+1 重编受影响的包
需求:兼容性变化,需要准确无误的重编受影响的软件包,根据影响和风险的不同,分为测试重编和正式重编,测试重编不 Release+1 提交 PR、只用来验证变化会不会导致问题,如 API 头文件变化,正式重编是指要 Release+1 提交 PR,如 soname 变化会导致找不到依赖,就要正式重编。测试重编、正式重编,都要保障编译源依赖的是变化后的包,不能基于老包编译。
解决:为了避免遗漏或者范围过广出现无效重编,我们根据兼容性变化的具体内容和影响范围,确定重编类型,如表格所示,然后使用 rpm-dep 工具找出受影响的依赖包。
- 对于测试重编,我们将 PR 编译通过的软件包结合当前编译源,制作成临时编译源;在这个临时编译源的基础上,我们对受影响的包进行测试重编。
- 对于正式重编,创建 issue 进行跟踪,按照依赖关系层级排序,自动依次发起正式重编 PR,上一层级的 PR 编译成功、进入编译源后,提交下一层级的 PR,直到所有需要 Release+1 重编的包都完成编译。
效果:重编精准,无遗漏无冗余 Release+1;按依赖层级编译、构造编译源,编译依赖新包;人工只需确认,效率提升 100%。
7.精准全面测试+快速高质量发布
需求:PR 编译通过后,要对软件包进行更新发布,既要保证软件包更新的及时性,也要保证软件包更新的质量。
解决方案:我们通过消息机制保证编译完成的软件包能够及时更新至测试平台,立即获取更新后的软件包到测试 YUM 源,然后进行升级测试、安装测试、服务启停测试、功能测试,并根据前面提到的 rpm-dep 工具给出的受影响包列表执行受影响包的测试,以此来做到精准测试,覆盖全面,同时高效地得到质量反馈。
此外,为了防止出错软件包阻塞其他通过测试的软件包的正常发布流程,对于测试未通过的软件包,会以单个软件包的粒度回退,清理对应软件包及其重编包,并发起问题处理流程。然后继续进行其他保证其他正常软件包的及时发布。
效果:小时级软件更新速度(代码提交到更新发布);软件升级冲突、功能等问题有效拦截。
8.rpm-sync:分支间高效同步
PR 合入后,后台会根据 PR 填入的 commit 模板信息,进行分类。识别到是 bugfix, 安全修复,后台会自动调整相关 commit,向下游分支发起同步。如果同步失败,会自动创建工单通知相关人员对同步的 PR 进行分析调整。
效果:多分支及时保持同步;流程自动化,形成完备的错误处理机制;减少人工同步、多分支维护的工作量。
9.整体数据流设计
问题:通过前面的介绍,可以看到,整个自动化流程涉及了上游源码社区的更新监控,代码托管平台的管理、下游编译平台、分发平台以及测试平台多个流程的不同组件,平台、流程、环节多,交互复杂,包括事件通知、状态等待、结果回传等。
解决方案:基于这个问题,我们设计了一整套消息机制,通过消息队列解耦不同平台的数据依赖问题,并通过 CI 平台的流水线驱动不同任务运行。
比如,当我们监听到上游社区更新了新版本,这个消息会写入消息队列,等待对应的代码同步流水线处理更新。再将同步完成的消息写入消息队列,等待后续的编译、测试、同步流水线的批次处理。
这套消息处理机制,解耦了不同流程间的依赖,仅通过统一的消息来完成整个流程的执行。并且因为消息保存在消息队列中,下游流程不依赖上游数据的实时更新,对于执行失败的下游任务,我们可以重新从队列中取得对应的消息,然后从执行失败点继续完成后续工作。
此外,消息队列可以驱动流程运行,但它没有持久存储的能力,没法记录并追踪某个更新的软件当前状态(除非我们遍历消息队列),因此,我们通过数据库来记录某个软件包当前处在哪个流程中,来保证每个软件包的可追踪性。
效果:提升平台开发和运行效率 50%+。
三、未来展望
以上通过自动化基础设施、工具平台的加持,软件包自主维护效率得到了成倍的提升,质量也得到一定保障,不过距离自主维护能力的成熟还有较大的差距。当前部分重要包已经掌握了代码架构、重要功能的实现,但还有较多的软件包需要逐步积累维护能力;自主可控版本的软硬件生态与业界标杆也有差距,随着软硬件生态的推进,版本的兼容、稳定也会面临一些挑战。
项目在 systemd、glibc、gcc 等关键用户态软件包社区已经有一些贡献,获得了 numactl 等社区的 maintainer,不过上游社区参与度远远不够,声量及影响力仍然很弱。
当前 AI、异构等新场景不断涌现,开发者、管理员、企业等需求日新月异,传统的 OS 也需要与时俱进、不断创新,才能保持生命力。千里之行始于足下,自主维护能力建设已经迈出了坚实的一步,剩下的就是脚踏实地,一步一个脚印,把自主可控做实做深。