OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 020

Nix 与 NixOS:存储、回滚和声明式思维

从 store、profile 与命令式工具理解 NixOS 的系统模型。

所在专题Nix 语言与构建模型 ↗
约 15 分钟阅读开始阅读

NixOS 的工作原理#

NixOS 基于 Nix ,一个纯函数式软件包管理系统。Nix 将所有软件包彼此隔离地存储在 /nix/store 下。

/nix/store/5rnfzla9kcx4mj5zdc7nlnv8na1najvg-firefox-3.5.4/

示例路径的哈希部分标识 store 对象;常见 derivation 的输出路径由构建描述等输入决定,不能直接当作输出文件内容哈希。软件包构建完成后不会被覆盖;相反,如果您更改软件包的构建描述(即其“Nix 表达式”),它会被重新构建并安装到 /nix/store 中的不同路径,以避免与旧版本冲突。NixOS 对此进行了扩展,不仅使用 Nix 构建软件包,还使用 ​​Nix 构建配置文件等。例如,SSH 守护进程的配置也是根据 Nix 表达式构建的,并存储在类似这样的路径下。

/nix/store/s2sjbl85xnrc18rl4fhn56irkxqxyk4p-sshd_config

NixOS 通过从 Nix 表达式构建整个系统配置,确保这些配置不会相互覆盖,可以回滚等等。

Nix/NixOS 存储软件包的方式带来的一个重要影响是,它不把软件按传统发行版方式统一安装到 /bin 、 /sbin 、 /lib 、 /usr 等目录。所有软件包都保存在 /nix/store 目录中。(保留 /bin/sh 等兼容入口;具体布局随系统模块而异。)正是由于不使用 /bin 等“全局”目录,才使得软件包的多个版本可以共存。Nix 确实有一个 /etc 用于存放系统级配置文件,但该目录中的大多数文件都是指向 /nix/store 中生成文件的符号链接。 不采用传统 FHS 软件布局,会让依赖固定 /usr/lib 或动态加载器路径的第三方预编译程序需要包装、修补或兼容环境;不是 Nix 管理的依赖本身普遍失效。

声明式系统配置模型#

在 NixOS 中,整个操作系统——包括内核、应用程序、系统软件包、配置文件等等——都是由 Nix 包管理器根据纯函数式构建语言的描述构建的。纯函数式的特性意味着构建新的配置不会覆盖之前的配置。其他大部分特性都源于此。

您可以通过在 /etc/nixos/configuration.nix 文件中编写规范来配置 NixOS 系统,该规范定义了您希望在机器上实现的功能。例如,以下是 BIOS/GRUB 系统的历史配置片段,实际安装还须生成完整硬件配置并确认磁盘目标;不可直接用于新机器:

{
  boot.loader.grub.device = "/dev/sda";
  fileSystems."/".device = "/dev/sda1";
  services.openssh.enable = true;
}

修改 /etc/nixos/configuration.nix 文件后,运行以下命令即可生效:

nixos-rebuild switch

该命令执行配置所需的一切操作,包括下载和编译 OpenSSH、生成 SSH 服务器的配置文件等等。

可靠的升级#

固定构建输入能减少原系统安装历史对构建结果的影响;升级仍需检查配置兼容、激活脚本和可写数据迁移。

原子升级#

NixOS 采用_事务性_配置管理方式:配置更改(例如升级)是_原子性的_ 。这意味着,新旧 store 产物分离、profile 原子切换减少了升级中途覆盖文件的风险;启动加载器写入、服务激活和可写数据仍有独立故障边界。

回滚#

由于新配置的文件不会覆盖旧文件,因此您可以(原子地)回滚到之前的配置。例如,如果在 nixos-rebuild switch 后,您发现不喜欢新配置,您可以直接回滚:

nixos-rebuild switch --rollback

事实上,尚未删除且在启动菜单保留范围内的旧配置可显示在启动菜单中。因此,如果新配置崩溃或无法正常启动,您只需在启动菜单中选择旧配置即可回滚。回滚过程非常快捷:无需从备份中恢复大量文件。

可复现的系统配置#

