全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-09-06_「转」飞书 Android 升级 JDK 11 引发的 CI 构建性能问题

您的位置:首页 >> 新闻 >> 行业资讯

飞书 Android 升级 JDK 11 引发的 CI 构建性能问题 动手点关注干货不迷路?? 一、摘要本文从飞书 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相关的参数值 boolUseG1GC=false{product}{default} boolUseParallelGC=false{product}{default} boolUseSerialGC=true{product}{ergonomic} uintParallelGCThreads=0{product}{default} uintConcGCThreads=0{product}{default} //并发构建时GC相关的参数值 boolUseG1GC=true{product}{ergonomic} boolUseParallelGC=false{product}{default} boolUseSerialGC=false{product}{default} uintParallelGCThreads=63{product}{default} uintConcGCThreads=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)变更进行更充分的测试可以事前发现问题,完善的防劣化机制可以有效拦截问题,有区分度的监控报警可以及时发现劣化,强大的自动归因机制可以给分析问题提供更多输入,后面会持续完善这些方面来提供更好的研发体验。 八、致谢在分析和修复问题的过程中,有不少同学提供线索、提出疑问、探讨修复方向,正是这些沟通和讨论促成了全面深入的归因和更优的解决方案,在此特别感谢这些同学。同时也特别感谢投稿后 Leader 的审核、评委的建议、技术学院和 PR 相关同学的支持。 九、相关资料https://stackoverflow.com/questions/68344424/unrecognized-attribute-name-module-class-com-sun-tools-javac-util-sharednametab/69250502#69250502 https://developer.android.com/studio/releases/gradle-plugin#jdk-11 https://github.com/bytedance/ByteX https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html https://docs.oracle.com/cd/E40972_01/doc.70/e40973/cnf_jvmgc.htm#autoId2 https://www.oracle.com/technical-resources/articles/java/g1gc.html https://docs.oracle.com/en/java/javase/11/gctuning/available-collectors.html#GUID-F215A508-9E58-40B4-90A5-74E29BF3BD3C https://github.com/openjdk/jdk/blob/jdk8-b01/hotspot/src/os/linux/vm/os_linux.cpp https://github.com/openjdk/jdk/blob/jdk-11%2B8/src/hotspot/os/linux/os_linux.cpp https://github.com/openjdk/jdk/blob/jdk-11%2B8/src/hotspot/os/linux/osContainer_linux.cpp https://www.oracle.com/java/technologies/javase/8u191-relnotes.html https://mp.weixin.qq.com/s/AU-7IuMnRN5GQ6mx5Pht7Q https://docs.oracle.com/en/java/javase/11/troubleshoot/environment-variables-and-system-properties.html#GUID-BE6E7B7F-A4BE-45C0-9078-AA8A66754B97

上一篇:2025-04-14_【招聘】岗位上新:PLTFRM 谱博、socialmanGo、Blank 下一篇:2023-04-01_「转」TPVFormer项目原作解读:面向自动驾驶场景的纯视觉三维语义占有预测

TAG标签:

15
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价