自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

飛書(shū) Android 升級(jí) JDK 11 引發(fā)的 CI 構(gòu)建性能問(wèn)題

原創(chuàng) 精選
移動(dòng)開(kāi)發(fā) Android
本文從飛書(shū) Android 升級(jí) JDK 11 意外引發(fā)的 CI 構(gòu)建性能劣化談起,結(jié)合高版本 JDK 在 Docker 容器和 GC 方面的新特性,深挖 JVM 和 Gradle 的源碼實(shí)現(xiàn),抽絲剝繭地介紹了分析過(guò)程和修復(fù)方法,供其他升級(jí) JDK 的團(tuán)隊(duì)參考。

作者|秦兵兵 & 宋志陽(yáng)

一、摘要

本文從飛書(shū) Android 升級(jí) JDK 11 意外引發(fā)的 CI 構(gòu)建性能劣化談起,結(jié)合高版本 JDK 在 Docker 容器和 GC 方面的新特性,深挖 JVM 和 Gradle 的源碼實(shí)現(xiàn),抽絲剝繭地介紹了分析過(guò)程和修復(fù)方法,供其他升級(jí) JDK 的團(tuán)隊(duì)參考。

二、背景

最近飛書(shū)適配 Android 12 時(shí)把 targetSdkVersion 和 compileSdkVersion 改成了 31,改完后遇到了如下的構(gòu)建問(wèn)題。

圖片

在 StackOverflow 上有不少人遇到同樣的問(wèn)題,簡(jiǎn)單無(wú)侵入的解決方案是把構(gòu)建用的 JDK 版本從 8 升到 11。

圖片

飛書(shū)目前用的 AGP 是 4.1.0,考慮到將來(lái)升級(jí) AGP 7.0 會(huì)強(qiáng)制要求 JDK 11,而且新版 AS 已經(jīng)做了鋪墊,所以就把構(gòu)建用的 JDK 版本也升到了 11。

圖片

三、問(wèn)題

升級(jí)后不少同學(xué)反饋?zhàn)觽}(cāng)發(fā)組件(即發(fā)布 AAR)很慢,看大盤(pán)指標(biāo)確實(shí)上漲了很多。

圖片

除了子倉(cāng)發(fā)組件指標(biāo)明顯上升,每周例行分析指標(biāo)時(shí)發(fā)現(xiàn)主倉(cāng)打包指標(biāo)也明顯上升,從 17m上升到了 26m,漲幅約 50%。

圖片

四、分析

1.主倉(cāng)打包和子倉(cāng)發(fā)組件變成了單線程

子倉(cāng)發(fā)組件指標(biāo)和主倉(cāng)打包指標(biāo),都在 06-17 劣化到了峰值,找了 06-17 主倉(cāng)打包最慢的 10 次構(gòu)建進(jìn)行分析。

圖片

初步分析就有一個(gè)大發(fā)現(xiàn):10 次構(gòu)建都是單線程。

圖片

而之前正常的構(gòu)建是并發(fā)的

圖片

子倉(cāng)發(fā)組件的情況也一樣,由并發(fā)發(fā)布變成了單線程發(fā)布。

圖片

圖片

2.并發(fā)變單線程和升級(jí) JDK 有關(guān)

查了下并發(fā)構(gòu)建相關(guān)的屬性,org.gradle.parallel 一直為 true,并沒(méi)有更改。然后對(duì)比機(jī)器信息,發(fā)現(xiàn)并發(fā)構(gòu)建用的是JDK 8,可用核心數(shù)是 96;單線程構(gòu)建用的是 JDK 11,可用核心數(shù)是 1。初步分析,問(wèn)題應(yīng)該就在這里,從 JDK 8 升到 JDK 11 后,由并發(fā)構(gòu)建變成了單線程構(gòu)建,導(dǎo)致耗時(shí)明顯上升。而且升級(jí) JDK 11 的修改是在 06-13 合入主干的,06-14 構(gòu)建耗時(shí)明顯上升,時(shí)間上吻合。

圖片

圖片

3.整體恢復(fù)了并發(fā),但指標(biāo)沒(méi)下降

為了恢復(fù)并發(fā)構(gòu)建,容易聯(lián)想到另一個(gè)相關(guān)的屬性 org.gradle.workers.max。

