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

飛書 Android 升級 JDK 11 引發的 CI 構建性能問題

原創 精選
移動開發 Android
本文從飛書 Android 升級 JDK 11 意外引發的 CI 構建性能劣化談起,結合高版本 JDK 在 Docker 容器和 GC 方面的新特性,深挖 JVM 和 Gradle 的源碼實現,抽絲剝繭地介紹了分析過程和修復方法,供其他升級 JDK 的團隊參考。

作者|秦兵兵 & 宋志陽

一、摘要

本文從飛書 Android 升級 JDK 11 意外引發的 CI 構建性能劣化談起,結合高版本 JDK 在 Docker 容器和 GC 方面的新特性,深挖 JVM 和 Gradle 的源碼實現,抽絲剝繭地介紹了分析過程和修復方法,供其他升級 JDK 的團隊參考。

二、背景

最近飛書適配 Android 12 時把 targetSdkVersion 和 compileSdkVersion 改成了 31,改完后遇到了如下的構建問題。

圖片

在 StackOverflow 上有不少人遇到同樣的問題,簡單無侵入的解決方案是把構建用的 JDK 版本從 8 升到 11。

圖片

飛書目前用的 AGP 是 4.1.0,考慮到將來升級 AGP 7.0 會強制要求 JDK 11,而且新版 AS 已經做了鋪墊,所以就把構建用的 JDK 版本也升到了 11。

圖片

三、問題

升級后不少同學反饋子倉發組件(即發布 AAR)很慢,看大盤指標確實上漲了很多。

圖片

除了子倉發組件指標明顯上升,每周例行分析指標時發現主倉打包指標也明顯上升,從 17m上升到了 26m,漲幅約 50%。

圖片

四、分析

1.主倉打包和子倉發組件變成了單線程

子倉發組件指標和主倉打包指標,都在 06-17 劣化到了峰值,找了 06-17 主倉打包最慢的 10 次構建進行分析。

圖片

初步分析就有一個大發現:10 次構建都是單線程。

圖片

而之前正常的構建是并發的

圖片

子倉發組件的情況也一樣,由并發發布變成了單線程發布。

圖片

圖片

2.并發變單線程和升級 JDK 有關

查了下并發構建相關的屬性,org.gradle.parallel 一直為 true,并沒有更改。然后對比機器信息,發現并發構建用的是JDK 8,可用核心數是 96;單線程構建用的是 JDK 11,可用核心數是 1。初步分析,問題應該就在這里,從 JDK 8 升到 JDK 11 后,由并發構建變成了單線程構建,導致耗時明顯上升。而且升級 JDK 11 的修改是在 06-13 合入主干的,06-14 構建耗時明顯上升,時間上吻合。

圖片

圖片

3.整體恢復了并發,但指標沒下降

為了恢復并發構建,容易聯想到另一個相關的屬性 org.gradle.workers.max。

圖片

由于 PC 和服務器可用核心數有差異,為了不寫死,就試著在 CI 打包時動態指定了 --max-workers 參數。設置參數后主倉打包恢復了并發構建,子倉發組件也恢復了并發。

圖片

但觀察了一周大盤指標后,發現構建耗時并沒有明顯的回落,穩定在 25 m,遠高于之前 17 m的水平。

圖片

4.重點 Task 的耗時沒下降

細化分析,發現 ByteXTransform(ByteX是字節推出的基于 AGP Transform 的開源字節碼處理框架,通過把多個串行執行重復 IO 的 Transform 整合成一個 Transform 和并發處理 Class來優化 Transform 性能,詳見相關資料)和 DexBuilder 的走勢和構建整體的走勢一致,06-21 后都維持在高位,沒有回落。ByteXTransform 劣化了約 200 s,DexBuilder 劣化了約 200 s,而且這兩個 Task 是串行執行,合在一起劣化了約 400 s,接近構建整體的劣化9 m。GC 情況在 06-21 后也沒有好轉。

圖片

圖片

圖片

5.獲取 CPU 核心數的 API 有變化

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

