Kotlin 云端差分缓存技术
内容提要
字节跳动Buildinfra团队开发的Kotlin云端差分方案通过云端模糊匹配缓存将全量编译转化为增量编译,减少编译时间。方案已在抖音项目上线并取得了较好的效果。
延伸解读
为何传统 BuildCache 在 Monorepo 中失效
文章指出,Gradle BuildCache 采用严格的键值匹配,任何细微变化(如源码字符改动、插件 classpath 变动)都会导致缓存未命中,触发全量编译。在 Monorepo 全源码场景下,这种设计导致编译时间从多仓二进制时代的 25 分钟劣化到 40 分钟以上,90 分位构建时间更是不可控。因此,仅靠 BuildCache 无法支撑大型 Monorepo 的研发效率,需要新的缓存匹配思路。
模糊匹配与差分编译如何协同工作
方案核心是云端模糊匹配缓存:先根据 Kotlin 版本、编译器参数等生成 unique_hash 筛选一组候选缓存,再通过服务端计算与当前代码的差异(added/removed/modified)并评分,选出最接近的缓存包。客户端 Hook Kotlin 编译流程,将缓存包与当前源码的 diff 转换为增量编译参数,从而把全量编译转化为增量编译,大幅缩短编译时间。
上线收益与跨平台兼容性挑战
该方案已在抖音等字节 Monorepo 项目稳定运行半年,90 分位编译时间下降 60%,基本抹平了从二进制迁移到 Monorepo 带来的 10 分钟以上劣化。但过程中也遇到 Kotlin 1.7 增量编译的官方 bug,如跨大小写敏感系统时编译产物删除异常,团队通过 cherry-pick 修复和包名大小写检测来解决,提示了方案对编译器版本的依赖和跨平台适配的重要性。
方案的可复用性与未来展望
文章认为,这种通过模糊匹配缓存将全量编译模拟为增量编译的思想,不仅适用于 Kotlin,还可复用到 Java Compile、Transform、Dex 等支持增量且编译幂等的任务中,从而提升整个 Android 构建效率。目前 Android CI 领域普遍采用 clean 编译,无法复用中间产物,该方案为超大型工程提供了新的优化方向,并积累了 Kotlin 编译器相关的技术储备。
Q&A
Kotlin云端差分方案是什么?它解决了什么问题?
Kotlin云端差分方案是字节跳动Buildinfra团队自研的编译优化方案,通过云端模糊匹配缓存将全量编译转化为增量编译,从而减少编译时间。它主要解决Monorepo全源码模式下Kotlin编译耗时过长的问题,因为传统的BuildCache在Monorepo中容易失效,导致频繁全量编译。
为什么在Monorepo中Gradle BuildCache效果不佳?
Gradle BuildCache采用严格的Cache key生成策略,任何细微变化(如编译插件classpath变动、源码中一个字符不同)都会导致cache miss,从而触发整个模块重新编译。在Monorepo中,改动一行代码就可能全局miss,不得不重新编译所有源码,导致编译时间大幅增加。
Kotlin云端差分方案的核心原理是什么?
核心原理包括两点:一是云端模糊匹配缓存,先根据必要参数匹配一组可能符合的缓存,再从中选择最接近的缓存包;二是全量转增量,将缓存包对应的源码与当前源码做diff,得到变更文件列表,然后将diff输入Kotlin编译器,自动完成增量编译,从而将全量编译转化为增量编译。
该方案在抖音项目上线后取得了怎样的收益?
方案上线后,抖音项目Kotlin编译时间劣化部分基本被抹平,90分位编译时间下降60%,大幅提升了开发体验和CI合码效率。目前该方案已上线大部分字节Monorepo项目,稳定运行半年多。
Kotlin云端差分方案中,服务端如何选择最佳缓存包?
服务端为每个缓存包与当前编译的差异计算diffScore,其中removed文件记3分,modified记2分,added记1分。然后从一组缓存中选取diffScore最小的缓存包作为最佳产物。为了缩小范围,服务端会基于主干分支上最近一天内有缓存包的commit进行筛选。
该方案在实现过程中遇到了哪些挑战?如何解决的?
主要挑战包括:1. Kotlin 1.7增量编译不稳定,遇到跨端缓存复用问题(OSX复用CI缓存时删除文件未清理编译产物),通过cherry-pick官方修复commit解决;2. 大小写敏感问题(Linux和OSX文件系统差异导致包名冲突),通过提供包名大小写检测工具帮助业务整改;3. 缓存解压慢,改用lz4压缩算法;4. 缓存淘汰率高,通过关联build-cache和计算缓存包hash减少重复上传。
Kotlin云端差分方案可以应用到其他编译任务吗?
可以。该方案的思想有望复用到所有支持增量、编译幂等特性的任务中,比如Java Compile、Transform、Dex等,从而进一步提升Android构建效率。目前Android CI构建领域普遍采用clean编译,无法复用中间产物,该方案为这类场景提供了新的优化思路。