圖片

由于 PC 和服務(wù)器可用核心數(shù)有差異,為了不寫(xiě)死,就試著在 CI 打包時(shí)動(dòng)態(tài)指定了 --max-workers 參數(shù)。設(shè)置參數(shù)后主倉(cāng)打包恢復(fù)了并發(fā)構(gòu)建,子倉(cāng)發(fā)組件也恢復(fù)了并發(fā)。

圖片

但觀察了一周大盤(pán)指標(biāo)后,發(fā)現(xiàn)構(gòu)建耗時(shí)并沒(méi)有明顯的回落,穩(wěn)定在 25 m,遠(yuǎn)高于之前 17 m的水平。

圖片

4.重點(diǎn) Task 的耗時(shí)沒(méi)下降

細(xì)化分析,發(fā)現(xiàn) ByteXTransform(ByteX是字節(jié)推出的基于 AGP Transform 的開(kāi)源字節(jié)碼處理框架,通過(guò)把多個(gè)串行執(zhí)行重復(fù) IO 的 Transform 整合成一個(gè) Transform 和并發(fā)處理 Class來(lái)優(yōu)化 Transform 性能,詳見(jiàn)相關(guān)資料)和 DexBuilder 的走勢(shì)和構(gòu)建整體的走勢(shì)一致,06-21 后都維持在高位,沒(méi)有回落。ByteXTransform 劣化了約 200 s,DexBuilder 劣化了約 200 s,而且這兩個(gè) Task 是串行執(zhí)行,合在一起劣化了約 400 s,接近構(gòu)建整體的劣化9 m。GC 情況在 06-21 后也沒(méi)有好轉(zhuǎn)。

圖片

圖片

圖片

5.獲取 CPU 核心數(shù)的 API 有變化

進(jìn)一步分析發(fā)現(xiàn)其他 Transform (由于歷史原因,有些 Transform 還沒(méi)有接入 ByteX)并沒(méi)有劣化,只有 ByteXTransform 明顯劣化了 200s。聯(lián)想到 ByteXTransform 內(nèi)部使用了并發(fā)來(lái)處理 Class,而其他 Transform 默認(rèn)都是單線程處理 Class,排查的同學(xué)定位到了一行可能出問(wèn)題的代碼。

圖片

調(diào)試 DexBuilder 時(shí)發(fā)現(xiàn)核心邏輯 convertToDexArchive 也是并發(fā)執(zhí)行。

圖片

再聯(lián)想到雖然使用 --max-workers 恢復(fù)了并發(fā)構(gòu)建,但 OsAvailableProcessors 字段仍然為 1,而這個(gè)字段在源碼中是通過(guò)下面的 API 獲取的ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors()

圖片

ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors() 和Runtime.getRuntime().availableProcessors() 的效果一樣,底層也是 Native 方法。綜上推斷,可能是 JDK 11 的 Native 實(shí)現(xiàn)導(dǎo)致了獲取核心數(shù)的 API 都返回了 1,從而導(dǎo)致雖然構(gòu)建整體恢復(fù)了并發(fā),但依賴(lài) API 進(jìn)行并發(fā)設(shè)置的 ByteXTransform 和 DexBuilder 仍然有問(wèn)題,進(jìn)而導(dǎo)致這兩個(gè) Task 的耗時(shí)一直沒(méi)有回落。

圖片

直接在 .gradle 腳本中調(diào)用這兩個(gè) API 驗(yàn)證上面的推斷,發(fā)現(xiàn)返回的核心數(shù)果然從 96 變成了 1。

圖片

圖片

另外有同學(xué)發(fā)現(xiàn)并不是所有的 CI 構(gòu)建都發(fā)生了劣化,只有用 Docker 容器的 CI 構(gòu)建發(fā)生了明顯的劣化,而 Linux 原生環(huán)境下的構(gòu)建正常。所以獲取核心數(shù)的 Native 實(shí)現(xiàn)可能和 Docker 容器有關(guān)。