圖片

調試 DexBuilder 時發現核心邏輯 convertToDexArchive 也是并發執行。

圖片

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

圖片

ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors() 和Runtime.getRuntime().availableProcessors() 的效果一樣,底層也是 Native 方法。綜上推斷,可能是 JDK 11 的 Native 實現導致了獲取核心數的 API 都返回了 1,從而導致雖然構建整體恢復了并發,但依賴 API 進行并發設置的 ByteXTransform 和 DexBuilder 仍然有問題,進而導致這兩個 Task 的耗時一直沒有回落。

圖片

直接在 .gradle 腳本中調用這兩個 API 驗證上面的推斷,發現返回的核心數果然從 96 變成了 1。

圖片

圖片

另外有同學發現并不是所有的 CI 構建都發生了劣化,只有用 Docker 容器的 CI 構建發生了明顯的劣化,而 Linux 原生環境下的構建正常。所以獲取核心數的 Native 實現可能和 Docker 容器有關。

GC 劣化推斷也是同樣的原因。下面用 -XX:+PrintFlagsFinal 打印所有的 JVM 參數來驗證推斷??梢钥吹絾尉€程構建用的是 SerialGC,GC 變成了單線程,沒能利用多核優勢,GC 耗時占比高。并發構建用的是 G1GC,而且 ParallelGCThreads = 64,ConcGCThreads = 16(約是 ParallelGCThreads 的 1/4),GC 并發度高,兼顧 Low Pause 和 High Throughput,GC 耗時占比自然就低。

// 單線程構建時 GC 相關的參數值
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}
// 并發構建時 GC 相關的參數值
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 獲取可用核心數的 Native 實現,由于 AS 默認使用 OpenJDK,這里就用OpenJDK 的源碼進行分析。

JDK 8 實現

圖片

JDK 11 實現

圖片

JDK 11 默認沒有設置可用核心數并開啟了容器化,所以可用核心數由 OSContainer::active_processor_count() 決定。

查詢 Docker 環境下的 CPU 參數并代入計算邏輯,很容易得出可用核心數是 1,從而導致 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

圖片

五、修復

1.設置相關的 JVM 參數

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

從源碼看,修復該問題主要有兩種辦法:

圖片

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

設置 -XX:-UseContainerSupport,讓 JVM 禁用容器化

設置 -XX:ActiveProcessorCount=[count]

圖片

根據 Oracle 官方文檔和源碼,可以指定 JVM 的可用核心數來影響 Gradle 構建。

這個方法適用于進程常駐的場景,避免資源被某個 Docker 實例無限占用。例如 Web 服務的常駐進程,若不限制資源,當程序存在 Bug 或出現大量請求時,JVM 會不斷向操作系統申請資源,最終進程會被 Kubernetes 或操作系統殺死。

設置 -XX:-UseContainerSupport

圖片

根據 Oracle 官方文檔和源碼,通過顯式設置 -XX:-UseContainerSupport 可以禁用容器化,不再通過 Docker 容器相關的配置信息來設置 CPU 數,而是直接查詢操作系統來設置。

這個方法適用于構建任務耗時不長的場景,應最大程度調度資源快速完成構建任務。目前 CI 上均為短時間的構建任務,當任務完成后,Docker 實例會視情況進行緩存或銷毀,資源也會被釋放。

選擇的參數

對于 CI 構建,雖然可以查詢物理機的可用核心數,然后設置-XX:ActiveProcessorCount。但這里根據使用場景,選擇了設置更簡單的 -XX:-UseContainerSupport 來提升構建性能。

2.怎么設置參數

通過命令行設置

這個是最先想到的方法,但執行命令 "./gradlew clean, app:lark-application:assembleProductionChinaRelease -Dorg.gradle.jvmargs=-Xms12g -Xss4m -XX:-UseContainerSupport" 后有意外發現。雖然 OsAvailableProcessors 字段和 ByteXTransform 的耗時恢復正常;但構建整體仍然是單線程且 DexBuilder 的耗時也沒回落。

