成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Kotlin 云端差分緩存技術

移動開發
本篇文章我們來具體闡述下 BuildInfra 團隊自研的解決方案 - 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,直接進入下一步。

由于精確匹配需要達成的條件較多,比如只要修改了一個 .kt文件,就無法匹配到緩存

因此可以考慮實現一套模糊匹配的機制:

  • 先根據一些必要參數匹配一組可能符合要求的緩存
  • 然后從這一組緩存中尋找與當前場景最接近的緩存包來進行使用

全量轉增量

最接近的緩存包,也只是最接近的,是不能直接拿來用的。

那我們是不是可以將這個緩存包對應的源碼與當前源碼做 diff,然后將全量轉增量呢?

比如,某個模塊有 A.kt,B.kt,我們基于某個 Commit 打出來了一個云端緩存包。

有一天,我們新增了一個 C.kt,改動了 B.kt,這時候,我們找到之前打出來的緩存包,對他們對應的源碼做一次 diff,那么我們可以得到這組變動:

[
   changed: ["B.kt"],
   added: ["C.kt"]
]

我們只需要將這個 Diff 輸入給 kotlin 編譯器,kotlin 就會自動幫我們完成耗時較低的增量編譯

基于 #1#2 的兩個核心原理,我們設計了一套『Kotlin 差分編譯系統』,整體架構圖如下,有四個重要的角色:

  • 客戶端:Hook Kotlin Compiler,負責將全量編譯轉為增量編譯。
  • 服務端:基于客戶端的請求匹配最佳緩存。
  • 種子節點:基于 develop 分支,定時打包,上傳緩存。
  • 分布式緩存:我們內部使用的緩存系統,主要通過 K-V 的形式來存儲我們的緩存包。

圖片圖片

四、方案詳解

客戶端方案

客戶端的核心邏輯:對 kotlin 編譯流程進行 hook,構造/加載緩存包,將全量編譯轉換為增量編譯,轉換的關鍵在于增量編譯與全量編譯的差異部分:

  1. 增量編譯環境中有上次編譯好的編譯產物、增量追蹤文件(符號引用關系,abi 信息等)。
  2. 增量編譯相比于上次編譯有哪些輸入文件產生了修改的文件 diff 信息。

圖片圖片

緩存包內容

為了能夠在全量編譯時順利還原出一個正確的增量編譯現場,要求加載的緩存包中應該包含的內容有:

  1. 編譯產物 (減少不必要的源文件重新編譯耗時)
  2. metadata 包括 kt 、java、layout 文件等 源文件信息,包括這些文件在項目中的相對路徑以及 hash 值,在還原編譯現場時,根據這些內容還原為源文件的 diff 信息 ( add, remove ,modify)
  3. 編譯器信息:編譯器參數,編譯器 JAR 包的 hash 信息等。
  4. 增量中間產物:涉及到一系列追蹤文件,這些文件包含了kotlin 編譯器在增量編譯時 用來計算 dirty file (需要重新編譯的源文件)的關鍵信息,包括符號引用關系、abi 信息、源文件與編譯產物的對應關系等。

圖片圖片

緩存匹配

如前文中的『核心原理』章節所描述,我們采取以下方案來進行緩存的匹配。

  • 先根據一些精準參數匹配一組可能符合要求的緩存
  • 然后從這一組緩存中尋找與當前場景最接近的緩存包來進行使用

其中以下參數會參與精確參數(unique_hash)的計算

  • Kotlin 版本
  • Kotlin Compiler Plugin (KCP)
  • Kotlin 編譯器參數

我們通過將這些參數組合到一起組成一個 hash 值:unique_hash。

在之后的流程中,會使用 unique_hash 來篩選出一組緩存。我們再通過服務端的策略來篩選出最佳的緩存。

圖片圖片

編譯流程 hook

拿到了緩存包后,我們就可以進行緩存包的復用了。但是我們在上一個流程中匹配到的僅僅是最優緩存,但是這個緩存和我們當前的包還存在 diff,比如代碼、模塊依賴信息 等的差異。

