共享状态缓存(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 不一致。
怎么优化?我总结了几个实战经验:
- 固定全局变量:把
DISTRO_FEATURES、MACHINE等变量写死,不要在不同构建机之间变化。 - 使用
BB_HASHBASE_WHITELIST:把不影响输出的变量加入白名单,比如TOPDIR、TMPDIR。这样不同路径的构建也能共享缓存。 - 拆分 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 缓存体系
结合前面的内容,我给出一个完整的配置方案:
# 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,比优化编译器参数划算得多。