GC 劣化推斷也是同樣的原因。下面用 -XX:+PrintFlagsFinal 打印所有的 JVM 參數(shù)來(lái)驗(yàn)證推斷。可以看到單線程構(gòu)建用的是 SerialGC,GC 變成了單線程,沒(méi)能利用多核優(yōu)勢(shì),GC 耗時(shí)占比高。并發(fā)構(gòu)建用的是 G1GC,而且 ParallelGCThreads = 64,ConcGCThreads = 16(約是 ParallelGCThreads 的 1/4),GC 并發(fā)度高,兼顧 Low Pause 和 High Throughput,GC 耗時(shí)占比自然就低。

// 單線程構(gòu)建時(shí) GC 相關(guān)的參數(shù)值
bool UseG1GC = false {product} {default}
bool UseParallelGC = false {product} {default}
bool UseSerialGC = true {product} {ergonomic}
uint ParallelGCThreads = 0 {product} {default}
uint ConcGCThreads = 0 {product} {default}
// 并發(fā)構(gòu)建時(shí) GC 相關(guān)的參數(shù)值
bool UseG1GC = true {product} {ergonomic}
bool UseParallelGC = false {product} {default}
bool UseSerialGC = false {product} {default}
uint ParallelGCThreads = 63 {product} {default}
uint ConcGCThreads = 16 {product} {ergonomic}

圖片

圖片

6.Native 源碼分析

下面分析下 JDK 8 和 JDK 11 獲取可用核心數(shù)的 Native 實(shí)現(xiàn),由于 AS 默認(rèn)使用 OpenJDK,這里就用OpenJDK 的源碼進(jìn)行分析。

JDK 8 實(shí)現(xiàn)

圖片

JDK 11 實(shí)現(xiàn)

圖片

JDK 11 默認(rèn)沒(méi)有設(shè)置可用核心數(shù)并開(kāi)啟了容器化,所以可用核心數(shù)由 OSContainer::active_processor_count() 決定。

查詢(xún) Docker 環(huán)境下的 CPU 參數(shù)并代入計(jì)算邏輯,很容易得出可用核心數(shù)是 1,從而導(dǎo)致 Native 方法返回 1

cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
cat /sys/fs/cgroup/cpu/cpu.shares

圖片

五、修復(fù)

1.設(shè)置相關(guān)的 JVM 參數(shù)

總結(jié)上面的分析可知,問(wèn)題的核心是在 Docker 容器默認(rèn)的參數(shù)配置下 JDK 11 獲取核心數(shù)的 API 返回值有了變化。Gradle 構(gòu)建時(shí) org.gradle.workers.max 屬性的默認(rèn)值、ByteXTransform 的線程數(shù)、DexBuilder 設(shè)置的 maxWorkers、OsAvailableProcessors 字段、GC 方式都依賴(lài)了獲取核心數(shù)的 API,用 JDK 8 構(gòu)建時(shí) API 返回 96,用 JDK 11 構(gòu)建時(shí)返回 1,修復(fù)的思路就是讓 JDK 11 也能正常返回 96。

從源碼看,修復(fù)該問(wèn)題主要有兩種辦法:

圖片

設(shè)置 -XX:ActiveProcessorCount=[count],指定 JVM 的可用核心數(shù)

設(shè)置 -XX:-UseContainerSupport,讓 JVM 禁用容器化

設(shè)置 -XX:ActiveProcessorCount=[count]

圖片

根據(jù) Oracle 官方文檔和源碼,可以指定 JVM 的可用核心數(shù)來(lái)影響 Gradle 構(gòu)建。

這個(gè)方法適用于進(jìn)程常駐的場(chǎng)景,避免資源被某個(gè) Docker 實(shí)例無(wú)限占用。例如 Web 服務(wù)的常駐進(jìn)程,若不限制資源,當(dāng)程序存在 Bug 或出現(xiàn)大量請(qǐng)求時(shí),JVM 會(huì)不斷向操作系統(tǒng)申請(qǐng)資源,最終進(jìn)程會(huì)被 Kubernetes 或操作系統(tǒng)殺死。

設(shè)置 -XX:-UseContainerSupport

圖片

根據(jù) Oracle 官方文檔和源碼,通過(guò)顯式設(shè)置 -XX:-UseContainerSupport 可以禁用容器化,不再通過(guò) Docker 容器相關(guān)的配置信息來(lái)設(shè)置 CPU 數(shù),而是直接查詢(xún)操作系統(tǒng)來(lái)設(shè)置。

