共享状态缓存(SSTATE)深度优化:配置 SSTATE_DIR、SSTATE_MIRRORS,利用预构建缓存加速增量构建

Yocto 的增量构建,说白了就是「只编译改动的部分」。但如果你做过几个项目就会知道——不改代码,只改个配置,经常也要重编大半个系统。为什么会这样?因为 BitBake 的依赖链太长了,一个 .bbappend 文件改了,可能触发几十个 recipe 重新解析。

这时候,SSTATE 缓存就是你的救命稻草。我习惯把它叫做「编译缓存加速器」。它能把编译好的 .so、.a、二进制文件打包存起来,下次同样的 task 直接解压,不用再跑一遍 gcc。

SSTATE 的核心原理:它到底缓存了什么?

每个 recipe 的每个 task(比如 do_compile、do_package)执行完后,BitBake 会计算一个 task hash。这个 hash 基于 recipe 内容、依赖、配置变量等一堆东西。下次构建时,如果 hash 没变,就直接从 SSTATE 缓存里拉取结果。

说白了,SSTATE 就是「task 级别的增量缓存」。不是文件级别的,是 task 级别的。粒度更粗,但命中率更高。

关键点:SSTATE 缓存的是 task 输出,不是整个 recipe 输出。每个 task 一个 .sstate 文件,命名格式为 ${SSTATE_PKGNAME}_${task}.tgz。

我在项目中遇到过一种情况:团队里有人改了全局的 DISTRO_FEATURES,结果所有 recipe 的 hash 都变了,SSTATE 全部失效。嗯,这就是典型的「缓存雪崩」。后面我会讲怎么避免。

配置 SSTATE_DIR:本地缓存放哪儿?

SSTATE_DIR 就是本地缓存目录。默认在 build/tmp/sstate-control 和 build/tmp/sstate-cache 里。但我不建议用默认路径——你想想看,每次 rm -rf tmp 就把缓存全清了,多浪费。

我个人的习惯是:

# 在 local.conf 中设置
SSTATE_DIR = "/opt/yocto/sstate-cache/${MACHINE}"

这样不同机器、不同架构的缓存分开存放,互不干扰。而且 /opt/yocto/ 放在独立分区,就算 build 目录删了,缓存还在。

小技巧:如果你有多个项目共用同一套 BSP,可以把 SSTATE_DIR 设成同一个路径。这样项目 A 编译过的包,项目 B 直接复用。我在公司就是这么干的,节省了至少 40% 的 CI 时间。

SSTATE_MIRRORS:团队共享缓存

本地缓存再大,也只是单机。团队协作时,每个人都从零开始编译一遍,那太傻了。SSTATE_MIRRORS 就是用来解决这个问题的——它允许你指定一个远程缓存源,BitBake 在本地没命中时,会去远程拉。

配置方式很简单:

# 在 local.conf 或 site.conf 中
SSTATE_MIRRORS = "\
    file://.* http://sstate-server.example.com/sstate/PATH \
    file://.* https://s3.amazonaws.com/my-bucket/sstate/PATH \
"

这里要注意:file://.* 是匹配本地缓存的 pattern,后面跟远程 URL。BitBake 会把本地路径替换成远程 URL 去下载。

我曾经踩过的坑:SSTATE_MIRRORS 的 URL 末尾一定要带 PATH 占位符。如果不带,BitBake 会拼接出错误的下载路径。比如 http://server/sstate/PATH 会被替换成 http://server/sstate/x86_64/xxx.sstate。少了 PATH,下载直接 404。

搭建 SSTATE 缓存服务器

远程缓存服务器可以用 Nginx 或 Apache 搭一个静态文件服务。我推荐用 Nginx,配置简单:

server {
    listen 80;
    server_name sstate-server.example.com;

    root /var/www/sstate-cache;
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;

    location / {
        try_files $uri $uri/ =404;
    }
}

然后把 SSTATE_DIR 里的文件 rsync 到服务器上:

rsync -avz --delete /opt/yocto/sstate-cache/ user@sstate-server:/var/www/sstate-cache/

配合 cron 定时同步,团队就能共享缓存了。

性能数据:在我之前的项目中,配置 SSTATE_MIRRORS 后,CI 构建时间从 45 分钟降到了 12 分钟。命中率大概在 70% 左右。剩下的 30% 是第一次构建的新包或 hash 变化的包。

SSTATE 的命中率优化:为什么缓存总是不命中?

