Nix 二进制缓存与替换器:substituters、签名与自建缓存

二进制缓存是 Nix 能在生产环境立足的关键:它让「构建」变成「下载」。本文详解 substituters 查询顺序、narinfo 与 nar 格式、trusted-public-keys 信任模型、nix-serve 与 harmonia 自建缓存、S3 后端与 CDN 部署,以及签名与推送的完整流程。

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 的流程可以简化为:

  1. 求值得到 derivation,算出所有输出路径
  2. 对闭包中的每个路径,询问 substituter 是否可用
  3. 可用的直接下载,不可用的本地构建
  4. 构建结果可选地推送到缓存

这个「先问后建」的行为由 --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被描述的路径
URLnar 归档的相对位置
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 的协同。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nix」更多文章

  1. NixOS 代际管理与回滚:从 generation 机制到引导项治理
  2. Nix 语言生态打包:Python、Node 与 Rust 的依赖治理
  3. Nix 派生与 Store 内幕:derivation、输入寻址与引用图