我們需要計算出拉到的緩存包和當前編譯的 diff,然后將 diff 傳給 kotlin 編譯器,并將全量編譯轉成增量編譯。

為了能夠實現增量編譯的轉換,需要在編譯流程的三個階段進行 hook

  1. 編譯前執行前解析模塊依賴信息。
  2. 在提交編譯參數到編譯器執行前,匹配、下載、校驗、加載緩存包,將全量編譯參數轉換為增量編譯參數。
  3. 編譯完成后構造并上傳緩存包。

圖片圖片

其中,最核心的 Hook 點在于:將全量編譯轉化成增量編譯。

internal fun CallCompilerAsyncOpt.callCompilerAsyncOpt(
    ....
) {
    val icEnv = IncrementalCompilationEnvironment(
            changedFiles,
            classpathChanges,
            workingDir ,
            ...
        )
    val hookedIcEnv = hook(icEnv)

    val environment = GradleCompilerEnvironment(
        incrementalCompilationEnvironment = hookedIcEnv,
        outputFiles,...
    )
    optRunJvmCompilerAsync(..., source, args, environment , ... )
}

我們會 Hook KotlinCompile 中的 callCompilerAsync 方法,將原來的全量 Env 轉換成增量 Env。增量的 InputChanges 來源于我們的緩存包和編譯的 diff (源碼、依賴的變更等)。

而這個關鍵的全量轉增量 Hook 步驟為:

  1. 匹配下載緩存:為了能夠匹配到最接近的緩存包,實現最佳增量編譯效率,需要對這些參數進行比較:
  1. 根據 metadata 中 java/kotlin/layout 文件的 hash 比較源碼文件的 diff (其中包括 layout xml 是因為 KAE 會根據 layout 進行 kotlin 代碼插樁)
  2. 利用 kotlin 1.7+ 的 classpath-snapshot.bin 計算依賴模塊依賴信息的 abi diff
  1. 校驗加載緩存: 基于緩存包中的 metadata ,結合編譯產物、編譯中間產物等,將對應信息加載到對應位置。
  2. 構造增量編譯參數: 將加載的緩存包中的相對路徑轉換為絕對路徑,用絕對路徑的 diff 信息等構造出可用的增量 Env , 并提交給 Kotlin Compiler

預期之外的問題

理論上來講,如果 kotlin 編譯器是能保證增量編譯是完全不出錯的,那么我們的方案肯定不會引入穩定性的問題。

但是這樣的期待或許過高。我們的方案是基于 Kotlin 1.7.21 進行開發的,kotlin 在 1.7+ 引入了新的 kotlin 增量編譯方案,還在一個相對不穩定的階段。實際上,我們也在上線前后遇到了各種奇怪的問題。經過長時間的 debug 和翻閱源碼后得到了解決,目前在抖音項目上已經趨于穩定。

跨端緩存復用問題

問題表現為:OSX 復用 ci 種子節點打包的緩存時,對于部分刪除的源代碼文件,并沒有將其對應的編譯產物刪除

通過一系列的排查發現,該問題是 Kotlin Compiler 自身的 bug ,且在高版本進行了修復,bug 來源于 kotlin 的增量編譯中間產物中,用于追蹤源碼文件與編譯產物關系的信息異常,在涉及到大小寫敏感不一致的系統時,無法正常刪除編譯產物。

解決方案: 對高版本的修復 commit 進行了 cherry-pick

kotlin 修復 https://github.com/JetBrains/kotlin/commit/be71d8841ebc22c79bb6b4bc6f3ad93c147ba9c0

大小寫敏感問題

由于 OSX 不是一個大小寫敏感的操作系統,這意味著,在一個文件夾中,不能同時存在去掉大小寫后字符一樣的兩個文件。比如:

圖片圖片

但是這個行為在 Linux 系統上是允許的。

我們的遇到的問題是,項目中不同文件夾中有兩個包名叫 com.xxx.legoImp ,一個叫com.xxx.legoimp 。這兩個包名會在 Linux 種子節點上進行 Jar 包壓縮,在 mac 上進行解壓 ,由于 Linux 不是 Case Sensitive 的系統,所以 jar 包中,他倆會同時存在,但是在 mac 上解壓時,mac 會自動合并了這兩個包,并且轉換成小寫。這樣就會在運行時崩潰,找不到這個包名。

