125G 代码,10 秒内准备完成:CNB 刷新 AOSP 克隆效率

最近我们团队开始进行一个新项目,需要基于 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 代码的耗时问题,团队还购置了一台本地的高性能机器,专门服务于代码编译。每次有开发同学需要编译的时候,就去该机器上增量拉代码,然后编译,这样可以尽可能使用机器上的代码缓存,然而,这种方法仍存在局限性:
  • 无法满足并行运行多个编译任务的需求
  • 大部分时间机器处于闲置状态,资源利用率

面对这些持续存在的挑战,我们意识到需要从根本上解决代码clone的速度问题。这不仅关系到开发效率,还直接影响着我们的 CI/CD 流程和资源利用。因此,我们开始探索各种可能的优化方案,希望能找到一个既能显著提升 clone 速度,又能满足我们复杂开发环境需求的解决方案。

我们首先研究了几种常见的代码 clone 优化技术:
  • 浅 clone (shallow clone): 虽然可以减少下载的历史提交数量,但无法获取完整的代码历史,不利于代码追溯和版本管理,并且速度提升不明显。
  • 稀疏 clone (sparse clone): 虽然 `repo` 工具的按仓库粒度选择性克隆功能在管理大量仓库时提供了灵活性,但也带来了复杂性、依赖管理、版本一致性等方面的挑战。在使用这种方法时,团队需要充分考虑这些缺点,并制定相应的策略来管理和协调开发工作。
  • 部分 clone (partial clone): 虽然可以只下载文件元数据,但在需要实际文件内容时仍需额外下载,可能导致意外的延迟。

这些方法虽然在代码速度或者存储空间方面有所改善,但都无法全面解决我们面临的问题。特别是在处理超大型仓库时,这些方法往往显得力不从心。
主要影响我们开发的核心 2 大矛盾点:
  • 代码 clone 速度太慢
  • 高并发下无法复用代码缓存复用,容易造成代码冲突和代码污染


解法出现



图片

CNB 基于 Docker 生态,对环境、缓存、插件进行抽象,通过声明式的语法,帮助开发者以更酷的方式构建软件。

在这里可以托管代码和制品、项目开源以及参与社区协作,通过云原生构建可以更快的构建软件,使用云原生开发,告别复杂的本地开发环境,一键唤起云上工作空间。

其中他有一个非常重要的特性:“读秒克隆” , 就是可以在数秒内完成代码准备,无视仓库的大小,并且是并发安全的,高并发场景下也是如此。
这个特性对于我们团队非常重要,如果真的可以秒级 clone 准备好代码,仅仅在代码 clone 上就可以给我们团队代码大约 200 倍的速度提升。我们马上开始用 125GB 的 AOSP 代码仓库进行测试。

迁移到 CNB 也很简单
  1. 把代码仓库从使用 repo 管理,迁移到单仓 monorepo 来管理,用平台自动化脚本同步了历史提交记录,然后把代码仓库 push 到 CNB 的 git 仓库上
  2. 用 ubuntu 18.04 作为基础镜像,准备一个可以用来构建 AOSP 的 Dockerfile,作为构建容器。
  3. CNB 的配置是声明式的,主打"Everything as Code",通过配置文件来描述流水线,非常简单清晰,与 Git 代码仓库同源管理

1、首次启动,无缓存拉取代码,直接构建
  • 拉取代码:16m 52s, 124.44GB
  • 构建耗时:46m 4s

2、第二次以后,带有代码缓存,并且并发构建

真的 秒级 clone , 3.8s !!!124.44GB !!! 

且并发构建依然可以命中缓存!

  • 拉取代码:3.8s , 124.44GB

  • 构建耗时:1m 30s

下面截图就展示了并发 6 条流水线时的真实耗时:

图片


性能对比


代码准备

编译

未使用 CNB,无缓存加速

使用 CNB,缓存加速

3.8s

1m30s


使用 CNB 后,团队在代码克隆方面的需求得到了显著改善,传统的浅克隆和部分克隆等优化措施变得不再必要,因为 CNB 提供了快速且全面的代码克隆功能。这一优势使得 CNB 在与其他 CI 系统的比较中脱颖而出,有效解决了团队在处理超大型仓库时所面临的代码克隆和构建缓存问题。

秒级克隆原理

为什么 CNB 可以做到秒级克隆这么快?

CNB 使用 git-clone-yyds 插件实现的秒级克隆, 出于好奇,我阅读了代码,以了解其内部实现的机制。

1、缓存机制加速代码拉取

CNB 上代码首次被 clone 下来以后,会被持久化缓存到构建机的母机上。往后启动流水线只需要增量更新代码,这个过程比全量 clone 代码要快得多,可以在数秒内完成( 如 AOSP 125GB -> 3 秒完成)。

代码缓存能显著减少启动流水线时准备代码所需的时间。然而,如果仅仅实施代码缓存,其本质与在开发机上拉取代码并构建相似,存在明显缺陷。当团队中多人同时开发,需要并发启动多条流水线时(如同时进行端到端测试(e2e)、单元测试、构建多个包等),这些并发构建操作会同时修改工作空间(workspace)中的多个文件。这种情况下,被缓存的源代码可能被污染,从而影响流水线的幂等性,导致流水线失败,或者产生不可预测的结果,增加了维护成本,并且还有可能会出现 cache miss 的情况,导致不得不重新全量 clone 代码。

