sccache 是由 mozilla 开发的编译缓存工具,设计思路是作为一个编译的 wrapper,把编译产物放在保存到存储后端上,尽可能避免重复编译,从而加快整体的编译速度。sccache 支持的编译后端包括 gcc, clang, MSVC, rustc, NVCC 等等。以 Rust 编译为例,可以这样来使用它:
export RUSTC_WRAPPER=/path/to/sccache
cargo build
最近社区提出有没有办法加快 databend 的编译速度,我想到了可以使用 sccache 来加速,优势如下:
- Databend 依赖众多,而大部分依赖很少会变化,他们都能够被很好的缓存下来
- Databend 的 dev 构建跑在 AWS 的 Self-hosted Runner 上,而且本身就已经配置好了内网的 S3 bucket
开始踩坑
各方面条件都很完善,看来只需要写一个 Github Action 就好了。事实证明我太乐观了:
主要坑的地方有两块:
- 为了方便测试环境的维护,Databend CI 统一使用了 build-tools 镜像来跑测试,搭配 sccache 起来需要额外做不少配置
- 云平台出于安全考虑启用了基于节点 Role 的认证,没有配置 AK/SK,sccache 没有支持 IMDSv2
第一个问题通过不停地骚扰 @everpcpc 得到了解决,后面这个问题相对更麻烦。我们首先给 sccache 提交了一个 Issue: Support IMDSv2 for AWS IAM Role authentication,然后开始尝试自己解决问题。
调试 sccache
调试 sccache 的过程非常痛苦:sccache 是一个 CS 的架构,用户每次调用 sccache 的时候都会在后台启动一个 server,通过 client 和 server 之间的通信来决定是否使用缓存。所有跟存储后端的交互都是由 server 来执行的,在编译过程中没法直接看到日志,只能把日志重定向到某个文件,然后看文件的输出。此外 sccache 的历史比较悠久,很多的地方都不太利于调试,比如:
let res = self
.client
.execute(request)
.await
.with_context(move || format!("failed GET: {}", url))?;
if res.status().is_success() {
let body = res.bytes().await.context("failed to read HTTP body")?;
info!("Read {} bytes from {}", body.len(), url2);
Ok(body.into_iter().collect())
} else {
Err(BadHttpStatusError(res.status()).into())
}
请求失败的时候只会打出状态码,没有实际错误内容,在调试 IMDSv2 时走了很多弯路。为了尽快解决问题,我直接把 sccache fork 了过来,把底层的存储修改成了使用 opendal:
- let credentials = self
- .provider
- .credentials()
- .await
- .context("failed to get AWS credentials")?;
-
- let bucket = self.bucket.clone();
- let _ = bucket
- .put(&key, data, &credentials)
- .await
- .context("failed to put cache entry in s3")?;
+ self.op.object(&key).write(data).await?;
这下舒服多了,很快定位到了问题并修复掉,现在 Databend 已经成功使用了 sccache 了。在安装好 sccache 之后,只需要配置如下:
- name: Setup Build Tool
uses: ./.github/actions/setup_build_tool
with:
image: ${{ inputs.target }}
bypass_env_vars: RUSTFLAGS,RUST_LOG,RUSTC_WRAPPER,SCCACHE_BUCKET,SCCACHE_S3_KEY_PREFIX,SCCACHE_S3_USE_SSL,AWS_DEFAULT_REGION,AWS_REGION,AWS_ROLE_ARN,AWS_STS_REGIONAL_ENDPOINTS,AWS_WEB_IDENTITY_TOKEN_FILE
- name: Build Debug
shell: bash
run: cargo -Z sparse-registry build --target ${{ inputs.target }}
env:
RUSTC_WRAPPER: /opt/rust/cargo/bin/sccache
SCCACHE_BUCKET: databend-ci
SCCACHE_S3_KEY_PREFIX: cache/
SCCACHE_S3_USE_SSL: true
总结
这里记录一些比较简单的数据:首次全量编译的时候需要花费一些额外的时间上传编译产物,后续的编译就可以复用这些产物,不再需要编译了。
- 没有任何 cache 的时候: 6m 20s
- 首次编译
Finished dev [unoptimized + debuginfo] target(s) in 9m 04s
- 第二次编译
Finished dev [unoptimized + debuginfo] target(s) in 5m 35s
- 去除 debug 日志后
Finished dev [unoptimized + debuginfo] target(s) in 4m 38s
这里的首次编译时间还需要考虑 sccache debug 日志对性能的影响,实际上应该不会增长那么多。不过 Databend 还是有很多地方没有办法缓存,后面可以看一些有没有能优化的地方。我给上游提交了一个 proposal: Use opendal to handle the cache storage operations,看看能不能把 sccache 的存储后端访问改为使用 opendal,这样对接存储和调试起来就更方便了~