My First Impressions of Nix

Michael Lynch

我对 Nix 的初体验

原文由 Michael Lynch 发布,订阅该博客

Nix 是一款根据源文件来配置软件环境的工具。最近在 Hacker News 和 Twitter 上越来越频繁地看到关于 Nix 的讨论。这个想法很吸引我,所以过去几周一直在捣鼓它。

我与基础设施即代码的渊源

十年前,我接触到了 Salt,这是一款能让你用源代码来定义计算机系统配置的工具。我很喜欢这种用一个 git 仓库来定义我的电脑和虚拟机上安装了哪些服务的理念。即使把电脑彻底清空,只要重新运行一遍配置工具,就能恢复到原来的状态。

我摆弄了几年 Salt,直到后来发现了 Ansible,我觉得它把同样的理念实现得更好。

我所有的开发工作都是在家庭实验室服务器上的虚拟机里完成的。每个项目单独一个虚拟机,全部用 Ansible 来管理。

Ansible 的问题

Ansible 最大的问题是慢得让人痛苦。在我的某台虚拟机上跑一次,通常要花 10 到 15 分钟。

比如我想安装一个新的 apt 软件包 foo。我是直接运行 sudo apt install --yes foo,5 秒钟就装好?还是打开我的 Ansible role,修改配置加上一条安装 foo 的步骤,然后运行 playbook,再等上 15 分钟?显然,我更多时候会选择前者,结果就是实际环境和本应描述环境的 Ansible 文件渐渐偏离了。

另一个问题是 Ansible 的变更不向后兼容。所以,如果我想在 playbook 里用上 Ansible 的新特性,就得把所有的 playbook 都改成兼容的版本。但我从来没有动力去把所有 playbook 重写并重新测试一遍,结果就只能困在旧版本上。我现在还停留在三年前发布的 Ansible 2.9 上。

Nix 的吸引力

我看到越来越多的人在谈论 Nix 和 NixOS。我关注的不少开发者都在分享他们尝试 Nix 的经历。

其中影响最大的一条推荐来自 Mitchell Hashimoto,他是 Hashicorp 的联合创始人,这家公司打造了许多被广泛使用的开源基础设施工具,比如 Vagrant、Packer、Consul 和 Terraform。他称 Nix 是“近年来我学到的、对我产生最积极影响的技术,没有之一”。

到目前为止我的 Nix 之旅。我依然认为它是近年来我学到的最具积极影响的技术。

Nix 的理念和 Ansible 很像。Nix 让你用代码定义配置,然后让系统进入相应的状态。

Nix 与 Ansible 的对比

Nix 和 Ansible 有几处重要的不同,这正是我觉得有意思的地方。

Nix 比 Ansible 更快

Ansible 没有“状态”的概念。如果在一台虚拟机上跑一次 Ansible 要花 15 分钟,那么一分钟后再跑一次同样的 playbook,大概还要花 10 分钟。两次运行之间可能没有需要更新的软件包,能省一点时间,但几乎所有的工作都得重做一遍。

Ansible 永远不会说:“哦,我刚配置过这台机器,现在没什么要做的了。”因为自上次运行以来任何事情都可能发生,所以它必须把每一项配置都重新执行一遍。

而 Nix 则有状态的概念。如果你在 200 行的 Nix 配置里只改了一行,它不需要把另外 199 行的工作重做一遍。它可以对照配置文件评估系统的当前状态,识别出只需应用那一行的改动。而这个改动通常几秒钟就能完成。

编辑(2023-06-19):一位更有经验的读者澄清说 Nix 并非像我以为的那样通过“状态”来实现快速

Nix 之所以快,不是因为它是有状态的,而是因为它是函数式且可复现的,这使得它可以在不牺牲正确性的前提下利用缓存。

我原本以为 Nix 会记录是哪些任务让系统达到了哪种状态。例如,执行任务 X 和 Y 会到达状态 A,而执行任务 X、Y、Z 则会到达状态 B。我以为处于状态 A 的 Nix 系统要到达状态 B,只需执行任务 Z 就行。

而我现在了解到的是,如果你让处于状态 A 的 Nix 系统去到状态 B,Nix 实际上会执行任务 X、Y 和 Z,但由于任务 X 和 Y 的结果已被缓存,所以几乎是瞬间完成的。

Nix 为本地配置而优化

Ansible 被设计为通过网络去配置系统。你当然也可以把目标指定为 localhost,但那并不是 Ansible 所优化的场景。

Nix 则是为配置其所处的环境而设计的。在 Nix 中,你定义好环境里应该有什么,然后 Nix 就在本地为你创建出这个环境。在 NixOS 中,你定义的是整个操作系统,NixOS 会让操作系统进入你所定义的状态。你甚至可以改动文件系统、Linux 内核或引导程序这类底层的东西。