這個和 Gradle 的構建機制有關。

  • 執行上面的命令時會觸發 GradleWrapperMain#main 方法啟動 GradleWrapperMain 進程(下面簡稱 wrapper 進程)
  • wrapper 進程會解析 org.gradle.jvmargs 屬性,然后通過 Socket 傳遞給 Gradle Daemon 進程(下面簡稱 daemon 進程),所以上面的 -XX:-UseContainerSupport 只對 daemon 進行有效,對 wrapper 進程無效,同時 wrapper 進程也會初始化DefaultParallelismConfiguration#maxWorkerCount 然后傳給 daemon 進程
  • daemon 進程禁用了容器化,所以能通過 API 獲取到正確的核心數,從而正確顯示 OsAvailableProcessors 字段和并發執行 ByteXTransform;但 wrapper 進程沒有禁用容器化,所以獲取的核心數是 1 ,傳給 daemon 進程后導致構建整體和 DexBuilder 都是單線程執行。

圖片

圖片

圖片

這里有個不好理解的點是 ByteXTransform 和 DexBuilder 都是 daemon 進程中執行的 Task,為什么 ByteXTransform 恢復正常了,而 DexBuilder 沒有?

因為 ByteXTransform 內部主動調了 API ,能獲取到正確的核心數,所以 ByteXTransform 可以并發執行;但 DexBuilder 受 Gradle Worker API (詳見相關資料)的調度,執行時的 maxWorkers 是被動設置的(wrapper 進程傳給 daemon 進程的)。如果通過 -XX:ActiveProcessorCount=[count] 給 wrapper 進程指定核心數,然后斷點,會發現 maxWorkers = count 。所以當 wrapper 進程沒有禁用容器化時,獲取的核心數是 1,DexBuilder 會單線程執行,因而沒有恢復正常。

圖片

圖片

上面引出來的一個點是既然構建整體和 DexBuilder 都受 Gradle Worker API 調度,為什么之前在 CI 上執行“./gradlew clean, app:lark-application:assembleProductionChinaRelease --max-workers=96”時,構建整體恢復了并發,但 DexBuilder 仍然沒有恢復正常?

因為 DexBuilder 的并發度除了受 maxWorkers 影響,還受 numberOfBuckets 的影響。

對于 Release 包,DexBuilder 的輸入是上游 MinifyWithProguard (不是MinifyWithR8,因為顯式關閉了R8)的輸出(minified.jar),minified.jar 會分成 numberOfBuckets 個 ClassBucket,每個 ClassBucket 會作為 DexWorkActionParams 的一部分設置給 DexWorkAction,最后把 DexWorkAction 提交給 WorkerExecutor 分配的線程完成 Class 到 DexArchive 的轉換

圖片

圖片

圖片

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

圖片

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

圖片

所以要讓構建整體恢復并發,讓DexBuilder 的耗時恢復正常,還需要讓 daemon進程接收的 maxWorkers 恢復正常,即讓wrapper 進程獲取到正確的核心數。通過給工程根目錄下的 gradlew 腳本設置 DEFAULT_JVM_OPTS 可以達到這個效果。

圖片

所以最終執行如下構建命令時,wrapper 進程和 daemon 進程都能通過 API 獲取到正確的核心數,從而讓構建整體、ByteXTransform、DexBuilder、OsAvailableProcessors 字段顯示都恢復正常。

圖片

但上面的命令在 CI Docker 容器中執行時正常,在本地 Mac 執行時會報無法識別 UseContainerSupport。通過判斷構建機器和環境(本地 Mac,CI Linux 原生環境,CI Docker 容器)動態設置參數可以解這個問題,但顯然比較麻煩。

圖片

通過環境變量設置

后來發現環境變量 JAVA_TOOL_OPTIONS 在創建 JVM 時就會檢測,簡單設置后對 wrapper 進程和 daemon 進程都有效,也可以解決上面所有的問題。

圖片

選擇的設置方法

