1. 世代:NixOS 的「时间机器」
传统 Linux 发行版升级是原地修改:apt upgrade 覆盖 /usr/bin、/lib 里的文件,一旦升级后系统起不来,你只能进救援模式手动修。NixOS 的做法完全不同:
- 每次
nixos-rebuild switch都构建出一个全新的系统闭包(一个 store 路径) - 这个闭包通过一个 generation(代际) 链接被「激活」
- 上一个系统的闭包原封不动地留在 store 里,只是不再被激活
因此「回滚」不是「撤销修改」,而是「把激活指针拨回上一个 generation」。这几乎是瞬间完成的,且不依赖任何备份。这个机制是 NixOS 在服务器领域最有说服力的卖点之一,也是 NixOS 运维实战 的核心主题。
2. /nix/var/nix/profiles 布局
2.1 目录结构
系统级 profile 都放在 /nix/var/nix/profiles/:
/nix/var/nix/profiles/
├── system -> system-42-link # 当前激活的系统(间接链接)
├── system-40-link -> /nix/store/aaa-nixos-system-host-24.05...
├── system-41-link -> /nix/store/bbb-nixos-system-host-24.05...
├── system-42-link -> /nix/store/ccc-nixos-system-host-24.05...
├── default -> system-42-link # 与 system 同义,兼容旧工具
└── per-user/
└── root/
└── profile-3-link -> ...
关键理解:
system-N-link是符号链接,指向 store 里的系统闭包- 系统闭包本身是一个巨大的闭包,包含内核、initrd、所有 systemd 单元、所有配置好的服务
- 数字
N单调递增,不会复用(回滚也不会让编号变小)
2.2 查看世代列表
nix-env --list-generations --profile /nix/var/nix/profiles/system
输出示例:
40 2026-09-01 10:12:03
41 2026-09-15 09:30:41
42 2026-10-01 11:02:18 (current)
也可以直接 ls -l /nix/var/nix/profiles/,但 nix-env --list-generations 会解析时间戳,更易读。
2.3 世代与 store 的关系
# 查看某世代指向的真实闭包
readlink -f /nix/var/nix/profiles/system-41-link
# 查看该世代包含哪些包
nix-store --query --requisites /nix/var/nix/profiles/system-41-link | wc -l
世代链接是 gc root 的来源之一——只要 system-N-link 存在,对应的整个闭包就不会被 GC 回收。这解释了「为什么删了世代之后磁盘才释放」。
3. nixos-rebuild 与世代的创建
3.1 四个子命令的区别
| 命令 | 构建 | 激活 | 创建世代 | 用途 |
|---|---|---|---|---|
nixos-rebuild build | 是 | 否 | 否 | 只构建,验证能否成功 |
nixos-rebuild test | 是 | 是 | 否 | 临时切换,重启后还原 |
nixos-rebuild switch | 是 | 是 | 是 | 正式应用,成为新世代 |
nixos-rebuild boot | 是 | 否 | 是 | 只设为下次启动的系统 |
test 是最被低估的子命令:它在不创建世代的前提下把新配置激活到当前运行系统,如果出错,reboot 或 nixos-rebuild switch 就能回到干净状态。远程改网络配置前,强烈建议先 test。
3.2 switch 的完整流程
1. 求值 configuration.nix(或 flake 的 nixosConfigurations.host)
2. 构建系统闭包 -> /nix/store/xxx-nixos-system-host-24.05
3. 创建新世代链接 system-43-link
4. 更新 /nix/var/nix/profiles/system 指向 43
5. 运行 activation script:切换 /run/current-system、重启受影响的服务
6. 更新引导项(systemd-boot / GRUB)
7. 更新 /etc(NixOS 的 /etc 是链接到当前系统的)
第 6 步很关键:引导项不是手写的,而是由当前世代自动生成,所以每个世代在启动菜单里都有一项,可以从菜单直接选旧系统启动。
3.3 用 flake 时的差异
# flake 形式
nixos-rebuild switch --flake .#my-host
# 指定远程主机
nixos-rebuild switch --flake .#my-host --target-host root@server --use-remote-sudo
远程部署的完整方案参见 NixOS 远程部署工具。
4. 回滚:rollback 与 switch-generation 子命令
4.1 最常用的一条命令
nixos-rebuild switch --rollback
等价于「把系统 profile 切到上一个世代并激活」。注意它会创建新世代吗?不会——它是把 system 链接重新指向 system-(N-1),不产生新编号。
4.2 精确切换任意世代
# 列出世代编号后,切到指定世代
nixos-rebuild switch --switch-generation 40
# 只设为下次启动
nixos-rebuild boot --switch-generation 40
4.3 用 nix-env 操作底层 profile
nix-env --profile /nix/var/nix/profiles/system --list-generations
nix-env --profile /nix/var/nix/profiles/system --switch-generation 40
nix-env --profile /nix/var/nix/profiles/system --delete-generations 38 39
nix-env --delete-generations 是删除世代(释放磁盘),与切换是两回事。删除后该世代的 gc root 消失,下次 GC 才会真正回收。
4.4 回滚的作用范围
回滚会还原:内核、initrd、systemd 服务定义、/etc 内容、已安装的包、防火墙规则。不会还原:
/home下的用户数据(NixOS 不管这些)/var下的状态数据(数据库文件、日志)- 你已经手动写入的、非声明式管理的文件
这是最容易踩的坑:数据库 schema 升级后再回滚系统,旧版本的服务可能读不了新格式的数据。参见 NixOS 存储与文件系统 里关于快照与备份的建议。
5. boot 引导项管理
5.1 systemd-boot
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 10; # 只保留最近 10 个引导项
};
boot.loader.efi.canTouchEfiVariables = true;
configurationLimit控制引导菜单里保留多少个世代,超过的旧项会被删除(但不删除 store 里的闭包)- 引导项文件在
/boot/loader/entries/,每次 switch 自动生成 configurationLimit设太小会导致「菜单里没有可回滚的旧系统」,设太大则/boot分区会爆
5.2 GRUB
boot.loader.grub = {
enable = true;
device = "/dev/sda";
configurationLimit = 10;
useOSProber = false; # 多系统时按需开启
};
GRUB 的世代菜单由 grub-mkconfig 在每次 switch 时重新生成,配置模板在 /nix/store 里(只读)。
5.3 检查 /boot 空间
df -h /boot
ls /boot/loader/entries/ | wc -l
/boot 通常只有 512 MB。若世代过多、内核较大,容易写满导致 switch 失败。定期清理与 configurationLimit 是必需的运维动作。
5.4 从引导菜单启动旧世代
在启动菜单里选中旧世代启动后,系统运行的是旧闭包,但当前世代指针没变。要让这次启动「持久化」,需要在该系统里执行:
sudo /run/current-system/bin/switch-to-configuration switch
# 或
sudo nixos-rebuild switch --switch-generation <N>
6. 特殊化与多配置共存
6.1 specialisation:一个世代里的多个变体
specialisation.rescue.configuration = {
system.nixos.tags = [ "rescue" ];
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [ 22 ];
# 一个最小化、必然可启动的配置
};
构建后,启动菜单里会出现 Specialisation rescue 选项,从它启动会得到一个同世代的变体。这是「安全网」的另一种形态:即使主配置把网络或显示搞坏,rescue 变体仍可进入。
6.2 查看特殊化
ls /nix/var/nix/profiles/system/specialisation/
# 或在启动菜单中直接看到
6.3 与世代的区别
| 维度 | generation | specialisation |
|---|---|---|
| 粒度 | 整个系统配置的一次快照 | 同一系统下的一个变体 |
| 编号 | 单调递增 | 名字(如 rescue) |
| 用途 | 版本回滚 | 应急入口、多模式启动 |
| 菜单项 | 每个世代一项 | 每个特殊化一项 |
7. 回滚演练
7.1 演练目标
在生产机上验证回滚链路,而不是等真出事时才发现「菜单里没有旧项」。建议流程:
# 1. 记录当前世代
nix-env --list-generations --profile /nix/var/nix/profiles/system | tail -3
# 2. 做一次无害改动并 switch(例如改个 motd)
# 编辑 configuration.nix 后:
sudo nixos-rebuild switch
# 3. 确认新世代出现
# 4. 回滚
sudo nixos-rebuild switch --rollback
# 5. 确认回到旧世代且服务正常
systemctl --failed
7.2 远程回滚的安全姿势
远程改网络配置前:
# 先 test,观察 SSH 是否仍可连
sudo nixos-rebuild test --flake .#server
# 若失联,重启即可恢复(test 不创建世代,重启回旧系统)
sudo reboot
若已 switch 且失联,通过带外通道(IPMI、云控制台)在引导菜单选旧世代。
7.3 用 VM 测试回滚逻辑
把回滚流程写成 NixOS 虚拟机与集成测试 里的 nixosTest,在 CI 中验证:
testScript = ''
machine.succeed("nixos-rebuild switch --rollback")
machine.succeed("systemctl is-system-running --wait")
'';
8. 自动化与运维
8.1 启动失败自动回滚
NixOS 有一个鲜为人知但极实用的特性:如果某次 switch 后系统在启动时激活失败,可以配置自动回滚到上一个世代。
system.autoUpgrade = {
enable = true;
allowReboot = false;
dates = "04:00";
flake = "github:my-org/infra#server";
};
配合监控(见 NixOS 运维实战)检测 systemctl --failed,可在异常时触发脚本回滚。
8.2 保留策略
# 只保留最近 10 个世代,其余删除
nix-env --profile /nix/var/nix/profiles/system --delete-generations +10
# 删除 30 天前的世代
nix-env --profile /nix/var/nix/profiles/system --delete-generations 30d
8.3 与 GC 的联动
# 删除旧世代后,回收空间
sudo nix-collect-garbage -d
-d 会先删除所有 profile 的旧世代,再 GC。不要在没确认能回滚前执行它——它会把你的「时间机器」清空。
8.4 日常巡检清单
nix-env --list-generations --profile /nix/var/nix/profiles/system | tail -5
df -h /boot /nix
systemctl --failed
readlink -f /nix/var/nix/profiles/system
9. 常见坑
9.1 「回滚后服务还是坏的」
原因通常是状态数据没回滚(数据库 schema、缓存文件)。回滚只回滚代码与配置,不回滚 /var。解决办法是数据库层面的迁移兼容或备份恢复。
9.2 引导菜单里没有旧世代
configurationLimit 设太小,或 /boot 空间不足导致旧条目被删。调大 configurationLimit 并清理 /boot。
9.3 世代删不掉
error: cannot delete generation ... because it is the current generation
当前世代不能删。先 --switch-generation 到别的世代。
9.4 空间没释放
删除世代只是移除 gc root,不会立即释放空间。必须再跑 nix-collect-garbage。若仍有路径存活,用 nix-store --query --roots 反查。
9.5 混用 flake 与 channel
用 flake 部署的机器不要再用 nix-channel --update,两者会各自维护 profile,导致「明明 switch 了却还是旧版本」。统一到 flake 后,删除 /nix/var/nix/profiles/per-user/root/channels。
9.6 远程 switch 把 SSH 弄丢
防火墙配置改动是最常见原因。参见 NixOS 网络与防火墙配置 中「先 test 再 switch」的纪律,并确保保留一个带外通道。
10. 总结
代际机制让 NixOS 拥有了几乎零成本的回滚能力:
- 每次
nixos-rebuild switch产生一个不可变的系统闭包与一个 generation 链接 --rollback与--switch-generation是回滚的两把钥匙,test是安全前置- 引导项由世代自动生成,
configurationLimit决定菜单保留多少历史 specialisation提供了「同一世代内的应急变体」- 回滚只覆盖代码与配置,状态数据需要独立备份策略
把回滚演练纳入常规运维(甚至写进 CI 的 nixosTest),才是真正把「可回滚」变成「敢升级」。建议继续阅读 NixOS 运维实战 了解 GC、升级与巡检的完整闭环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。