最近我们团队开始进行一个新项目,需要基于 Google 的 Android 14 系统源码来定制 ROM。我们需要下载 AOSP(Android Open Source Project)的代码进行开发。AOSP 是一个提供了支持移动设备和嵌入式设备开发的完整技术栈,可以用来开发智能手机、平板电脑、车载系统、智能电视等各种设备。AOSP 项目源码规模很大,占用磁盘 124.44GB,超过 1400 仓库。
“大佬,可以打一个有最新代码的 ROM 么? 我测试一下新功能”
“可以,我去操作一下, 大概 1 个半小时后你再来拿”
“1个半小时这么久?”
“对啊, 拉代码就得 20 分钟, 还得跑测试和编译….”
团队在开发中,经常会出现类似的对话,我们发现团队在开发这种超大型仓库时,面临着多方面的问题和困难:
代码拉取耗时长:由于代码仓库很大 (125GB)导致拉代码很慢,通过 repo 下载代码需要接近 20 分钟。 存储空间占用大:团队开发很多,代码占据了每个开发电脑的 125GB 空间。 效率低:当前团队的 CI 系统,使用 repo 来管理多仓的 AOSP,容易在代码 clone 阶段就挂掉。并且流水线每次代码准备的时间都得 20 分钟,构建耗时长达 50 分钟,每次编译出包都超过 1 小时,加上其他跑测试的时间,总耗时就更长了。要是遇上高并发,编译效率更一言难尽 并发构建受限:机器上拉下来的代码,每次只能供一个流水线操作,强行并发会造成冲突和污染,多个流水线只能串行跑,效率极低,构建时间随着流水线增加而线性增加。
无法满足并行运行多个编译任务的需求 大部分时间机器处于闲置状态,资源利用率
浅 clone (shallow clone): 虽然可以减少下载的历史提交数量,但无法获取完整的代码历史,不利于代码追溯和版本管理,并且速度提升不明显。 稀疏 clone (sparse clone): 虽然 `repo` 工具的按仓库粒度选择性克隆功能在管理大量仓库时提供了灵活性,但也带来了复杂性、依赖管理、版本一致性等方面的挑战。在使用这种方法时,团队需要充分考虑这些缺点,并制定相应的策略来管理和协调开发工作。 部分 clone (partial clone): 虽然可以只下载文件元数据,但在需要实际文件内容时仍需额外下载,可能导致意外的延迟。
主要影响我们开发的核心 2 大矛盾点:
代码 clone 速度太慢 高并发下无法复用代码缓存复用,容易造成代码冲突和代码污染
解法出现
把代码仓库从使用 repo 管理,迁移到单仓 monorepo 来管理,用平台自动化脚本同步了历史提交记录,然后把代码仓库 push 到 CNB 的 git 仓库上 用 ubuntu 18.04 作为基础镜像,准备一个可以用来构建 AOSP 的 Dockerfile,作为构建容器。 CNB 的配置是声明式的,主打"Everything as Code",通过配置文件来描述流水线,非常简单清晰,与 Git 代码仓库同源管理
拉取代码:16m 52s, 124.44GB 构建耗时:46m 4s
真的 秒级 clone , 3.8s !!!124.44GB !!!
且并发构建依然可以命中缓存!
拉取代码:3.8s , 124.44GB
构建耗时:1m 30s
下面截图就展示了并发 6 条流水线时的真实耗时:
性能对比
代码准备 | 编译 | |
未使用 CNB,无缓存加速 | ||
使用 CNB,缓存加速 | 3.8s | 1m30s |
增加锁机制,串行化流水线,同时只能有一个流水线可以运行,其他流水线需要排队等待。这种方式虽然可以解决问题,但会显著增加流水线的运行时间,降低系统的吞吐量,硬件资源利用率低,用户等待时间长。 把代码缓存放到分布式存储中,这样可以避免代码被污染,如 bazel 的 remote cache。但是这种方式需要额外的成本:维护分布式存储,网络传输成本与耗时,数据一致性等问题,并且不能替代所有的场景。
file1 是没修改的,直接从 lowerdir 透明地呈现在 merged 层。 file2 在 upperdir 中被修改。当读取 file2 时,会看到 upperdir 中的修改版本。 file3 在 upperdir 中被删除。虽然 lowerdir 中仍然存在 file3(在 upperdir 中它被标记为 "Whiteout" 状态),但在 merged 层中它是不可见的。 file4 是新增的文件,直接存在于 upperdir 中。
git 代码缓存初始化:如果是首次拉代码,需要运行 git init 和 git fetch 进行代码的拉取,如果是已存在 .git 目录,则直接使用 git fetch 对 .git 进行更新 workspace 代码准备,使用 OverlayFS 对 .git缓存 进行复制,瞬间创建副本。通过 mount -t overlay 的方式,把代码缓存挂载到 lowerdir,并且建立一个空的 upperdir,然后 merged 就是最终在流水线看到的 workspace 的文件视图。 在 merged 文件夹上进行 checkout、build 等操作,基于 CoW 的特性,可以实现当需要写入时,从 lowerdir 的 cache 上 copy 出来再写入,如果只需要读,直接从 lowerdir 读取,这样就能保证在并发场景下的代码隔离。 构建结构后,删除副本。
时间复杂度:O(1),无视仓库大小,并发安全。 空间复杂度:O(1),存储空间不随并发数增长。
当然这里只是 CNB 对克隆时间的加速,如何将 AOSP 的编译时间从46分钟显著缩短至仅1分钟?后面我们再深入探讨 CNB 如何通过使用 CoW 和 volume 实现编译缓存的具体实现。
CNB 系统通过创新的 git-clone-yyds 插件和 Copy-on-Write (CoW) 机制,不仅解决了大型代码仓库的克隆速度和并发构建问题,还为我们开启了更多可能性。