Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。

在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。

一、Monorepo 中的噩梦

在 2022-2023 年,我们的头部业务开始慢慢地从原来的多仓二进制模式,迁移到全新 Monorepo 方案。

在多仓二进制时代,由于 Maven 的加持,大部分时候我们的都不需要直接编译代码,而是复用 Maven 的『缓存』。

在工程进入 Monorepo 时代之后,我们花了大量的精力建设 BuildCache,来试图抹平 Monorepo 与二进制的构建速度差异,但遗憾的是,尽管我们已经做了很多的努力,但由于 Gradle 脆弱的 BuildCache 设计,导致很多时候改动一行代码就会全局 miss,不得不重新编译一遍所有的源码。这很不环保(

  • 本地编译:首次编译/更新代码/切分支等场景,非常容易触发整个工程的重新编译,动辄 20min+。

  • CI 编译:从原来多仓二进制时代的 25min 劣化到了 40min+

并且,90 分位的构建劣化更是飙升到不可控的地步,它直接决定了本地开发、CI 合码的体验。

在可预见的将来,随着代码的快速增长、更多子仓的合入,仅靠 BuildCache 已经完全无法支撑大型 Monorepo 工程的开发。

二、分析

如果想优化 Kotlin 的全量编译,如果从计算机通用优化角度来看,不外乎以下几种方案:

  • 更高效的实现

  • 并发

  • 缓存

更高效的实现:语言编译是一个庞大且复杂的系统,涉及前后端编译,目前针对前后端编译流程进行优化实施起来难度较大,是一个长期且艰巨的任务。但它并不和其他优化方案有冲突。

并发:如果是相互之间没有复杂依赖关系的多 Module,那么 Gradle 已经保证了模块之间的并行度;只要你的项目依赖足够 flat,那么就可以充分利用计算机并发资源,在 CI 场景会获得非常大的提升,所以从工程角度可以进行依赖的优化来提升并行度。但这也不是一个通用的方案,非常依赖于业务方的改造。

缓存:Kotlin 目前使用的是 Gradle 的缓存,众所周知,Gradle 为了保证正确性,有着一套极其严格的 Cache key 生成策略,很多「可能」不影响 kotlin 编译正确性的变化(比如编译插件的 classpath)却会让整个 BuildCache 崩盘,全部 Task 全部 Cache Miss。

在 kotlin 中,不仅是 classpath 这些变动会导致 cache miss,kotlin 的 sourceset(即源码)也会作为 cache key 的计算输入。这意味着,只要代码里有一个字符不一样,就会导致 cache miss,整个模块就需要重新编译,那么这块是否有优化空间呢?

综合来看,从缓存角度入手,可以以比较低成本来实现较大的收益。

为此,我们提出了一套创新性的方案:

通过云端模糊匹配缓存来将全量编译转化为增量编译,来减少全量编译的耗时。

三、核心原理

我们通过改造 Kotlin Gradle Plugin 入手,当 Kotlin Task 由于各种原因不能命中 Build Cache 时,就会 fallback 到我们的『Kotlin 差分编译系统』。通过云端模糊匹配缓存来将全量编译转化为增量编译,来将本来动辄 10min+ 的全量编译转化成 10s 内的增量编译。

云端缓存模糊匹配

常规编译构建缓存方案如 Gradle Build Cache 采用的是 kv 一对一匹配。

对于 Kotlin 来说,即通过对源码文件、依赖 Jar 包,编译参数等信息算出一个缓存 key 之后,唯一匹配出一个缓存。当匹配到缓存后,缓存包的内容就是 Kotlin 的最终产物,然后就可以跳过 Kotlin Task,直接进入下一步。

由于精确匹配需要达成的条件较多,比如只要修改了一个 .k

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值