1. 为什么二进制缓存是 Nix 的杀手锏
纯源码构建的包管理器(如早期 Gentoo)最大的痛点是每台机器都要重新编译。Nix 的设计从一开始就绕开了这个陷阱:因为 store 路径由输入哈希唯一确定,所以任何人事先构建好的产物,理论上都可以被任何人直接复用。
二进制缓存(binary cache)就是把「构建」降级为「下载」的机制。它的价值体现在:
- CI 构建一次,开发机、生产机、其他架构机器全部直接拉取
- 官方
cache.nixos.org覆盖了 nixpkgs 绝大多数包的预编译产物 - 私有代码可推送到自建缓存,团队内共享而不必暴露源码
- 断网或受限环境可以预置离线缓存
而 substituter(替换器) 是客户端侧的概念:当 Nix 需要某个 store 路径时,它先问「有哪个 substituter 能提供它?」,能提供就下载,不能才自己构建。理解这套查询与信任机制,是搭建企业级 Nix 基础设施的前提。
本文与 Nix 构建与 CI 互补:后者讲 CI 流水线集成,本文讲缓存本身的机制与自建。
2. substituters 机制与查询顺序
2.1 构建前的「替代检查」
nix build 的流程可以简化为:
- 求值得到 derivation,算出所有输出路径
- 对闭包中的每个路径,询问 substituter 是否可用
- 可用的直接下载,不可用的本地构建
- 构建结果可选地推送到缓存
这个「先问后建」的行为由 --substitute(默认开)与 --no-substitute(强制本地构建)控制:
nix build nixpkgs#firefox # 优先用缓存
nix build --option substitute false nixpkgs#hello # 强制本地构建
nix build --no-require-sigs ... # 允许未签名缓存(见第 3 节)
2.2 配置项与顺序
客户端配置里与缓存相关的键:
# /etc/nix/nix.conf
substituters = https://cache.nixos.org https://my-cache.example.com
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= my-cache-1:abc...=
substituters是有序列表,Nix 按顺序询问- 一旦某个 substituter 提供可用路径,就不再继续问后续的
- 所以把更近、更快、更私有的缓存放前面是常见优化
2.3 每用户与系统级配置
# 临时覆盖(命令行)
nix build --substituters https://my-cache.example.com nixpkgs#hello
# 用户级
mkdir -p ~/.config/nix
echo 'substituters = https://my-cache.example.com' >> ~/.config/nix/nix.conf
注意:非 trusted 用户无法添加新的 trusted-public-keys。在多人机器上,普通用户改 nix.conf 里的 substituters 会被忽略,除非该用户属于 trusted-users。这是常见「为什么我的缓存没生效」的原因。
3. 信任模型与 trusted-public-keys
3.1 为什么需要签名
二进制缓存本质上是「别人给你的二进制」。如果没有签名校验,攻击者可以伪造一个路径相同但内容被篡改的产物。Nix 的方案是基于公钥的签名:
- 缓存方用私钥对每个 store 路径的**指纹(fingerprint)**签名
- 客户端用配置里的公钥验签
- 验签失败 → 拒绝使用,改为本地构建
指纹的构造是确定性的:它由 store 路径、nar 哈希、nar 大小、引用列表拼接而成。因此签名绑定的是「这个路径的内容」,而不是「这个文件」。
3.2 配置信任的三种方式
# 1. 信任官方缓存
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
# 2. 信任自建缓存(多公钥用空格分隔)
trusted-public-keys = cache.nixos.org-1:... my-cache-1:abc123...=
# 3. 单次绕过(仅调试,生产禁用)
nix build --no-require-sigs --substituters https://untrusted.example.com ...
3.3 谁可以绕过签名
--no-require-sigs 只在当前用户属于 trusted-users(或 root)时生效。这防止普通用户引入任意二进制。相关的 require-sigs 默认值为 true。
| 场景 | require-sigs | 效果 |
|---|---|---|
| 默认 | true | 只接受已签名且公钥受信的路径 |
| 自建缓存已签名 | true | 把公钥加入 trusted-public-keys 即可 |
| 内网无签名缓存 | true + 未加公钥 | 拒绝,退回本地构建 |
| 调试 | false | 仅 trusted 用户可绕过 |
4. narinfo 与 nar 格式剖析
4.1 narinfo:路径的元数据清单
当客户端询问「你有没有 /nix/store/zzz-hello?」时,缓存返回一个 narinfo 文件:
StorePath: /nix/store/zzz-hello-2.12.1
URL: nar/1abc...def.nar.xz
Compression: xz
FileHash: sha256:2ghi...=
FileSize: 204800
NarHash: sha256:3jkl...=
NarSize: 655360
References: ccc-glibc-2.38-4 aaa-hello-2.12.1
Deriver: xxx-hello-2.12.1.drv
Sig: my-cache-1:base64signature...
字段含义:
| 字段 | 作用 |
|---|---|
| StorePath | 被描述的路径 |
| URL | nar 归档的相对位置 |
| Compression | 压缩算法(none/xz/zstd/brotli) |
| FileHash | 压缩后文件的哈希,用于传输校验 |
| NarHash | 解压后 NAR 的哈希,用于签名与内容校验 |
| References | 该路径的引用列表,供闭包遍历 |
| Deriver | 产出它的 .drv |
| Sig | 对指纹的签名 |
4.2 nar:规范化的归档格式
NAR(Nix Archive) 是 Nix 自己的归档格式,设计目标只有一个:同样的目录树,永远产生逐字节相同的归档。它记录了文件的类型、可执行位、内容与目录顺序,但不记录 mtime、owner 等易变元数据。
# 手动导出某路径为 nar 并查看大小
nix-store --dump /nix/store/zzz-hello | wc -c
# 从 nar 恢复
nix-store --restore ./out < <(nix-store --dump /nix/store/zzz-hello)
正因为 NAR 是规范化的,NarHash 才具有跨机器可比性,签名与去重才成立。
4.3 手动探测一个缓存
# 直接 curl narinfo(以官方缓存为例)
curl -s https://cache.nixos.org/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.narinfo
# nix 自带的查询
nix path-info --store https://cache.nixos.org -S nixpkgs#hello
5. 自建缓存:nix-serve 与 harmonia
5.1 nix-serve:最简方案
nix-serve 直接暴露本机 store 为 HTTP 缓存,适合小团队:
# NixOS 配置
services.nix-serve = {
enable = true;
port = 5000;
secretKeyFile = "/var/lib/nix-serve/cache-priv-key.pem";
};
它读取本机 store 的 narinfo(实时生成),因此缓存内容 = 本机 store 内容。优点是无状态、零配置;缺点是性能依赖本机 store 查询,且没有预热机制。
5.2 harmonia:Rust 重写的高性能替代
harmonia 是 nix-serve 的现代替代,用 Rust 编写,支持 S3 后端与更好的并发:
services.harmonia = {
enable = true;
settings = {
bind = "[::]:5000";
sign_key = "/var/lib/harmonia/cache-priv-key.pem";
};
};
5.3 生成签名密钥
# 生成密钥对(写入当前目录)
nix-store --generate-binary-cache-key my-cache-1 ./cache-priv-key.pem ./cache-pub-key.pem
# 公钥内容形如:
# my-cache-1:AbCdEf...=
私钥文件必须严格保护(chmod 600,最好由 NixOS 密钥管理实战 的 sops-nix 注入)。
6. 推送与签名:nix copy 与 nix store sign
6.1 推送闭包到缓存
# 推送到远程 HTTP 缓存(需该缓存支持写入,或推送目标为 ssh store)
nix copy --to https://my-cache.example.com ./result
# 用私钥边推边签
nix copy --to https://my-cache.example.com --secret-key-files ./cache-priv-key.pem ./result
# 推送到 ssh store(目标机 store 本身即缓存源)
nix copy --to ssh://builder@cache-host ./result
6.2 先签后推的两步法
# 1. 对 store 路径签名
nix store sign --key-file ./cache-priv-key.pem /nix/store/zzz-hello
# 2. 推送到缓存(签名信息随 narinfo 一起上传)
nix copy --to https://my-cache.example.com /nix/store/zzz-hello
6.3 验证签名
nix store verify --store https://my-cache.example.com --trusted-public-keys "$(cat cache-pub-key.pem)" /nix/store/zzz-hello
7. S3 后端与 CDN 部署
7.1 为什么用 S3
把缓存放到对象存储有三个好处:无限容量、天然高可用、可挂 CDN。nar 与 narinfo 都是不可变对象(路径变了内容就变),非常适合对象存储。
7.2 上传布局
一个 S3 缓存桶的典型布局:
s3://my-nix-cache/
├── nix-cache-info # 缓存元信息(StoreDir、WantMassQuery 等)
├── zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.narinfo
├── nar/
│ └── 1abc...def.nar.xz
nix-cache-info 是关键的小文件:
StoreDir: /nix/store
WantMassQuery: 1
Priority: 40
7.3 用 nix copy 直接推 S3
nix copy --to 's3://my-nix-cache?region=us-east-1' --secret-key-files ./cache-priv-key.pem ./result
7.4 harmonia 的 S3 模式
harmonia 支持「本地 store + S3 后备」模式:本地命中优先,未命中则回源 S3,同时可以把新路径自动上传 S3。这在多构建节点场景下非常实用。
7.5 加一层 CDN
由于 nar 是内容不可变的,可以直接把 S3 桶挂在 CloudFront 等 CDN 后面,并把 substituters 指向 CDN 域名,获得边缘缓存与更低延迟。
8. 客户端配置与多缓存优先级
8.1 推荐配置模板
# /etc/nix/nix.conf
experimental-features = nix-command flakes
substituters = https://my-cache.example.com https://cache.nixos.org
trusted-public-keys = my-cache-1:AbCdEf...= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
trusted-substituters = https://my-cache.example.com
connect-timeout = 5
substituters:私有缓存放前面,官方兜底trusted-substituters:允许非 trusted 用户使用的缓存(多用户机器)connect-timeout:缓存不可达时快速失败,避免卡住构建
8.2 NixOS 中的声明式配置
nix.settings = {
substituters = [
"https://my-cache.example.com"
"https://cache.nixos.org"
];
trusted-public-keys = [
"my-cache-1:AbCdEf...="
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
];
};
8.3 缓存优先级的影响
Priority 字段(在 nix-cache-info 中)与 substituters 顺序共同决定选择。实践中,把本地/内网缓存放最前能让 90% 的拉取不出机房。
9. 常见坑与排障
9.1 缓存不生效
排查清单:
# 1. 确认实际生效的配置
nix config show | grep -E 'substituters|trusted-public-keys'
# 2. 确认用户是否 trusted(非 trusted 用户的 substituters 设置被忽略)
nix config show | grep trusted-users
# 3. 手动询问缓存
nix path-info --store https://my-cache.example.com /nix/store/zzz-hello
9.2 签名校验失败
error: path '/nix/store/zzz-hello' is not valid
常见原因:公钥没配对、签名时用的 key name 与公钥前缀不一致、narinfo 被中途修改。用 nix store verify 逐路径定位。
9.3 推送时「路径已存在」
error: path '/nix/store/zzz-hello' already exists in the store
这不是错误——说明目标缓存已有该路径(因为路径由输入决定,内容必然相同)。可以忽略,或用 --no-check-sigs 调整。
9.4 大闭包上传超时
推送大闭包(如整个桌面环境)时,建议:
nix copy --to https://my-cache.example.com \
--parallel-connections 4 \
--retry 3 \
./result
9.5 磁盘与成本
缓存桶会持续增长。建议按「保留最近 N 个版本 + 定期清理」策略管理,或使用支持生命周期规则的 S3 桶策略。这与 NixOS 运维实战 中的 store 瘦身思路一致。
10. 总结
二进制缓存把 Nix 从「每次都编译」变成「构建一次、处处下载」,是它能在生产环境落地的基础设施:
- substituter 是客户端侧的查询机制,按
substituters顺序询问 - trusted-public-keys 定义了「你信任谁的二进制」,是安全边界
- narinfo + nar 是缓存的物理格式,NAR 的规范化保证了哈希可比
- nix-serve / harmonia 提供自建方案,harmonia 更适合大规模与 S3 场景
- 签名与推送通过
nix store sign与nix copy完成,公钥需分发到客户端
掌握这套机制后,你可以把团队 CI 的构建产物变成共享资产,让所有机器都只下载不编译。下一步建议阅读 Nix 构建与 CI 把缓存接入流水线,以及 NixOS 运维实战 理解缓存与世代、GC 的协同。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。