经费到手,工作室终于有了一台作为开发支持的服务器。

这是早在六月份的事情,其它组使用服务器已有两个月之久,但直到最近,我们组的 CI/CD 服务才被建立起来。

原因无它,太麻烦了。

早在代码托管于 GitHub 时,通过 GitHub Action 实现 CI/CD 便是一段回想就觉得心累的记忆。现在的业务代码从 GitHub 转存到自建的 GitLab 服务中,好在 GitHub 上的服务已经成功运行,抽空根据官方文档搭建 CI/CD 服务,难度应该不大。

然而,理想与现实,分立于银河的两端。

Docker

搭基础环境,就不是很顺利了。

原本构建采用的基础镜像是 adoptopenjdk/openjdk8:debian。结果在运行 30.0.1 版本 Build Tools 中的 apksigner 对 APK 进行签名时,抛出了如下异常:

Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/android/apksigner/ApkSignerTool has been compiled by a more recent version of the Java Runtime (class file version 53.0), this version of the Java Runtime only recognizes class file versions up to 52.0
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

鉴于 Android 不少工具无法作业于高版本的 Java,甚至 Android Framework 都未能提供对 Java 8 完整的支持,apksigner 最低运行环境为 Java 9 着实有种见了鬼的感觉。

好在这并不是一个多难解决的问题,在基础镜像更改为 adoptopenjdk/openjdk11:debian 后,apksigner 成功运行。

Download

内网拉取镜像的速度明显要快于从 Google 下载 Android SDK 的速度,为了减少 CI/CD 的运行时间,Docker 镜像在构建时便提前预置了 SDK,但这么做也有弊端,新旧项目采用 SDK 版本不尽相同,所需构建镜像的数量也就不可避免地多了起来,加上包含 SDK 的镜像数据量都不小,每次上传镜像,运维老哥总怀疑我把服务器当网盘使。

至少,运行环境完成了。测试用项目开始了第一次 CI。

Downloading https://services.gradle.org/distributions/gradle-6.1.1-all.zip
...

傻了。

哪怕出门溜个弯回来,省略号仍旧只有那零星的几个点。试用 wget 下载,提示接近四个小时的下载时间让我们怀疑人生。基本只有在某云才能见到可与之一拼的速度。

因为 Gradle 的更新速度较快,集成到镜像里并不现实。

最终采用的处理方式是,在内网 Nexus 服务中创建 Gradle Distributions 的代理仓库,并手动上传组内项目所需几个 Gradle 版本的缓存,当代理仓库缓存未命中时才从原地址下载 Gradle。

之后 .gitlab-ci.yml 文件中手动将 gradle-wrapper.properties 中的链接替换为代理仓库的链接。

before_script:
  ...
  - sed -i -e "s/https\\\:\/\/services\.gradle\.org\/distributions/<Studio Proxy Repository Path>/g" gradle/wrapper/gradle-wrapper.properties
  ...

Gradle

在经过了各种调整后,第一个 Pipeline 终于跑通。

耗时 1:37:43。

( _ _)ノ|

如果单纯作为自动化流程,对于服务器性能暂时存在盈余的我们来说并不是什么大问题,但作为持续集成服务,在这样的耗时下保证持续,不存在的。

查看日志发现缓存并没有生效。Gradle 的缓存虽然在 Job 中有成功上传,但 Job 在重新执行时缓存并没有下载。后来问运维老哥得知,因为 GitLab 与相关环境基于 Kubernetes,每次执行 Job 都会创建一组新的容器,因此在单纯在 .gitlab-ci.yml 内设置缓存无效。

最后采用的方式是通过 minIO 服务实现缓存。除此之外,项目依赖的下载源也通过 Nexus 进行代理,提高下载速度。

至此解决 Gradle 的构建速度问题,平均每个 Job 的执行时间下降到 2 分钟左右,Pipeline 总时间不超过 10 分钟。