NixOS 代际管理与回滚:从 generation 机制到引导项治理

NixOS 最令人安心的一点是「升级永远不会把你锁死」——每次 nixos-rebuild 都生成一个新的 generation,旧的完整保留。本文深入 profiles 布局、世代切换与回滚、systemd-boot 与 GRUB 引导项管理、特殊化配置,以及回滚演练与自动化。

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 与世代的区别

维度generationspecialisation
粒度整个系统配置的一次快照同一系统下的一个变体
编号单调递增名字(如 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、升级与巡检的完整闭环。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「nix」更多文章

  1. Nix 语言生态打包:Python、Node 与 Rust 的依赖治理
  2. Nix 派生与 Store 内幕:derivation、输入寻址与引用图
  3. Nix 求值与构建性能优化:从 eval 剖析到远程构建