解決方案:我們提供了一個類似的包名大小寫檢測,幫助業務提前暴露問題,并進行代碼的整改。

方案性能極致優化

加速緩存解壓

在最初的緩存包壓縮中,采用的是 Java ZipFile 中默認的壓縮算法,上線后發現有較多模塊的緩存解壓時間較長,后續參考了Gradle build-cache 方案,將壓縮算法修改為 lz4,大大降低了解壓縮的時間。

降低緩存淘汰率

在本方案中,緩存遠端存儲在我們的分布式緩存服務中,由于分布式緩存空間有限(2TB),超過緩存空間時會根據 LRU 淘汰最近未使用的緩存,這樣就會導致緩存命中率下降,而由于在抖音項目中,生產緩存的種子節點會在每個 MR 合入后進行緩存打包,每次全量打包緩存時,緩存包約1GB+ , 為了減少不必要的緩存上傳,采取了兩個方案:

  1. 與 build-cache 進行關聯,如果種子節點打包時命中 build-cache ,將 kotlin 緩存與 build-cache 進行關聯,而不是重復上 kotlin 緩存
  2. 計算緩存包內容 hash ,如果存在相同的緩存包,將他們關聯到一起,而不是重復上傳緩存。

服務端策略

區別于傳統的 Build Cache 服務端的簡單 K-V 存儲,我們的服務端需要幫助客戶端去存儲產物、匹配產物、選擇最佳產物。客戶端和服務端的通信時序圖如下:

圖片圖片

其中,服務端最核心的邏輯在于匹配最佳產物

每一個 unique hash 都會對應一組產物,我們要從這一組產物里找到一個最優解。

最優解的定義是:這個產物離我們當前的編譯較為接近。我們為這個『接近』,定義了一個記分制度。

data class Diff(val added: List<String>, 
                val removed: List<String>,
                val modified: List<String>)
fun diffScore(): Int {
    return this.diff.removed.size * 3 +
            this.diff.modified.size * 2 +
            this.diff.added.size
}

每個產物和當前編譯都會產生一個 diffdiff 里有 addedremovedmodified

我們定義一個函數 diffScoreremove 記三分,modified 記兩分,added 記一分。

之所以按照這樣的系數,是因為 removed 大概率會引起很多底層依賴的重新編譯,所以我們需要盡可能地減少 removed 的代碼,added 的代碼一般不會引起其它代碼重新編譯,所以我們可以記為一分。modified 介于兩者之間,記兩分。

那么,我們的核心的邏輯就變成了:從一組緩存里,找到 diffScore 最少的產物

但是因為我們的 unique hash 對應的 hash 生成并不復雜,一般來說,除非是升級 kotlin 版本,其它情況這個 unique hash 基本不會變,所以這個緩存的量可能會很大,我們不可能實現找到所有的緩存,并且進行 diffScore 的比較。我們需要篩選出一定范圍內的緩存。

首先,我們的所有的產物生成都是在主干分支上。

那么意味著,對于 feature 分支,與其代碼最接近的 commit 應該是圖中的 base commit

圖片圖片

因為成本的原因,種子任務可能并不會在主干分支的每個節點都打緩存包,可能是定時打包。那么意味著不是所有的 merge commit 都有緩存包。

所以,我們找到這個 commit 的前后一天時間內的 commit。并和表中的所有產物進行取交集

圖片圖片

最后得到所有黃色的點。(黃色的點是打了緩存包的 merge 節點)

我們將所有黃色的點對應的 MetaData 與當前 commit 的 MetaData 進行 diff。

取一個 diffScore 最少的節點,作為最終的產物。

val bestArtifact = artifacts.minOf {
    it.diffScore()
}

這樣我們就可以認為它是和當前 commit 最接近的 commit。對應的產物對于客戶端來說,轉增量編譯的風險最小、速度最快。

五、上線收益