NixOS 的声明式配置模型使得在另一台机器上重现系统配置变得轻而易举(例如,在生产服务器上进行更改之前,先在测试环境中进行测试)。固定依赖并为目标机器配置硬件、磁盘和引导后,可将声明配置带到目标 NixOS 机器,然后运行 nixos-rebuild switch 即可。这将使您获得相同的配置(内核、应用程序、系统服务等),但可变状态(例如 /var 目录中的内容)除外。

可以安全地测试更改#

回滚可以帮助恢复声明环境,但不能撤销任意服务副作用或数据迁移。例如,无论更改是像启用系统服务这样简单,还是像使用新版本的 Glibc 重建整个系统这样复杂,您都可以通过以下方式进行测试:

nixos-rebuild test

此操作会构建并激活新配置,但不会将其设置为默认启动配置。因此,重启系统后,系统将采用已有启动默认配置,前提是该配置仍可用。

对适合虚拟机验证的模块,可采用以下方案:

nixos-rebuild build-vm
./result/bin/run-*-vm

此命令会构建并启动一个包含新系统配置的虚拟机(即主机配置的克隆,并包含您对 configuration.nix 文件所做的所有更改)。该机制可以共享宿主机的 Nix store,VM 的可写根文件系统与主机系统激活分开;也可能通过配置额外挂载目录,不能把 VM 视为绝对无数据共享。按当前构建产物脚本和 virtualisation 选项核实磁盘、网络及共享目录。

系统回滚≠配置回滚#

Generation 回滚 和 配置文件本身 是两回事。

回滚的是:  Nix store 里构建好的系统(二进制结果)
不回滚的是:/etc/nixos/configuration.nix 这个文本文件

具体发生了什么

你的操作流程:

configuration.nix (v2)
       ↓ nixos-rebuild switch
   Generation 42  ← 当前启动
   Generation 41  ← 上一个

nixos-rebuild switch --rollback
       ↓
   Generation 41  ← 当前启动
   Generation 42  ← 还在 nix store 里

但是 /etc/nixos/configuration.nix 还是 v2 的内容!

也就是说,你回滚之后如果再 nixos-rebuild switch,会重新部署 v2,又回到出问题的状态。 所以正确的工作流是配置文件用 Git 管理,这样两个维度都有历史记录:

cd /etc/nixos
git init
git add .
git commit -m "working config baseline"

# 改了配置
git add configuration.nix
git commit -m "try enabling some service"
nixos-rebuild switch

# 出问题了
nixos-rebuild switch --rollback   # 系统恢复
git revert HEAD                   # 配置文件也恢复

Generation 负责系统状态的回滚,Git 负责配置意图的回滚,两者配合才完整。 build-vm 更干净,VM 里的系统跟宿主机的 generation 完全隔离,宿主机的 generation 列表根本不会变,所以这个问题在 build-vm 场景下不存在。适合 VM 的服务和模块可以先这样验证;真实硬件、图形及驱动效果仍需要对应环境验证。

Arch (命令式) vs NixOS (声明式)#

"作为 Arch 用户,你习惯了‘改变系统状态’。在 NixOS,你优先通过声明‘描述系统状态’,可写运行时状态仍存在。"


核心差异对照表#

维度 Arch Linux (命令式) NixOS (声明式)
安装软件 sudo pacman -S git 在 environment.systemPackages 中添加 git
文件修改 手动修改 /etc/fstab 在 fileSystems 中定义挂载点
系统状态 当前状态是过去所有命令的累加 当前状态是配置文件的计算结果
回滚 依赖包历史、备份或文件系统快照等额外机制 保留系统代际时可切换旧闭包;数据另行恢复
副作用 修改文件是即时的、有副作用的 修改配置不会生效,除非运行 nixos-rebuild

NixOS 的“纯净”本质#

1. /nix/store:只读的世外桃源

在 Arch 中,/bin/git 就是那个二进制文件。在 NixOS,系统环境中的 Git 通常通过 /run/current-system/sw/bin/git 等 profile 链接访问,它指向一个类似这样的路径: /nix/store/h3l9...-git-2.43.0/bin/git

  • 哈希前缀:代表了构建该软件的所有依赖和配置。如果依赖变了,哈希就变了。
  • 不可变性:你不能修改 /nix/store 里的任何东西。这减少了覆盖包文件的风险,但不能避免激活错误、数据损坏或管理员强制改动。