缓存不命中,通常有这几个原因:

  • 全局变量变化:比如 TUNE_FEATURES、DISTRO_FEATURES 变了,所有 recipe 的 hash 都变。
  • 依赖链太长:一个底层库改了,上层所有依赖它的 recipe 都得重编。
  • 时间戳或路径差异:不同构建机的 TOPDIR 不同,可能导致 hash 不一致。

怎么优化?我总结了几个实战经验:

  1. 固定全局变量:把 DISTRO_FEATURES、MACHINE 等变量写死,不要在不同构建机之间变化。
  2. 使用 BB_HASHBASE_WHITELIST:把不影响输出的变量加入白名单,比如 TOPDIR、TMPDIR。这样不同路径的构建也能共享缓存。
  3. 拆分 recipe:把一个大 recipe 拆成多个小 recipe,减少依赖链长度。比如把 openssl 的编译和测试分开。
# 在 local.conf 中配置 hash 白名单
BB_HASHBASE_WHITELIST ?= "TOPDIR TMPDIR STAMPS_DIR SSTATE_DIR"

检查命中率的小工具:运行 bitbake -S printdiff <recipe> 可以查看 task hash 的变化原因。我经常用这个来排查「为什么缓存没命中」。

SSTATE 的清理策略:缓存太大怎么办?

SSTATE 缓存会越积越大。一个完整的嵌入式 Linux 镜像,SSTATE 缓存可能轻松超过 10GB。如果不清理,磁盘很快就满了。

我建议用以下策略:

策略 方法 适用场景
按时间清理 删除 30 天前的缓存文件 日常开发,旧版本很少回退
按大小清理 保留最近 10GB 的缓存 磁盘空间紧张
按 recipe 清理 只保留常用 recipe 的缓存 项目稳定期,很少改底层

清理脚本示例:

#!/bin/bash
# 删除 30 天前的 sstate 文件
find /opt/yocto/sstate-cache -name "*.tgz" -mtime +30 -delete
# 删除空目录
find /opt/yocto/sstate-cache -type d -empty -delete

配合 cron 每周执行一次:

0 3 * * 0 /usr/local/bin/cleanup-sstate.sh

SVG 流程图:SSTATE 缓存命中流程

下面这张图展示了 BitBake 在构建时如何查找 SSTATE 缓存:

SSTATE 缓存命中流程 开始构建 task 计算 task hash 检查 SSTATE_DIR 本地缓存 命中 未命中 解压本地缓存 检查 SSTATE_MIRRORS 远程未命中 → 重新编译 task 完成 本地命中直接解压,远程命中下载后解压

从流程图可以看出:本地缓存命中是最快的,远程缓存命中需要网络下载,但比重新编译快得多。只有远程也没命中时,才需要重新编译。

实战:配置一个高效的 SSTATE 缓存体系

结合前面的内容,我给出一个完整的配置方案:

# local.conf 中的 SSTATE 配置

# 1. 本地缓存目录,按机器架构分开
SSTATE_DIR = "/opt/yocto/sstate-cache/${MACHINE}"

# 2. 远程缓存镜像,优先使用内网服务器
SSTATE_MIRRORS = "\
    file://.* http://sstate.internal.company.com/sstate/PATH \
    file://.* https://s3.cn-north-1.amazonaws.com.cn/yocto-sstate/PATH \
"

# 3. Hash 白名单,避免路径差异导致缓存失效
BB_HASHBASE_WHITELIST ?= "TOPDIR TMPDIR STAMPS_DIR SSTATE_DIR"

# 4. 开启签名验证(可选),确保缓存文件未被篡改
SSTATE_VERIFY_SIG = "1"
SSTATE_SIG_KEY = "/path/to/signing-key.pem"

注意:如果开启了签名验证,远程缓存服务器上的文件必须附带签名文件(.sig)。否则 BitBake 会拒绝使用未签名的缓存。我一开始没注意这个,折腾了半天才发现是签名问题。

总结

SSTATE 缓存是 Yocto 增量构建的加速核心。配置好 SSTATE_DIR 和 SSTATE_MIRRORS,配合 hash 白名单和定期清理策略,能把构建时间缩短 50% 以上。我个人建议:先把本地缓存配好,再搭远程缓存服务器。一步到位容易出问题,分步来更稳。

嗯,这一章就到这里。记住:缓存命中率就是你的构建效率。多花点时间优化 SSTATE,比优化编译器参数划算得多。