這個(gè)方法適用于構(gòu)建任務(wù)耗時(shí)不長(zhǎng)的場(chǎng)景,應(yīng)最大程度調(diào)度資源快速完成構(gòu)建任務(wù)。目前 CI 上均為短時(shí)間的構(gòu)建任務(wù),當(dāng)任務(wù)完成后,Docker 實(shí)例會(huì)視情況進(jìn)行緩存或銷(xiāo)毀,資源也會(huì)被釋放。

選擇的參數(shù)

對(duì)于 CI 構(gòu)建,雖然可以查詢(xún)物理機(jī)的可用核心數(shù),然后設(shè)置-XX:ActiveProcessorCount。但這里根據(jù)使用場(chǎng)景,選擇了設(shè)置更簡(jiǎn)單的 -XX:-UseContainerSupport 來(lái)提升構(gòu)建性能。

2.怎么設(shè)置參數(shù)

通過(guò)命令行設(shè)置

這個(gè)是最先想到的方法,但執(zhí)行命令 "./gradlew clean, app:lark-application:assembleProductionChinaRelease -Dorg.gradle.jvmargs=-Xms12g -Xss4m -XX:-UseContainerSupport" 后有意外發(fā)現(xiàn)。雖然 OsAvailableProcessors 字段和 ByteXTransform 的耗時(shí)恢復(fù)正常;但構(gòu)建整體仍然是單線程且 DexBuilder 的耗時(shí)也沒(méi)回落。

這個(gè)和 Gradle 的構(gòu)建機(jī)制有關(guān)。

  • 執(zhí)行上面的命令時(shí)會(huì)觸發(fā) GradleWrapperMain#main 方法啟動(dòng) GradleWrapperMain 進(jìn)程(下面簡(jiǎn)稱(chēng) wrapper 進(jìn)程)
  • wrapper 進(jìn)程會(huì)解析 org.gradle.jvmargs 屬性,然后通過(guò) Socket 傳遞給 Gradle Daemon 進(jìn)程(下面簡(jiǎn)稱(chēng) daemon 進(jìn)程),所以上面的 -XX:-UseContainerSupport 只對(duì) daemon 進(jìn)行有效,對(duì) wrapper 進(jìn)程無(wú)效,同時(shí) wrapper 進(jìn)程也會(huì)初始化DefaultParallelismConfiguration#maxWorkerCount 然后傳給 daemon 進(jìn)程
  • daemon 進(jìn)程禁用了容器化,所以能通過(guò) API 獲取到正確的核心數(shù),從而正確顯示 OsAvailableProcessors 字段和并發(fā)執(zhí)行 ByteXTransform;但 wrapper 進(jìn)程沒(méi)有禁用容器化,所以獲取的核心數(shù)是 1 ,傳給 daemon 進(jìn)程后導(dǎo)致構(gòu)建整體和 DexBuilder 都是單線程執(zhí)行。

圖片

圖片

圖片

這里有個(gè)不好理解的點(diǎn)是 ByteXTransform 和 DexBuilder 都是 daemon 進(jìn)程中執(zhí)行的 Task,為什么 ByteXTransform 恢復(fù)正常了,而 DexBuilder 沒(méi)有?

因?yàn)?ByteXTransform 內(nèi)部主動(dòng)調(diào)了 API ,能獲取到正確的核心數(shù),所以 ByteXTransform 可以并發(fā)執(zhí)行;但 DexBuilder 受 Gradle Worker API (詳見(jiàn)相關(guān)資料)的調(diào)度,執(zhí)行時(shí)的 maxWorkers 是被動(dòng)設(shè)置的(wrapper 進(jìn)程傳給 daemon 進(jìn)程的)。如果通過(guò) -XX:ActiveProcessorCount=[count] 給 wrapper 進(jìn)程指定核心數(shù),然后斷點(diǎn),會(huì)發(fā)現(xiàn) maxWorkers = count 。所以當(dāng) wrapper 進(jìn)程沒(méi)有禁用容器化時(shí),獲取的核心數(shù)是 1,DexBuilder 會(huì)單線程執(zhí)行,因而沒(méi)有恢復(fù)正常。