这就解决了我在使用 Ansible 时遇到的问题:把一个 Ansible playbook 升级到新版 Ansible,就得把系统上所有的 playbook 都升级。你可以让许多运行着不同版本 Nix 的 Nix 系统并存且互不干扰。但如果有些 Ansible 文件需要 Ansible 2.9,有些需要 2.10,还有些需要 2.14,那要同时维护它们就会非常麻烦。

Nix 的变更是原子性的

用 Ansible 时,很容易在配置进行到一半时失败,让系统处于一种未定义的状态。

而使用 Nix,变更是原子性的。要么 Nix 让你的系统成功进入期望的状态,要么就回滚到你尝试修改之前的那个状态。

对我有帮助的 Nix 学习资源

关于 Nix,最常见的抱怨之一就是文档缺失、错误或质量糟糕。我的感受是,现有的文档似乎都是写给有经验的 Nix 用户看的。

我找到的很多 Nix 文档都会写类似“只需加上这几行!”这样的话。

啊?

哪个文件?加在文件的什么位置?

以下是我目前找到的最好的几个资源:

第一次失败尝试:在虚拟机中安装 NixOS

我在 Proxmox 虚拟机上跟着“NixOS for the Impatient”这篇教程操作,一开始一切正常。接着,做到需要修改主机名并重启生效的那一步时,虚拟机却进入了一种奇怪的状态——看起来能启动,但无法登录。在登录界面输入密码后,屏幕就直接卡死了。

NixOS 已成功安装在我的 Proxmox 虚拟机服务器上,但在第二次启动登录后就卡死了。

第二次失败尝试:在 Raspberry Pi 4 上安装 NixOS

既然虚拟机行不通,我觉得下一个合乎逻辑的选择就是装在物理机上。手头正好有一台闲置的 Raspberry Pi 4,觉得拿它来做实验会很有意思。

我找到了两份看起来很官方的、在 Raspberry Pi 4 上安装 NixOS 的教程:

这两份教程的问题在于,它们都假设你已经在运行 Nix 环境了。而我是在主力机——一台 Win10 系统——上准备 microSD 卡的,所以根本还没有 Nix。

NixOS 下载页面上列出了一个 64 位 ARM 镜像。Raspberry Pi 4 支持 64 位 ARM,所以我想试试这个。

我打开了 Balena Etcher,这是我常用的烧录 microSD 卡的工具。第一个危险信号是 Etcher 基本上在说:“你在想什么?这根本不是一个可启动的镜像。”

缺少分区表。看起来这不是一个可启动的镜像。该镜像似乎不包含分区表,可能无法被你的设备识别或启动。

但我还是继续下去了!不过当我尝试用这张 microSD 卡启动时,树莓派也认为这不是一个可启动的镜像,于是就卡住了。

树莓派启动界面显示“Progress: Trying boot mode USB-MSD”

我又用官方的 Raspberry Pi Imager 工具重新烧录了一遍同样的镜像,结果还是一样。

更新:我最终成功了

成功:在 Dell Mini 电脑上安装 NixOS

我工作中大部分测试都是在一台 Dell Optiplex 7040 上做的。这是我手头唯一一台可以随便重装的物理机,所以就在它上面试了一下。

一切都和“NixOS for the Impatient”里写的一模一样。整个安装过程从头到尾花了大约 10 分钟,其中 7 分钟只是在复制文件。因为这只是测试设备,我跳过了加密,以免每次重启都要输入密码。

在 Dell Optiplex 7040 上安装 NixOS(文件复制部分已加速)

最后,我得到了一个完整可用的 NixOS 安装!

第三次失败尝试:再次在 Raspberry Pi 4 上安装 NixOS

既然已经有了一台可用的 NixOS 机器,我就想再给树莓派一次机会。之前卡住的原因是没有 Nix 环境来准备 microSD 镜像,现在我有了。

我按照 nix.dev 上的教程操作,这次比第一次走得更远,但仍然无法启动。树莓派只会显示一个彩色画面,然后就卡在那里:

当我从 NixOS 系统烧录 NixOS Pi aarch64 的 microSD 镜像并用它启动树莓派时,它卡在了彩色画面上。

更新:我最终成功了

任务一:开启 SSH 访问

好了,回到 Dell Optiplex 上那个可用的 NixOS 安装。

我需要从主力机通过 SSH 登录到这台 NixOS 系统。为此,得先把我的 SSH 密钥放到系统上。

在 NixOS 上,我打开了 Console 应用,然后输入了 sudo nano /etc/nixos/configuration.nix。接着,找到 environment.systemPackages 并加入了这几行:

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

要让改动生效,我运行了:

sudo nixos-rebuild switch

