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,这样对接存储和调试起来就更方便了~