引言
Nix 的 store 是内容寻址的:同一个 derivation 永远产出同一个路径,路径一旦存在就永不覆盖。这个性质带来了缓存复用与原子切换,也带来了一个必然结果——每次修改配置重新构建,旧路径并不会被替换,而是与新路径并存。一台跑了几年的 NixOS 机器,/nix/store 轻松涨到几百 GB。
回收机制的核心概念只有一个:gc root(垃圾回收根)。从根出发沿引用图可达的路径一律保留,其余称为「死路径」,可以被删除。绝大多数「删不掉」「删错了」的问题,都源于对根的理解偏差。本文从根出发,讲清回收参数、去重优化、占用分析与结构性瘦身。
目录
- 1. store 为什么会一直变大
- 2. gc root 是回收的唯一依据
- 3. 手动回收与常用参数
- 4. 自动回收策略
- 5. 硬链接去重与存储优化
- 6. 分析 store 占用
- 7. 结构性瘦身手段
- 8. 代际与 profile 的清理
- 9. 安全边界与恢复
- 10. 速查表与一句话记忆
1. store 为什么会一直变大
1.1 三个放大器
① 内容寻址:路径由输入哈希决定,输入变一点就是一个全新路径,旧路径原地不动
② 引用闭包:保留一个路径往往连带保留它的整条运行时闭包(可能是几百个路径)
③ 多层根:系统 profile、用户 profile、项目目录里的 result 符号链接,各留一份
典型曲线:首次安装数 GB;每次 nixos-rebuild switch 再 +1~3 GB(新闭包 + 旧世代);
每次开发构建数百 MB;长期不回收则累积到数百 GB。
1.2 为什么不能靠「删文件」解决
/nix/store 下的路径是共享的:同一个库可能被几十个包同时引用,
直接 rm -rf 会让其他包的运行环境瞬间损坏,
而且 store 的元数据库不知道你删了东西,后续操作会出现不一致。
正确做法只有一个:让路径变成"不可达",再交给 GC 统一删除。
记忆:store 变大是内容寻址的必然结果——三个放大器是「路径不覆盖、闭包连带保留、多层根各留一份」;绝不能直接
rm,只能让路径不可达后交给 GC。
2. gc root 是回收的唯一依据
2.1 可达性判定
GC 算法(概念版):
① 枚举所有 gc root
② 从每个根出发,沿 .drv 与产物的引用关系做图遍历
③ 可达集合 = 保留;不可达集合 = 死路径,可删除
因此:"没人用"是主观判断,"不可达"才是客观事实。
2.2 根的六种来源
| 来源 | 位置 | 说明 |
|---|---|---|
| 直接根 | /nix/var/nix/gcroots/ | 手工或工具创建的符号链接 |
| 间接根 | /nix/var/nix/gcroots/auto/ | 指向别处的符号链接的自动登记 |
| profile 根 | /nix/var/nix/profiles/、~/.local/state/nix/profiles/ | 每个世代是一个根 |
| 项目 result | 项目目录里的 result 符号链接 | 通过 auto 目录成为间接根 |
| 运行中进程 | /proc/*/root、打开的文件 | 正在被使用的路径不会被删 |
| 引导项 | NixOS 的 system-*-link | 启动菜单里的每个世代 |
2.3 查看根与死路径
nix-store --gc --print-roots # 列出所有根(含类型与目标)
nix-store --gc --print-dead | head # 只看将被删除的死路径
nix-store --gc --print-live | wc -l # 统计活路径数量
2.4 两个容易被忽略的点
① 运行中进程也是根:GC 会扫描 /proc,正在运行的服务所引用的路径不会被删;
但存在竞态——进程启动前路径被删就会启动失败,生产机回收应安排在维护窗口。
② 间接根的残留:删掉项目里的 result 之后仍可能"删不掉",
因为 gcroots/auto 里还有一条指向它的登记,需要先清掉登记。
记忆:GC 只认「从根可达」——六种根是直接根、间接根(auto 登记)、profile 世代、项目 result 链接、运行中进程、引导项;删除 result 后可能仍删不掉,是因为 auto 里还留着登记。
3. 手动回收与常用参数
3.1 基本命令与参数
nix-collect-garbage # 老命令
nix store gc # 新命令,等价
nix store gc --dry-run # 先看会删什么
nix-store --gc --print-dead | wc -l # 死路径条数
| 参数 | 作用 |
|---|---|
-d / --delete-old | 先删除所有 profile 的旧世代,再回收 |
--delete-older-than 30d | 只删除早于 30 天的世代 |
--max-freed 10G | 释放够 10G 就停止(避免长时间 IO 占用) |
--dry-run | 只报告不删除 |
nix-collect-garbage --delete-older-than 30d # 按时间回收
nix-collect-garbage --max-freed 20G # 限量回收
3.2 回收单个路径
nix store delete /nix/store/<hash>-<name> # 仍被引用时会拒绝删除
nix-store --query --referrers /nix/store/<hash>-<name> # 查谁在引用它
3.3 为什么「删了没效果」
常见原因:
① 只删了 result 链接,没删 profile 里的世代 → 世代仍是根
② 世代删了但 auto 登记还在 → 仍是间接根
③ 服务正在运行 → /proc 让它仍是根
④ 删除的是硬链接去重后的路径,磁盘并未真正释放(见第 5 节)
记忆:手动回收用
nix-collect-garbage(=nix store gc),-d先删旧世代、--delete-older-than按时间、--max-freed限量、--dry-run演练;「删了没效果」四个原因:只删 result、auto 登记残留、进程仍在用、硬链接导致未真正释放。
4. 自动回收策略
4.1 定时回收
# NixOS:每周回收一次,删除 30 天前的世代
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
dates 使用 systemd 时间表达式(daily、weekly、*-*-* 03:00:00 等),背后是 nix-gc.service 与 nix-gc.timer。
4.2 按剩余空间触发
# 空闲空间低于 min-free 时自动触发 GC,直到达到 max-free
nix.settings.min-free = 5 * 1024 * 1024 * 1024; # 5 GiB
nix.settings.max-free = 20 * 1024 * 1024 * 1024; # 20 GiB
这两个值的作用是"防爆盘":低于 min-free 时守护进程主动回收,
回收直到空闲达到 max-free 为止。与定时 GC 互补:定时管日常,min-free 管突发。
注意:min-free 触发的是回收,不保证能腾出足够空间(死路径本来就少时会失败)。
4.3 场景化建议
| 场景 | 建议配置 |
|---|---|
| 开发机 | nix.gc 每周 + --delete-older-than 14d |
| 生产服务器 | nix.gc 每周 + --delete-older-than 30d,并设 min-free |
| CI 构建机 | 每次任务结束回收,或把 min-free 设大一些 |
| 磁盘紧张的容器 | 只留 1~2 个世代,依赖 min-free 兜底 |
4.4 观察回收是否真的跑了
systemctl list-timers | grep nix-gc
systemctl status nix-gc.service --no-pager
journalctl -u nix-gc.service -n 30 --no-pager
df -h /nix
记忆:自动回收两条腿——定时
nix.gc(dates+--delete-older-than)管日常,min-free/max-free管突发爆盘;用systemctl status nix-gc与journalctl -u nix-gc确认它真的跑过。
5. 硬链接去重与存储优化
5.1 为什么会有重复内容
同一个文件出现在多个 store 路径里的情况非常普遍:
每个新世代都包含一份几乎相同的闭包,多个包也各自带一份相同的许可证文本与图标。
这些文件内容相同但路径不同,逻辑上占两份磁盘。
5.2 优化开关与效果
nix.settings.auto-optimise-store = true; # NixOS:每次构建后自动去重
nix store optimise # 手动对已有 store 做一次全量去重
du -sh --apparent-size /nix/store # 逻辑大小
du -sh /nix/store # 实际占用(差值即去重节省)
原理是:对内容相同的文件建立硬链接,让多个路径共享同一个 inode,从而只占一份磁盘。
5.3 代价与注意事项
代价:
- 每次构建后多一次全 store 扫描,构建密集的机器上 IO 明显增加
- 文件系统必须支持硬链接(部分网络文件系统不支持,会导致失败)
风险:
- 去重后的路径相互耦合,删除其中一个只减少链接计数,磁盘不一定释放
- 若怀疑 store 被外部修改,用 nix store verify --all 校验、nix store repair 修复
建议:
- 长期运行、世代保留较多的机器开启(收益通常 20%~40%)
- 一次性构建容器可以不开启;先量化差值再决定
记忆:
auto-optimise-store/nix store optimise用硬链接让相同内容的文件共享 inode,收益常达 20%~40%;代价是每次构建后多一次全 store 扫描,且要求文件系统支持硬链接;先比较du -sh与du -sh --apparent-size再决定。
6. 分析 store 占用
6.1 从闭包角度看
nix path-info -Sh /run/current-system # 当前系统闭包大小
nix path-info -Sh --recursive /run/current-system | sort -k2 -h | tail -20
nix path-info -S --json /run/current-system \
| jq -r 'to_entries[] | "\(.value.narSize)\t\(.key)"' | sort -n | tail
6.2 从引用图角度看
nix-tree /run/current-system # 交互式浏览引用图:谁依赖谁、谁占多大
nix-du -s=500MB | dot -Tpng > store.png # 可选:生成占用树图
nix-tree 的价值在于把「某个包为什么被保留」直接展示出来——顺着父节点一路往上,就能找到那个 gc root。
6.3 一个分析流程
① df -h /nix → 总量与增长趋势
② nix path-info -Sh /run/current-system → 当前系统闭包有多大
③ nix-tree /run/current-system → 谁是大头、为什么被保留
④ nix store gc --dry-run → 回收能释放多少
⑤ du -sh vs du -sh --apparent-size → 去重还能省多少
记忆:占用分析四把尺子——
df -h /nix看总量、nix path-info -Sh看闭包、nix-tree看引用图找大头与根、nix store gc --dry-run看可回收量;再用du -sh与--apparent-size的差值看去重潜力。
7. 结构性瘦身手段
7.1 控制保留策略
# 默认 keep-derivations = true、keep-outputs = false
nix.settings.keep-derivations = false;
nix.settings.keep-outputs = false;
含义:keep-derivations 决定是否保留 .drv 作为根的传播;
keep-outputs 决定是否保留构建输入的输出版本。
这两个开关影响"为了能重新构建而保留多少":关掉后 store 更小,
但重建时需要重新下载或重建部分依赖。
7.2 不让每次构建都留下根
nix build .#mypkg --no-out-link # 不创建 result 符号链接(用完即弃)
nix shell nixpkgs#jq -c jq --version # nix shell / nix run 不注册长期根
注意:--no-out-link 得到的路径没有根,下次 GC 就会被回收,
因此只适合"用完即弃"的验证,不适合需要保留的产物。
7.3 把大文件请出 store
不该放进 store 的内容:数据集、模型权重、虚拟机镜像、日志归档。
代价:每次变更都新增一份完整副本,且无法增量存储。
替代:放在普通目录(如 /srv/data)在配置里只引用路径;
需要版本化时用独立的制品库,而不是 store。
记忆:结构性瘦身三招——关掉
keep-derivations/keep-outputs减少保留、临时验证用--no-out-link、数据集与镜像不要放进 store。
8. 代际与 profile 的清理
8.1 系统与用户世代
nix-env --list-generations --profile /nix/var/nix/profiles/system
nix-env --delete-generations +5 --profile /nix/var/nix/profiles/system # 留最近 5 个
nix-env --delete-generations 30d --profile /nix/var/nix/profiles/system # 删 30 天前
nix-env --list-generations && nix-env --delete-generations +3 # 用户 profile
home-manager expire-generations "-30 days" # Home Manager
删除世代只是去掉 gc root,磁盘释放要等后续 GC;
因此"删世代 + 回收"要成对执行(`-d` 参数就是替你做了这两步)。
8.2 引导项与世代的关系
boot.loader.systemd-boot.configurationLimit = 10;
boot.loader.grub.configurationLimit = 10;
configurationLimit 会清理超出数量的旧世代,因此它既是「启动菜单整洁」的手段,也是「控制保留量」的手段。
8.3 推荐的保留策略
| 机器类型 | 保留世代数 | 理由 |
|---|---|---|
| 开发机 | 5~10 | 需要频繁回滚到近期配置 |
| 生产服务器 | 10~20 | 保留足够回滚纵深 |
| CI 构建机 | 1~2 | 只关心当前配置,磁盘优先 |
| 边缘设备 | 3~5 | 磁盘小,回滚需求有限 |
记忆:世代是最大的根——
nix-env --delete-generations +5保留最近 5 个、configurationLimit同时管启动菜单与根的数量;删世代只去掉根,必须再跑一次 GC 才真正释放磁盘。
9. 安全边界与恢复
9.1 不要手工删除 store 路径
禁止:rm -rf /nix/store/xxx
原因:破坏共享 inode 与元数据库一致性,可能导致大量包损坏
正确:nix store delete(会检查引用)或交给 GC
9.2 校验与修复
nix store verify --all # 校验所有路径的内容哈希(耗时)
nix store repair /nix/store/<hash>-<name> # 从缓存重新拉取或重建
nix-store --verify --check-contents --repair # 全量校验并修复
9.3 并发与共享 store
并发风险:GC 删除某路径时,脚本恰好要启动它 → 报 No such file or directory。
缓解:生产机在维护窗口回收或先停服务;用 --max-freed 限制单次时长。
共享 store(网络文件系统或 SSH store):
GC 必须只在"拥有该 store 的机器"上运行,客户端只做构建与使用;
否则会删掉别人正在用的路径。定时 GC 只配在 store 主机上。
9.4 爆盘后的急救顺序
nix-env --delete-generations +2 --profile /nix/var/nix/profiles/system
nix-collect-garbage --max-freed 20G
nix-tree /run/current-system # 找出大闭包
du -sh /nix/store/* 2>/dev/null | sort -h | tail -20
记忆:安全边界四条——不手工
rmstore、怀疑损坏用nix store verify与repair、回收要避开启动竞态、共享 store 只在 store 主机上回收;爆盘急救顺序是删旧世代 → 限量回收 →nix-tree找大头。
10. 速查表与一句话记忆
| 需求 | 命令 |
|---|---|
| 演练回收 | nix store gc --dry-run |
| 删旧世代并回收 | nix-collect-garbage -d |
| 按时间回收 | nix-collect-garbage --delete-older-than 30d |
| 去重 | nix store optimise |
| 看闭包与引用图 | nix path-info -Sh / nix-tree |
| 校验修复 | nix store verify --all / nix store repair |
一句话记忆:store 只增不减是内容寻址的必然结果,回收的唯一依据是「从 gc root 可达」——六种根分别是直接根、auto 间接根、profile 世代、项目 result 链接、运行中进程与引导项;因此「删了没释放」几乎总是根没去掉或硬链接没断,正确顺序是「删世代(-d/--delete-older-than)→ 跑 GC → 需要时 nix store optimise 去重」;日常用 nix.gc 定时 + min-free 兜底,分析用 df / nix path-info -Sh / nix-tree / du --apparent-size 四把尺子,结构性手段则是关掉 keep-derivations/keep-outputs、临时验证用 --no-out-link、大数据不要放进 store。
延伸阅读
- derivation 与 store 内幕
- 求值与构建性能优化
- NixOS 运维实战
- 二进制缓存与替换器
- Linux 存储 IO 性能 — 文件系统与 IO 调优
- 成本与 FinOps 实践 — 存储与资源的成本治理
- Nix 手册:垃圾回收
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。