现在有了 vimcurl,我就可以从 GitHub 上拉取我的 SSH 公钥了:

sudo mkdir -p /etc/nixos/ssh

GITHUB_USERNAME='mtlynch'
curl "https://github.com/${GITHUB_USERNAME}.keys" | \
  sudo tee --append /etc/nixos/ssh/authorized_keys

接下来,我运行 sudo vim /etc/nixos/configuration.nix 并加入了这几行:

  # Enable the OpenSSH daemon.
  services.openssh.enable = true;
  users.users.mike.openssh.authorizedKeys.keyFiles = [
    /etc/nixos/ssh/authorized_keys
  ];

最后,我重新构建并重启了系统。我不确定重启是否真的必要:

sudo nixos-rebuild switch && sudo reboot

成功了!之后我就可以从主力机 SSH 登录到 NixOS 系统了。

任务二:清理 Gnome 自带的杂项

这个系统给我的第一印象就是杂项太多。自带了一堆我不需要的应用,比如通讯录和天气:

我搜索了如何去掉它们,发现它们是 Gnome Shell 的一部分默认应用。你可以在 /etc/nixos/configuration.nix 中加入这一行来禁用它们:

services.gnome.core-utilities.enable = false;

或者也可以逐个移除:

 environment.gnome.excludePackages = with pkgs.gnome; [
    baobab      # disk usage analyzer
    cheese      # photo booth
    eog         # image viewer
    epiphany    # web browser
    gedit       # text editor
    simple-scan # document scanner
    totem       # video player
    yelp        # help viewer
    evince      # document viewer
    file-roller # archive manager
    geary       # email client
    seahorse    # password manager

    gnome-calculator
    gnome-calendar
    gnome-characters
    gnome-clocks
    gnome-contacts
    gnome-font-viewer
    gnome-logs
    gnome-maps
    gnome-music
    gnome-screenshot
    gnome-system-monitor
    gnome-weather
    gnome-disk-utility
    pkgs.gnome-connections
  ];

我选择了“全部禁用”的方案,然后重新构建:

sudo nixos-rebuild switch

瞧!所有杂项都不见了:

任务三:找回系统监视器(失败)

唯一一个我觉得值得保留的 Gnome 工具是 System Monitor。我尝试把它加入到 environment.systemPackages 列表中,但重新构建却失败了:

$ sudo nixos-rebuild switch
building Nix...
building the system configuration...
error: undefined variable 'gnome-system-monitor'

       at /etc/nixos/configuration.nix:130:5:

          129|     curl
          130|     gnome-system-monitor
             |     ^
          131|   ];
(use '--show-trace' to show detailed location information)

我还试了其他可能的名字,比如 gnome-shell-system-monitor,但怎么也找不到正确的安装方法。

编辑(2023-06-19):感谢读者指出,正确的包名是 gnome.gnome-system-monitor。之前我遗漏的是,可以在 search.nixos.org 上搜索软件包。

接下来想弄明白的几件事

我对这几天使用 Nix 和 NixOS 的体验很满意,基本符合我的预期。看起来只要用得好,它会非常强大,但前期需要投入大量时间,还得到处搜集资料。

我目前只是浅尝辄止,下面是接下来想进一步了解的几个方面。

在 NixOS 系统上使用 VS Code Remote SSH

我所有的开发都是通过 VS Code 的 Remote SSH 完成的。当我尝试从 VS Code 远程连接到 NixOS 系统时,安装失败了。VS Code 需要在目标系统上安装某种服务端,而它大概还不知道如何在 NixOS 上安装。

有一个叫 nixos-vscode-server 的 git 仓库,那很可能就是我需要的解决方案。我还没来得及尝试。

Nix 的核心概念是如何协同工作的

我看到“flakes”和“derivations”这类词,目前还不明白它们是什么意思。我也不懂 Nix 语言的语法,不过它和 JavaScript、Python 足够相似,所以目前还能勉强应付。但要真正高效地使用 Nix,显然还得去学习这门语言。

确定性究竟在何时体现?

在关于 Nix 的讨论中,我看到被提及最多的特性之一就是 Nix 是确定性的。

到目前为止,我还没搞懂它是怎么做到确定性的。在指定要安装的软件包时,我既没有指定完整性哈希,甚至连版本号都没写。如果一年后我再运行同样的 Nix 配置,我想得到的会是一个不同的系统,因为它会安装不同版本的 vimcurl

我猜肯定有办法更精确地指定软件包版本,只是我还没学到。

我究竟在信任谁?

在指定软件包时,我写的只是一个简单的包名列表:

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

要能像上面这样指定软件包,Nix 肯定是从某个默认仓库拉取的。那是否存在多个仓库?我又该如何选择使用哪个仓库?

本文章由 muse-spark-1.2-contributor 进行翻译

评论