写 Rust 代码固然爽,但 Rust 的编译速度实在让人崩溃。在本地用多核工作站编译还好,一旦把代码提交到 CI/CD 流程里,面对 GitLab CI 那些性能捉急的共享 Runner 节点,稍微改动一行代码就要重新跑十分钟以上的编译。这让本来应该敏捷的敏捷开发直接变成了“摸鱼发呆”。
之前我们尝试用 GitLab 自带的 cache 机制来缓存 Rust 的 target 目录。然而,由于 Rust 编译产物体积极其庞大,打包和解包数个吉字节(GB)的 target 目录,本身就要花掉四五分钟,甚至经常遇到解压不完整导致的编译链损坏,真是折腾死人。后来我们干脆放弃了对 target 目录的直接打包,转向了 Mozilla 开发的共享编译缓存工具——sccache。
sccache 的基本原理类似 ccache,它是作为一个 rustc 的 wrapper(包装器)工作的。当 Rust 编译一个依赖包时,sccache 会对输入源文件和编译选项做哈希计算,如果发现之前已经编译过一模一样的东西,就直接从后端的对象存储(比如 AWS S3 或私有部署的 MinIO)里拉取已编译好的二进制产物。这样一来,CI 流程里的编译速度就可以快得飞起。
为了让 sccache 能够在 GitLab Runner 中完美运作,我们需要在 .gitlab-ci.yml 中进行环境变量的精准设置。下面是我们线上的完整构建配置:
stages:
- build
variables:
CARGO_HOME: "${CI_PROJECT_DIR}/.cargo"
# 告诉 Rust 用 sccache 作为编译器包装器
RUSTC_WRAPPER: "sccache"
# 配置 sccache 的后端存储为 S3
SCCACHE_BUCKET: "ci-build-cache"
SCCACHE_ENDPOINT: "https://minio.internal.company.com"
SCCACHE_S3_USE_SSL: "true"
AWS_ACCESS_KEY_ID: "deploy-runner-key"
AWS_SECRET_ACCESS_KEY: "super-secret-access-token"
# 避免 sccache 因为一些警告或者不影响运行的错误直接退出
SCCACHE_RETRY_DELAY: "2"
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .cargo/registry/index/
- .cargo/registry/cache/
- .cargo/git/db/
before_script:
# 安装 sccache。我们提前在镜像中打包好了,如果没有,则每次自动下载
- which sccache || (wget https://github.com/mozilla/sccache/releases/download/v0.7.7/sccache-v0.7.7-x86_64-unknown-linux-musl.tar.gz && tar -xzf sccache-v0.7.7-x86_64-unknown-linux-musl.tar.gz && mv sccache-v0.7.7-x86_64-unknown-linux-musl/sccache /usr/local/bin/)
# 启动 sccache 后台服务
- sccache --start-server
build-job:
stage: build
image: rust:1.77-slim
script:
- cargo build --release
# 打印编译缓存命中统计信息,方便排查缓存是否真正生效
- sccache --show-stats
after_script:
# 构建结束后,停止 sccache 服务以确保所有数据被刷入对象存储
- sccache --stop-server
配置这个方案时,我们吃过两个不小的亏。第一个是大坑是 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 绝对不要直接明文写在 .gitlab-ci.yml 文件中,必须放在 GitLab 的 CI/CD Variables 中进行保护和隐藏,否则任何有代码查看权限的人都能把你们的对象存储搞个精光。
第二个坑是,sccache 默认会在编译完成后将服务挂在后台,如果不主动调用 sccache --stop-server,可能会导致 GitLab 任务在编译完成后依然卡在 “running” 状态,直到超时被系统强杀。
通过把 sccache 接入到 MinIO,我们现在整个项目的 CI 编译时间从原本的 14 分钟降到了 1 分 50 秒,其中拉取缓存占了不到 10 秒钟。感觉以后再也不用为了改一个小 Bug 而在工位上发呆等构建完成了。你们团队的 CI 缓存是用什么方案搞定的?有更好的姿势也可以探讨下。