圖片

圖片

上面引出來(lái)的一個(gè)點(diǎn)是既然構(gòu)建整體和 DexBuilder 都受 Gradle Worker API 調(diào)度,為什么之前在 CI 上執(zhí)行“./gradlew clean, app:lark-application:assembleProductionChinaRelease --max-workers=96”時(shí),構(gòu)建整體恢復(fù)了并發(fā),但 DexBuilder 仍然沒(méi)有恢復(fù)正常?

因?yàn)?DexBuilder 的并發(fā)度除了受 maxWorkers 影響,還受 numberOfBuckets 的影響。

對(duì)于 Release 包,DexBuilder 的輸入是上游 MinifyWithProguard (不是MinifyWithR8,因?yàn)轱@式關(guān)閉了R8)的輸出(minified.jar),minified.jar 會(huì)分成 numberOfBuckets 個(gè) ClassBucket,每個(gè) ClassBucket 會(huì)作為 DexWorkActionParams 的一部分設(shè)置給 DexWorkAction,最后把 DexWorkAction 提交給 WorkerExecutor 分配的線程完成 Class 到 DexArchive 的轉(zhuǎn)換

圖片

圖片

圖片

默認(rèn)情況下,numberOfBuckets = DexArchiveBuilderTask#DEFAULT_NUM_BUCKETS = Math.max(12 / 2, 1) = 6

圖片

雖然通過(guò) --max-workers 把 DexBuilder 的 maxWorkers 設(shè)置成了12,但由于 daemon 進(jìn)程默認(rèn)開(kāi)啟了容器化,通過(guò) Runtime.getRuntime().availableProcessors() 獲取的可用核心數(shù)是 1,因此 numberOfBuckets 并不是預(yù)期的 6 而是 1,所以轉(zhuǎn) dex 時(shí)不能把 Class 分組然后并發(fā)處理,導(dǎo)致 DexBuilder 的耗時(shí)沒(méi)有恢復(fù)正常。CI 上也是一樣的邏輯,numberOfBuckets 從 48 變成了 1,極大的降低了并發(fā)度。

圖片

所以要讓構(gòu)建整體恢復(fù)并發(fā),讓DexBuilder 的耗時(shí)恢復(fù)正常,還需要讓 daemon進(jìn)程接收的 maxWorkers 恢復(fù)正常,即讓wrapper 進(jìn)程獲取到正確的核心數(shù)。通過(guò)給工程根目錄下的 gradlew 腳本設(shè)置 DEFAULT_JVM_OPTS 可以達(dá)到這個(gè)效果。

圖片

所以最終執(zhí)行如下構(gòu)建命令時(shí),wrapper 進(jìn)程和 daemon 進(jìn)程都能通過(guò) API 獲取到正確的核心數(shù),從而讓構(gòu)建整體、ByteXTransform、DexBuilder、OsAvailableProcessors 字段顯示都恢復(fù)正常。

圖片

但上面的命令在 CI Docker 容器中執(zhí)行時(shí)正常,在本地 Mac 執(zhí)行時(shí)會(huì)報(bào)無(wú)法識(shí)別 UseContainerSupport。通過(guò)判斷構(gòu)建機(jī)器和環(huán)境(本地 Mac,CI Linux 原生環(huán)境,CI Docker 容器)動(dòng)態(tài)設(shè)置參數(shù)可以解這個(gè)問(wèn)題,但顯然比較麻煩。

圖片

通過(guò)環(huán)境變量設(shè)置

后來(lái)發(fā)現(xiàn)環(huán)境變量 JAVA_TOOL_OPTIONS 在創(chuàng)建 JVM 時(shí)就會(huì)檢測(cè),簡(jiǎn)單設(shè)置后對(duì) wrapper 進(jìn)程和 daemon 進(jìn)程都有效,也可以解決上面所有的問(wèn)題。

圖片

選擇的設(shè)置方法

對(duì)比上面兩種設(shè)置方法,這里選擇了更簡(jiǎn)單的即通過(guò)環(huán)境變量來(lái)設(shè)置 -XX:-UseContainerSupport。

3.新老分支同時(shí)可用