對比上面兩種設置方法,這里選擇了更簡單的即通過環境變量來設置 -XX:-UseContainerSupport。

3.新老分支同時可用

由于飛書自身的業務特點,老分支也需要長期維護,老分支上存在和 JDK 11 不兼容的構建邏輯,為了新老分支都能正常出包,需要動態設置構建用的 JDK 版本。

另外 UseContainerSupport 是 JDK 8u191 引入的(也就是說高版本的 JDK 8 也有上面的問題,教育團隊升 AGP 4.1.0 時把 JDK 升到了 1.8.0_332,就遇到上面的問題),直接設置給 JDK 1.8.0_131 會無法識別,導致無法創建 JVM。

圖片

所以飛書最終的解決方案是根據分支動態設置構建用的 JDK 版本,并且只在使用 JDK 11 時顯式設置JAVA_TOOL_OPTIONS 為 -XX:-UseContainerSupport。對于其他團隊,如果老分支用 JDK 11 也能正常構建,可以選擇默認使用 JDK 11 且內置了該環境變量的 Docker 鏡像,無需修改構建邏輯。

六、效果

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

圖片

圖片

圖片

圖片

圖片

七、總結

雖然最后解決了構建性能劣化的問題,但在整個引入問題-->發現問題-->分析問題的流程中還是有不少點可以改進。比如對基礎構建工具(包括Gradle、AGP、Kotlin、JDK)變更進行更充分的測試可以事前發現問題,完善的防劣化機制可以有效攔截問題,有區分度的監控報警可以及時發現劣化,強大的自動歸因機制可以給分析問題提供更多輸入,后面會持續完善這些方面來提供更好的研發體驗。

責任編輯:未麗燕 來源: 字節跳動技術團隊
相關推薦

2020-06-05 07:20:41

測試自動化環境

2010-01-07 11:21:25

2021-10-18 22:42:54

Windows 11操作系統微軟

2021-10-09 20:21:55

微軟Windows 11Windows

2021-10-09 08:57:46

Windows 11操作系統微軟

2009-06-30 16:08:19

性能問題代碼寫法

2022-02-22 09:00:00

軟件開發CI/CD 管道工具

2020-11-11 10:00:13

NAT性能內核

2024-04-10 07:16:17

JDBC驅動MySQL數據庫

2023-02-19 15:28:39

CI/CD 管道集成開發

2013-09-30 09:18:39

2013-06-20 09:59:12

Javascriptvar

2022-12-16 13:16:47

2010-02-04 15:01:07

Android架構

2017-12-28 10:27:28

2010-02-04 10:27:33

Android DDM

2021-11-11 20:49:22

數字化

2021-03-23 14:59:37

GoogleAndroidWebView

2023-08-18 10:24:52

GitLabCI 流水線
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 高清视频一区二区三区 | 伊人超碰| 欧美三级三级三级爽爽爽 | 一区二区三区视频在线观看 | 成人精品久久日伦片大全免费 | 福利视频网站 | 九九亚洲精品 | 日韩一区二区福利视频 | 国产成人精品一区二区三区网站观看 | 日韩视频精品在线 | 天天躁人人躁人人躁狂躁 | 欧美一区二区三区视频在线 | 日韩精品一区二区三区视频播放 | 国产精品久久精品 | 亚洲国产一区二区视频 | 国产精彩视频在线观看 | 一区二区在线视频 | 欧美一区二区三区国产精品 | 欧美激情五月 | 亚洲日本欧美日韩高观看 | 日韩一区精品 | 国产精品成人免费 | 亚洲成年影院 | 拍戏被cao翻了h承欢 | 中文字幕在线观看精品 | 狠狠操你 | 国偷自产av一区二区三区 | 国产在线91| 综合五月 | 亚洲国产精品一区 | 99精品免费 | 精品天堂| 国产精品久久久久久久一区二区 | 99tv| 在线观看日本网站 | 一二三区视频 | a黄在线观看| 中文字幕第一页在线 | 一二区成人影院电影网 | 亚洲成人一区 | 一区二区在线不卡 |