对于上面这种情况,大部分的 CI 系统会采用集中解决方案,如:

  • 增加锁机制,串行化流水线,同时只能有一个流水线可以运行,其他流水线需要排队等待。这种方式虽然可以解决问题,但会显著增加流水线的运行时间,降低系统的吞吐量,硬件资源利用率低,用户等待时间长。
  • 把代码缓存放到分布式存储中,这样可以避免代码被污染,如 bazel 的 remote cache。但是这种方式需要额外的成本:维护分布式存储,网络传输成本与耗时,数据一致性等问题,并且不能替代所有的场景。

那么,CNB 是如何做到在高并发中使用代码缓存的呢?

2、Copy-on-Write 机制

从计算机角度来看,git 代码克隆缓存和构建缓存等操作本质上是典型的文件独占问题。传统上,这些文件在同一时间只能服务于一次构建,限制了并发性能。为了解决这个问题,CNB 采用了  Copy-on-Write (简称 CoW)机制。

Copy-on-Write(CoW)是一种优化策略,允许多个进程共享同一份资源,直到需要修改时才创建副本。这种机制大家并不陌生,在 Docker 上就有使用。Docker 利用 OverlayFS(一种联合文件系统)来实现 CoW。OverlayFS 能够将多个目录层叠在一起,形成统一视图,这使得多个容器可以共享同一个基础镜像,只有在需要修改时才在特定容器中创建独立的可写层。这种技术不仅优化了存储使用,也是 Docker 容器瞬间启动的原因。

下图简单描述了 OverlayFS 的运作原理,其中:

  1. file1 是没修改的,直接从 lowerdir 透明地呈现在 merged 层。
  2. file2 在 upperdir 中被修改。当读取 file2 时,会看到 upperdir 中的修改版本。
  3. file3 在 upperdir 中被删除。虽然 lowerdir 中仍然存在 file3(在 upperdir 中它被标记为 "Whiteout" 状态),但在 merged 层中它是不可见的。
  4. file4 是新增的文件,直接存在于 upperdir 中。

图片


通过结合 CoW,CNB 能够有效地解决文件独占问题,允许多个并发构建共享相同的基础文件系统,同时保持各自的独立性。这不仅提高了系统的并发性能,还确保了每个构建环境的隔离性和一致性。

这就是 CNB 同时解决代码克隆速度慢和高并发下缓存复用冲突的关键所在。

3、git-clone-yyds 插件秒级 clone 原理

3.1、git-clone-yyds 工作流程图
git-clone-yyds 本质上是运行在母机上的一个 docker 容器,他通过  volume  把代码缓存挂载到工作区 (workspace)下。下图展示了 CNB 的 git-clone-yyds 在准备工作区的流程。

  1. git 代码缓存初始化:如果是首次拉代码,需要运行  git init  和  git fetch  进行代码的拉取,如果是已存在  .git  目录,则直接使用  git fetch  对  .git  进行更新
  2. workspace 代码准备,使用  OverlayFS  对  .git缓存  进行复制,瞬间创建副本。通过  mount -t overlay  的方式,把代码缓存挂载到 lowerdir,并且建立一个空的 upperdir,然后 merged 就是最终在流水线看到的 workspace 的文件视图。
  3. 在 merged 文件夹上进行 checkout、build 等操作,基于 CoW 的特性,可以实现当需要写入时,从 lowerdir 的 cache 上 copy 出来再写入,如果只需要读,直接从 lowerdir 读取,这样就能保证在并发场景下的代码隔离。
  4. 构建结构后,删除副本。

图片


3.2、从文件系统挂载上观察 git-clone-yyds
从文件系统挂载上看,可以更清晰的看到 CNB 是如何使用 git-clone-yyds 实现秒级的代码 clone 的。

图片


当母机上启动构建容器时,会将 /data/git/{group}~{repo}/cache 作为只读的 lowerdir 层,通过 OverlayFS 挂载到容器的/workspace目录下。这个 /workspace 目录具有 Copy-on-Write 特性,任何对文件的修改都只会影响容器自己的 upperdir,不会改变底层 lowerdir 的代码缓存。

这样就实现了代码的秒级拉取,同时保证了并发场景下的代码隔离。

3.3 并发构建场景下的缓存
在同一个代码仓库并发多个流水线构建时,母机上只会存在一份代码缓存,。每个流水线会通过自己的 git-clone-yyds 准备 workspace 代码,通过 OverlayFS 建立 CoW 文件夹等。同一个代码仓库不同的流水线的 workspace 是通过 pipeline id 文件夹实现隔离的,但是 workspace 底层的  lowerdir  是同一个。这样就保证了在并发启动多条流水线的时候,可以并发使用缓存。母机上只需要一份缓存代码,并且最重要的首次准备好代码缓存以后,可以在数秒内完成代码准备。高并发场景下,也是如此

图片

总的来说, git-clone-yyds 的性能非常高
  •  时间复杂度:O(1),无视仓库大小,并发安全。
  •  空间复杂度:O(1),存储空间不随并发数增长。

数据

从 CNB 系统全局性能监控来看,git-clone-yyds 非常快, git clone 的时间稳定在 10s 以下,大部分项目都在 3~6s 时间准备好工作区,速度非常快!

图片


更多的玩法


当然这里只是 CNB 对克隆时间的加速,如何将 AOSP 的编译时间从46分钟显著缩短至仅1分钟?后面我们再深入探讨 CNB 如何通过使用 CoW 和 volume 实现编译缓存的具体实现。


CNB 系统通过创新的 git-clone-yyds 插件和 Copy-on-Write (CoW) 机制,不仅解决了大型代码仓库的克隆速度和并发构建问题,还为我们开启了更多可能性。

图片