2. /etc 的真相

NixOS 中的 /etc 绝大部分文件都是只读软链接。

  • 命令式思维:想改 SSH 配置?打开 /etc/ssh/sshd_config 修改。
  • 声明式思维:在 configuration.nix 中写 services.openssh.settings.PermitRootLogin = "no";,Nix 会自动帮你生成软链接到正确的配置文件。

实战练习:从 Arch 逻辑转换#

Arch 命令:

sudo pacman -S hello
sudo systemctl enable --now docker

NixOS 声明: 在 configuration.nix 中:

{ pkgs, ... }: {
  environment.systemPackages = [ pkgs.hello ];
  virtualisation.docker.enable = true;
}

运行生效:sudo nixos-rebuild switch


给 Arch 用户的建议#

优先修改负责生成该文件的 Nix 模块,避免编辑生成文件导致配置与意图脱节。/etc/nixos 的源文件和其他可写、未托管文件另当别论。


关于Nix这个工具本身#

nix-env 的设计背景#

nix-env 是 Nix 最早期的用户界面,2003年左右随 Nix 本身一起诞生。


设计目的#

Nix 最初不是为 NixOS 设计的,而是作为一个可以装在任何 Linux/macOS 上的包管理器。

设想的场景是:

你用的是 Ubuntu / macOS
但你想要:
  - 多版本共存(python2 和 python3 同时装)
  - 不需要 root 权限装包
  - 可以回滚
  - 不污染系统的 /usr/lib

nix-env 就是为这个场景设计的,对标的是 apt install / brew install,给 Nix 用户提供 profile 管理,NixOS 上也能使用。

nix-env -iA nixpkgs.firefox    # 传统 channel/profile 的用户安装示例
nix-env -e firefox    # 从该用户 profile 移除
nix-env --rollback    # 回滚该用户 profile

它解决了什么真实问题#

多版本共存:

# 同时装两个版本,互不干扰(不同版本的 store 路径可共存;同一 profile 的同名程序还可能冲突)
nix-env -iA nixpkgs.python2 # 历史版本示例,不保证当前 nixpkgs 提供
nix-env -iA nixpkgs.python3

无 root 安装:

# 普通用户在服务器上装自己的工具,不需要找管理员
nix-env -iA nixpkgs.ripgrep

Nix 的早期设计已经围绕隔离包路径和 profile 回滚展开。


声明式管理与命令式入口的权衡#

时代变了,更好的替代品出现了:

nix-env 的问题 替代方案
命令式,状态不在文件里 home.nix 声明式管理
多版本 nixpkgs 混用,难以追踪 Flakes 锁版本
临时试用不必修改长期 profile nix-shell -p 或 nix shell 不改长期 profile;产物仍可能留在 store

nix-env 面向传统用户 profile,nix profile 面向新 CLI 的 profile;两种格式不能任意混用。在 Home Manager 已管理稳定软件集合时,可以减少额外命令式安装入口。


管理入口的选择#

nix-env 是"在非 NixOS 系统上用 Nix 管包"的早期解法,稳定用户环境可由 Home Manager 管理,传统 profile 和 channels 工作流仍有用途。



前半部分依据 NixOS 官方工作原理说明 整理,历史示例仅用于解释机制。

参考与延伸

  1. NixOS 手册 ↗
  2. Nix 语言手册 ↗
  3. NixOS 工作原理(原笔记出处) ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

Nix 语言与构建模型

  1. 01Nix 与 NixOS:存储、回滚和声明式思维
  2. 02Nix 语法:属性集、列表与内置函数
  3. 03Nix 函数与作用域
  4. 04Nix 属性集:递归与合并
  5. 05Derivation:从表达式到构建产物
CONTINUE READING / 下一篇Nix 语法:属性集、列表与内置函数

掌握常见数据类型、属性访问和配置表达式的基本写法。