目前項目上線了大部分的字節 Monorepo 項目,穩定運行了半年有余。期間也修復了 kotlin 1.7 上的若干官方增量編譯相關的 bug,最后取得了較為理想的收益。 

抖音從二進制演進到 Mopnorepo 后,Kotlin 部分的編譯時間劣化超過 10m+,我們通過本文的方案,將劣化的部分基本抹平,90 分位下降 60%,大幅提升了開發的體驗和 CI 合碼效率。

六、總結

本文主要闡述了 Kotlin 云端差分方案的技術原理。

實現超大型工程 Monorepo 全源碼的研發模式,僅靠 BuildCache 是無法實現的,Kotlin 云端差分方案對于全源碼起著不可或缺的作用。

通過模糊匹配緩存的方式將全量編譯模擬成增量編譯的思想,可能在其他端上比較容易實現或者就是標準方案,但目前 Android CI 構建領域均采用 clean 編譯,無法復用構建中間產物。該方案的出現也有望實現思想的復用,運用到所有『類似』的任務中,比如 Java Compile、Transform、Dex 等,只要它本身是支持增量、編譯冪等特性的,都可以復用這套方案,從而進一步提升 Android 的構建效率。

在研究云端差分方案期間,我們積累了大量的 Kotlin 編譯器相關的知識,為我們平時排查 kotlin 疑難問題提供了非常多的技術儲備,也發現了很多 kotlin 官方的 bug 和設計上的缺陷,我們也會在將來修復后回饋 kotlin 社區。

歡迎對編譯構建、Kotlin相關技術感興趣的小伙伴找我們進行技術交流與討論!

責任編輯:龐桂玉 來源: 字節跳動技術團隊
相關推薦

2022-04-06 15:58:25

火山引擎差分隱私LDPDC

2015-04-27 14:42:24

技術架構服務器性能

2023-09-03 19:13:29

AndroidKotlin

2012-04-02 16:35:49

網絡緩存

2012-12-26 09:32:55

智能手機移動傳感平臺云端分載GPS

2012-03-13 08:56:39

2016-01-04 11:04:47

無線技術傳感技術物聯網

2015-05-13 13:13:34

2016-02-01 17:17:38

AWS技術峰會AWS Summit2

2023-11-11 19:43:12

緩存數據庫

2011-12-28 10:44:02

PowerVM虛擬化

2011-07-11 10:00:34

PHP緩存技術

2018-08-07 10:44:50

緩存技術瀏覽器

2009-07-29 15:38:01

2015-06-10 11:47:21

云計算互聯網+O2O

2010-06-03 15:21:49

服務器虛擬化桌面與應用虛擬化數據中心

2013-07-11 11:41:31

2013IBM技術峰會移動

2010-06-01 15:35:32

2021-01-13 12:10:09

物聯網隱私網絡安全

2016-12-01 15:03:36

緩存技術客戶端
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 我要看黄色录像一级片 | 欧美1—12sexvideos | 亚洲精品综合一区二区 | 国产一区二区在线播放 | 欧洲亚洲一区二区三区 | 国产免费一区二区 | 黄色免费av | av在线免费不卡 | 亚洲第一网站 | 国产日日操 | 老司机午夜性大片 | 欧洲视频一区二区 | 国产91视频一区二区 | 中文一区二区 | 精品福利一区二区三区 | www.xxxx欧美| 四虎免费视频 | 国产福利视频在线观看 | 天天操网 | 成人影音 | 日韩欧美精品 | 欧美一区二区三区,视频 | 亚洲夜射 | 亚洲一区二区中文字幕 | 91精品国产91久久久久青草 | 国产综合第一页 | 欧美日韩精品专区 | 天天干天天色 | 日本天天操 | 日韩一级| 欧美另类视频 | 欧美日韩亚洲一区 | 九九热视频这里只有精品 | 免费成人毛片 | 毛片a级毛片免费播放100 | 在线播放亚洲 | 亚洲黄色国产 | 国产精品国产三级国产aⅴ原创 | 日韩国产中文字幕 | 欧美一区二区在线 | 国产高清视频在线观看 |