由于飛書(shū)自身的業(yè)務(wù)特點(diǎn),老分支也需要長(zhǎng)期維護(hù),老分支上存在和 JDK 11 不兼容的構(gòu)建邏輯,為了新老分支都能正常出包,需要?jiǎng)討B(tài)設(shè)置構(gòu)建用的 JDK 版本。

另外 UseContainerSupport 是 JDK 8u191 引入的(也就是說(shuō)高版本的 JDK 8 也有上面的問(wèn)題,教育團(tuán)隊(duì)升 AGP 4.1.0 時(shí)把 JDK 升到了 1.8.0_332,就遇到上面的問(wèn)題),直接設(shè)置給 JDK 1.8.0_131 會(huì)無(wú)法識(shí)別,導(dǎo)致無(wú)法創(chuàng)建 JVM。

圖片

所以飛書(shū)最終的解決方案是根據(jù)分支動(dòng)態(tài)設(shè)置構(gòu)建用的 JDK 版本,并且只在使用 JDK 11 時(shí)顯式設(shè)置JAVA_TOOL_OPTIONS 為 -XX:-UseContainerSupport。對(duì)于其他團(tuán)隊(duì),如果老分支用 JDK 11 也能正常構(gòu)建,可以選擇默認(rèn)使用 JDK 11 且內(nèi)置了該環(huán)境變量的 Docker 鏡像,無(wú)需修改構(gòu)建邏輯。

六、效果

06-30 22點(diǎn)以后合入了修改,07-01 的構(gòu)建整體耗時(shí)明顯下降,恢復(fù)到了 06-13(合入了 JDK 11 的升級(jí))之前的水平,ByteXTransform 和 DexBuilder 的耗時(shí)也回落到了之前的水平,構(gòu)建指標(biāo)恢復(fù)正常,OsAvailableProcessors 字段也恢復(fù)正常,GC 情況恢復(fù)正常,世界又清靜了。

圖片

圖片

圖片

圖片

圖片

七、總結(jié)

雖然最后解決了構(gòu)建性能劣化的問(wèn)題,但在整個(gè)引入問(wèn)題-->發(fā)現(xiàn)問(wèn)題-->分析問(wèn)題的流程中還是有不少點(diǎn)可以改進(jìn)。比如對(duì)基礎(chǔ)構(gòu)建工具(包括Gradle、AGP、Kotlin、JDK)變更進(jìn)行更充分的測(cè)試可以事前發(fā)現(xiàn)問(wèn)題,完善的防劣化機(jī)制可以有效攔截問(wèn)題,有區(qū)分度的監(jiān)控報(bào)警可以及時(shí)發(fā)現(xiàn)劣化,強(qiáng)大的自動(dòng)歸因機(jī)制可以給分析問(wèn)題提供更多輸入,后面會(huì)持續(xù)完善這些方面來(lái)提供更好的研發(fā)體驗(yàn)。

責(zé)任編輯:未麗燕 來(lái)源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2020-06-05 07:20:41

測(cè)試自動(dòng)化環(huán)境

2010-01-07 11:21:25

2021-10-09 20:21:55

微軟Windows 11Windows

2021-10-18 22:42:54

Windows 11操作系統(tǒng)微軟

2021-10-09 08:57:46

Windows 11操作系統(tǒng)微軟

2022-02-22 09:00:00

軟件開(kāi)發(fā)CI/CD 管道工具

2009-06-30 16:08:19

性能問(wèn)題代碼寫(xiě)法

2020-11-11 10:00:13

NAT性能內(nèi)核

2024-04-10 07:16:17

JDBC驅(qū)動(dòng)MySQL數(shù)據(jù)庫(kù)

2023-02-19 15:28:39

CI/CD 管道集成開(kāi)發(fā)

2013-09-30 09:18:39

2013-06-20 09:59:12

Javascriptvar

2022-12-16 13:16:47

2021-11-11 20:49:22

數(shù)字化

2010-02-04 15:01:07

Android架構(gòu)

2021-09-16 10:25:38

Java 17開(kāi)發(fā)者回收器

2010-02-04 10:27:33

Android DDM

2023-08-18 10:24:52

GitLabCI 流水線

2021-03-23 14:59:37

GoogleAndroidWebView
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)