Skip to main content

尝试评估 Preservation 解决“到处拉屎”问题

22 min read
    - date: 2025-12-23
des: |
移除掉【nix-community/impermanence(Impermanence 的核心思想是:除明确声明需持久化的内容外,其余一切均在重启后丢弃。这迫使你以完全声明的方式定义系统的所有持久状态。Impermanence 通过 绑定挂载(bind mount) 或 符号链接(symlink),将你指定的文件或目录从持久化存储位置(如一个单独的硬盘分区或Btrfs子卷)链接到系统期望的位置(如 /var/lib或 $HOME/.config)。其配置主要围绕 environment.persistence这个 NixOS 选项(系统级别)和 home.persistence这个 Home Manager 选项(用户级别)展开。Impermanence 模块是目前 NixOS 生态中最接近你“全生命周期声明式管理”理念的工具,它允许你精确控制系统中哪些内容可以跨越重启得以保留,从而实现高度的可重现性和整洁性)】

这里我想具体写一下,为啥我之前非常想用这个imp,现在却弃如敝履。从而回答“是否推荐使用 impermanence?”。那么需要先思考几个问题:

- impermanence 的边界?是否不推荐在desktop使用,更推荐在server上使用? # 仍然是 应用和distro本身工具之间的边界。比如说需要用Dokploy来托管应用,是否
- 是否是一种“虚假的掌控感”?其实很尴尬的一个事情就是,如果我们总是根据这些服务产生的文件,来逐个添加相应path,那么反而是多此一举,不是吗?
- 一个矛盾:更推荐使用server上使用impermanence,但是如果是 server 的话,默认是长时间(通常是数月甚至数年)都不会重启的。而其需要重启后,才能reset。所以这里就冲突了。对吗?
- 我有在nix-darwin 和 nixos 之间复用这个配置的需求,能否实现?另外,这个 impermanence 是只支持 modules,还是说在 hm里也能使用?
- 使用 impermanence 后,会产生哪些 unknown unknown 的问题?

TLDR

其实我真正想要的就是,启用某个服务,该服务的相应配置项就自动symlink到 target path,当我禁用该服务后,相应 target path 的配置文件就自动移除掉了。那么真的有方案可以实现这个需求吗?相应的代价又是什么呢?该方案本身的实现是否简洁?维护成本高吗?

其实这个就是一切相关问题的开始,下文也是围绕该问题展开。

上面说的这个问题其实就是“到处拉屎”问题,这个痛点毫无疑问是真实的。只不过有些人介意,而有些人不介意罢了。

但是巧了,我对这个问题就非常介意,我确实有“系统洁癖”,我也很讨厌那些乱七八糟的各种“清理软件”,各种这个“大师”、那个“助手”之类的,都是不懂。

所以我选择

当前对“”问题的,但 impermanence 不是合适的主方案,preservation 也仅在特定前提下才有价值。

  • Preservation / Impermanence 解决的并不是“自动打扫卫生”,而是“把哪些状态允许跨重启保留”这件事显式化。
  • 只有当根文件系统本身是 volatile root 或可 rollback root 时,才接近“白名单之外,重启后全部消失”的语义;如果 / 仍是普通持久分区,这个理解就是错的。
  • 对我当前这套配置而言,它在 nixos-vps 这条线上开始有工程价值,但前提仍然是先盘清状态边界,而不是直接沉迷于工具本身。
  • 真正更适合我仓库的落地方式,不是纯 host-local,也不是把 preservation 原语散落到每个模块里,而是“模块声明状态 ownership,顶层统一装配 preservation”。

动机/需求

开篇名义,我确实有系统洁癖,并且对这种“到处拉屎”的问题非常难受、非常讨厌,这也是我最初尝试使用NixOS的初衷(当然目前证明我一开始确实想多了)

但写到今天,我已经不太愿意把这个问题简单理解成“系统不够干净”。更准确一点说,我真正不舒服的不是单个缓存目录的存在,而是系统里不断产生一批我并没有明确选择、也没有清晰语义边界的状态。这种不适感,一开始会很自然地把人推向 ImpermanencePreservation 这类工具,因为它们看起来提供了一种足够彻底的治理方式。

问题也恰恰在这里。越往后看,我越觉得这类方案的诱惑,不只是“我想把机器弄得干净”,而是“我想重新定义哪些状态有资格存在”。它不是简单的清理,而是一种更激进的系统观。

所以这篇文章想回答的,其实不是一个单纯的工具选型问题,而是几个缠在一起的问题:

  • 这类工具到底在解决什么问题?
  • 它们是不是有点“脱裤子放屁”,只对系统洁癖用户有吸引力?
  • PreservationImpermanence 有什么关键差异?
  • 对我自己的仓库和 nixos-vps 现实而言,它到底有没有工程价值?
  • 如果真的要做,结构上应该怎么做,才不会把系统搞成另一种失控?

决策过程

约束条件

  • 当前先只讨论 NixOS,不讨论 nix-darwin 复用问题。
  • 短期的评估对象聚焦在 nixos-vps,而不是整个仓库一次性改造。
  • 长期目标并不是让某一台机器单独实验 preserve,而是希望所有 NixOS host 最终都能走到这一套状态治理模型。
  • 我不希望把所有路径白名单都堆到 host 层做成一个越来越大的清单;更理想的是“谁产生态,谁顺手维护对应的状态声明”。
  • k3s 这类重量级状态域先单独看,不和一般服务目录一视同仁。

这类工具实际解决的是什么问题?

我后面越来越倾向于用“状态边界显式化”来描述这类工具,而不是“系统变干净了”。Preservation / Impermanence 真正解决的,是把哪些状态允许持久化、哪些状态应该随着重启丢弃,明确地写进系统配置里。

这点听起来很像“白名单治理”,但需要补一个前提:只有根文件系统本身就是易失的,或者能在开机时回滚到干净快照时,这种白名单语义才成立。如果 / 仍然是普通持久化分区,那么它们更像是在声明“哪些路径是必须认真对待的持久状态”,而不是自动定义“其余所有路径都会被清理掉”。

换句话说,preservation 里定义的路径,并不天然等于“系统上唯一能活下来的东西”。只有同时满足下面两个条件时,才接近这种理解:

  • 根文件系统本身会在重启后回到干净状态。
  • Preservation / Impermanence 只把白名单路径重新接回来。

如果没有这个前提,它们最多只能表达“这些路径我要认真建模、认真保留”;不能表达“其余路径都将在重启后归零”。

所以,如果只是单纯想解决“目录太脏、缓存太多、软件到处写文件”这种烦躁感,这类工具其实并不是第一选择。更直接的手段仍然是 XDG 收口、systemd.tmpfiles、定时清理、服务状态目录约束,以及快照/备份。也正因为如此,我现在不会再把它们当成通用卫生工具,而更愿意把它们看作一种更强、更激进、也更有前提的状态管理策略。

Preservation > Impermanence

tip

这部分用来对二者做个简要对比,并说明为啥更推荐用 Preservation 而非 Impermanence 来解决“到处拉屎”的问题。

nix-community/preservation

nix-community/impermanence

如果只看理念,两者都在做“声明式的持久状态管理”;但如果看今天的实际定位,我会更偏向 Preservation

我对二者的区分,大致是这样的:

  • Impermanence 更像是一套围绕 ephemeral root 发展出来的传统方案。它的核心思路很直接:根本不假设系统天然干净,而是要求你显式声明哪些路径要跨重启保留。
  • Preservation 同样受这个思路启发,但它更强调“declarative management of non-volatile system state”,也就是把“非易失状态本身”建模成一等对象,而不是只把它当作易失根系统的补丁配套。

如果进一步拆开看,我当前更偏向 Preservation,主要是下面几个原因。

1. 定位更像“状态建模”,而不是“易失根配套”

Impermanence 的语义重心,天然还是围绕“根是易失的,所以我要把保留下来的路径重新挂回来”。这并没有错,而且很多时候它就是最直接的表达。

Preservation 更强调“非易失状态本身”是系统配置的一部分。这个细微区别对我很重要,因为我真正想做的并不是“把系统洗白”,而是“把持久状态的边界变成一等配置对象”。

2. 表达更显式,隐式魔法更少

我越来越不喜欢“好像能工作,但背后有一层我不太想碰的隐式行为”的方案。Impermanence 虽然也能完成相近目标,但历史包袱更重,隐式行为更多,尤其一旦牵涉 HM、挂载时序、symlink/bind mount 语义和其他模块联动时,问题就会变得比较黏。

Preservation 对“哪些路径重要、为什么重要、怎么保留、是不是 early boot 关键路径”的表达方式,整体更像一套认真建模的体系,而不是一层方便但较魔法的胶水。

3. 它更贴近我现在想要的结构方向

我现在更在意的不是“这个语法短不短”,而是它能不能自然支持下面这种结构:

  • 模块声明自己拥有哪些状态。
  • 顶层统一收集、归并、翻译成最终 preservation 配置。
  • host 只保留少量和布局有关的决策。

从这个角度看,Preservation 更适合被当作一个顶层 assembly 的目标格式,而不是散落在各处的一堆临时 glue。

不过,这里的结论也要收住一点。更偏向 Preservation,并不等于它自动更适合解决“到处拉屎”。更准确地说,它只是更适合在“我已经决定认真做状态治理”这个前提下,作为表达模型来使用。

结合我自己配置的评估

如果只谈抽象原则,这篇文会很容易滑向空谈。

真正让我改变看法的,还是把问题代回我自己的配置之后,发现很多原先看起来很优雅的想法,其实都需要重新估算成本。

它到底是不是有点“脱裤子放屁”?

有一点,但不能简单这么说。

更准确的描述应该是:

  • 对大多数机器,尤其是普通 desktop,Preservation / Impermanence 的收益经常不值得复杂度,更多像“系统洁癖工具”。
  • 对少数场景,它不是洁癖,而是明确的系统策略:
    • 你就是要 ephemeral root。
    • 你要把“允许持久化的状态”做成白名单。
    • 你要降低配置漂移和手工残留。
    • 你要让重装、回滚、迁移更机械化。

所以它不是天然多余,而是“只有在你真的认同这套状态观时才有价值”。对我这套仓库而言,如果只看 nixos-vps 这条线,它已经开始接近“有明确工程价值”;但对 desktop / darwin,这种价值就弱得多,更像洁癖而不是优先级很高的工程需求。

简单结论:仅对 server 这

为什么先看 nixos-vps

当前真正值得讨论的目标并不是“整个仓库是否应该马上全面 preserve”,而是 nixos-vps 这条线是否已经值得开始实践。

这个 host 和我原先设想中的“理想化实验环境”差异很大:

  • 它是长期运行的 VPS,不是动不动就重启的测试机。
  • 它上面还挂了明显会产生持久状态的服务。
  • 如果把 k3s 算进来,那么要处理的已经不是几个零散路径,而是一整块很重的状态域。

也正因为如此,它反而比 desktop 更适合作为第一批认真评估的对象。不是因为 VPS 更“干净”,而是因为它的状态边界更值得被认真建模。

为什么不能把它继续当作“更彻底的整洁方案”

我之前很容易把“声明式状态管理”想象成一种更彻底的整洁方案,但结合实际配置后,我反而更明显地看见了边界:很多服务本来就应该老老实实把状态放在 /var/lib/.../var/cache/.../run/...,这时真正应该做的首先是把状态路径收口、把服务语义理顺,而不是上来就引入 preservation 语法。

换句话说,工具并不能替代建模本身。

如果一个服务现在本来就在奇怪的位置产生态,那优先级应该是先把它收口到合理位置;如果一个用户态程序只是因为历史原因把东西堆在 $HOME 根目录,那优先级也不是直接 preserve 它,而是先搞清楚它到底是 config、state、data 还是 cache。

为什么 HM 用户态不能跳过

虽然这轮我先把重点放在 nixos-vps,但一旦真的要把 preserve 当成全仓库 NixOS host 的长期方向,HM 里的历史遗留路径、各种 mixed state、以及“哪些是值得保留的真实状态、哪些只是因为历史原因落在 $HOME 根目录里的残留物”,都会变成必须面对的问题。

我查过这台 VPS 的 HM 组合:

  • 引了 home/base/core
  • 引了 home/extra/zed-remote.nix
  • 没引 home/nixos/xdg.nix

所以这台 VPS 的 HM 目前不是 XDG 化主线,而是明显的 mixed state:

  • ~/.ssh/config
  • ~/.zed_server
  • zsh 历史和 dotdir 还在 home 根
  • atuin
  • zoxide
  • nushell sqlite history

这正是为什么我现在不会再说“先完全不碰 HM”。不碰可以作为阶段排序,但不能当作结构判断。因为对“到处拉屎”的主观感受而言,HM 反而是重灾区。

具体怎么实现?

对 Preservation 的使用有三种方案:

  • 纯 host-local:所有 preserve path 都堆在 host。
  • 纯 module-owned:谁产生态,谁直接把 preservation 写死在自己的模块里。
  • 调和方案:ownership 下沉,assembly 上收

先直接排除掉 host方案,原因跟“为什么在 hm/modules 里面都把相应配置根据topic分门别类地放到一起,并做配置化启用”的道理是一样的。你很难想象一个人的 nix配置,把所有配置都堆到host里,那跟没做分层有啥区别?local 就不是用来干这个的。如果你这么做了,会出现什么问题?

  • 很容易变成一个越来越大的白名单文件。
  • 模块改了状态路径,host 忘改。
  • ownership 分散,长期会腐化。

方案 B:纯 module-owned

它吸引人的地方也很明显:

  • 谁产生态,谁负责声明。
  • 模块改了,不容易漏。

但问题在于,它会把两类本来不在一个层级的信息混在一起:

第一类是“模块事实”:

  • 这个模块会产生什么状态。
  • 这些状态是必须持久化,还是可以丢弃。
  • 这些路径的语义是什么。

第二类是“宿主机策略”:

  • 这台机器是否使用 preservation。
  • 持久卷挂在哪里,比如 /persistent/state 还是别的。
  • 用 symlink 还是 bindmount。
  • 是否要在 initrd 阶段准备。
  • 这台 host 到底要不要启用这些状态声明。

前者适合跟模块 colocate,后者不适合下沉到通用模块里。否则模块会慢慢带上 preservation worldview,最后不是更优雅,而是更难维护。

方案 C:ownership 下沉,assembly 上收

我现在最终接受的,是一种中间结构:

  • 模块声明“我有哪些候选状态”。
  • 顶层统一收集这些声明,翻译成 preservation 配置。
  • host 只保留少量与布局相关的决策。

如果用一句话总结,就是:

ownership 下沉,activation / assembly 上收。

这个结构更适合我仓库的原因很简单:

  • 状态知识跟着模块走,改模块时不容易漏。
  • 顶层仍然保留汇总视图,不会失去整体可读性。
  • 持久卷布局可以统一调整,不会因为散写 preservation 原语而难以迁移。
  • review 时也更容易判断:某台机器到底 preserve 了什么、为什么 preserve。

这会如何映射到我的仓库

如果落到实际目录结构,这个思路会更具体一些。

对于 NixOS service modules,例如:

  • modules/nixos/base/tailscale-client.nix
  • modules/nixos/vps/singbox-server.nix
  • modules/nixos/extra/singbox-client.nix

这些模块内部可以顺手维护自己的状态声明,例如:

  • 这个模块的 preserve candidates 是哪些 path。
  • 哪些是 boot-critical。
  • 哪些更适合 symlink,哪些更适合 bindmount。
  • 哪些路径只有在某个 option 打开时才存在。

但我不希望模块直接决定最终的 preservation.preserveAt."/state" 之类的布局。因为 /state 这种前缀,不是模块事实,而是顶层布局决策。

对于 HM modules,这种 ownership 下沉反而更重要。像:

  • home/base/core/ssh.nix
  • home/base/core/zsh.nix
  • home/base/core/nushell.nix
  • home/extra/zed-remote.nix

这些模块本身最清楚:

  • ~/.ssh 到底是配置还是关键状态。
  • zsh historyatuinzoxidenushell sqlite history 到底哪些值得保留。
  • ~/.zed_server 到底更像 cache、state 还是必须保留的工作目录。

这比把所有用户态路径一股脑堆回 host 里,当一个大白名单,长期要健康得多。

Risks and Unknown Unknowns

如果说前面那些内容讨论的是“我想不想要这套模型”,那这部分讨论的就是“我真正上手后最可能低估什么”。

1. root 语义其实还没定义清楚

这是最大的 unknown unknown。

我现在讨论 preservation 很多,但 nixos-vps 目前还是 ext4 /。也就是说,我其实还没有先回答下面这个更基础的问题:

  • 我是要真正做 ephemeral root。
  • 还是只想先把持久状态白名单化。
  • 还是两步走,先白名单化,后面再改 root 语义。

这三者工程量差很多,风险模型也完全不一样。

2. early boot / initrd 依赖不是“小问题”

这类文件不是“以后再补”就行,漏了可能直接出事故:

  • /etc/machine-id
  • SSH host keys
  • random-seed
  • 某些必须在很早阶段就出现的系统身份文件

这类问题的风险不是“配置不优雅”,而是“机器启动后身份异常、SSH 不稳定、服务行为怪异”。如果没有先把 boot-critical 状态识别出来,后面所有关于 preserve 的讨论都很容易流于表面。

3. 服务状态目录可能比想象中大得多

我现在已经能明显识别一些路径:

  • /var/lib/acme
  • /var/lib/derper
  • /var/lib/sing-box

但还有一类状态不是我自己显式写死的,而是上游模块和服务偷偷生成的。典型就是:

  • tailscaled
  • k3s
  • 以及它们关联的 runtime、socket、generated config、certificate 之类的周边状态

尤其 k3s,我现在仍然维持同一个判断:它不是小 path 集合,而是一整块状态域,大概率就是 /var/lib/rancher/k3s 级别的东西。它应该整体决策,而不是零敲碎打。

4. HM 用户态很容易在 preserve 时变丑

最危险的不是漏掉某个 cache,而是把“历史上随手落在 home 根目录里的东西”误判为关键状态,最后把 preservation 写成了另一个版本的整盘搬家。

也就是说,在真正把 HM 用户态大面积纳入之前,我很可能先需要一轮“状态语义清洗”。否则看起来像是在做声明式治理,实际上只是把历史残留原封不动搬进白名单。

这不是“选哪个写起来更顺手”的问题,而是行为差异。

不同程序对 symlink 和 bindmount 的兼容性并不一样,权限、owner、mode、父目录存在性、原子 rename 等细节都可能成为坑。有些文件更适合 symlink,有些目录更适合 bindmount,而这类问题往往只有真的一条条接入时才会暴露。

6. 我可能低估了恢复和迁移成本

一旦用了 preservation,不只是“系统更干净”这么简单,它会反过来要求我想清楚:

  • 持久卷怎么备份。
  • 重装时怎么挂载回来。
  • root 换布局时怎么迁移 preserve root。
  • 哪些状态该跨机器复制,哪些不该。

例如:

  • machine-id 就不该跨机器复制。
  • SSH host key 也不该乱复制。
  • atuin、某些 user state、某些 app db 可能我又想保留。

这会逼着我面对“状态可迁移性”这个以前没认真定义过的问题。

7. 最大的观念误区,是把它当清理工具

这是一个很容易出现的认知偏差。

它不是 tmpfiles --clean 的升级版。它解决的是“状态边界显式化”,不是“定时打扫卫生”。

如果我真正想解决的是:

  • Downloads 太乱
  • cache 太多
  • 各种临时文件膨胀

那最有效的工具仍然是:

  • XDG 化
  • systemd.tmpfiles
  • 定时清理
  • 服务目录收口

而不是 preservation 本身。

最终方案

如果把前面的所有判断压缩成一个最终方案,那我现在认可的是下面这条路线。

结构结论

  • 不把 Preservation / Impermanence 当作通用“卫生工具”,而把它看成一种更激进的状态治理手段。
  • 结构上采取“模块声明状态 ownership,顶层统一装配 preservation”的方式,而不是纯 host-local 大白名单,也不是把 preservation 原语无差别散落到每个模块里。
  • host 层仍然保留少量机器布局职责,但不负责承担全部状态语义。

如果只用一句话总结,就是:

各模块/HM 声明自己的持久状态契约,统一由 preservation assembly module 汇总,host 只负责启用与布局。

为什么这是我现在认为最优的结构

因为它同时满足了几个我现在已经不想放弃的要求:

  • 可维护性来自“声明跟模块走”。
  • 汇总视图来自“顶层统一装配”。
  • 布局灵活性来自“host 仍然保留少量布局决策”。
  • review 可读性来自“没有把 preservation 原语散落到全仓库”。

这已经不是“纯 host-local”,也不是“纯 module-local 自动生效”,而是一个更稳定的中间形态。

Phase 1

第一阶段,我只会接入那些“明显有意义且边界清楚”的状态。

System / Service:

  • machine identity
  • ssh host keys
  • acme
  • derper
  • sing-box

HM:

  • ssh
  • zsh history
  • atuin
  • zoxide
  • nushell history
  • zed-remote

这一步的目标非常明确:

  • 先把明显有意义的状态纳入。
  • 不碰 k3s
  • 不碰大块 cache。
  • 不碰语义模糊的目录。

Phase 2

第二阶段,单独评估 k3s

重点看:

  • /var/lib/rancher/k3s
  • local-path / storage
  • container runtime 关联状态
  • server / agent 角色切换带来的边界问题

我不打算在第一阶段把 k3s 一起塞进来,因为它几乎肯定会把简单问题变复杂。

Phase 3

第三阶段,再考虑是否把更多 HM 状态和某些 app state 纳入。

这里的前提不是“能 preserve 就先 preserve”,而是要先回答:

  • 这些状态真的有保留价值吗?
  • 它们是 config、data、state 还是 cache?
  • 它们是本机身份的一部分,还是可迁移用户状态?

只有这些语义先清楚了,后续的白名单才不会越来越丑。

结论

我现在的结论已经和最初的直觉不太一样了。最开始我会把这类工具想象成一种更彻底、更纯粹的整洁方案,仿佛只要把状态白名单写好,系统就终于不会再“到处拉屎”;但结合我自己的配置、服务边界和 host 现实之后,我更愿意承认:它们首先是一种严格的状态治理模型,而不是一个通用清洁工具。

所以,问题已经不再是“要不要迷信某个工具”,而是“我是否真的愿意接受这套模型的前提与成本”。对普通机器、尤其是 desktop 来说,这套东西很容易带有明显的系统洁癖色彩;但对像 nixos-vps 这样有长期运行、状态边界、服务职责明确需求的 host 来说,它开始具备真正的工程意义。

如果再压缩成几个最重要的判断,大概就是:

  • 这套方案不是单纯洁癖,但确实只对一部分人、一部分机器有高价值。
  • 它不是天然“白名单之外全部重启清空”,除非先把 root 做成 volatile / rollback root。
  • 真正难的不是 preservation 语法,而是 root 语义、HM 状态语义、以及 k3s 这种大状态系统的边界。
  • 对我自己的仓库而言,更合理的方向不是“把所有 preservation 逻辑塞进一个地方”,而是“让状态知识跟着模块走,让最终装配回到顶层”。

因此,我现在不会简单地说“人人都该用 preservation”,也不会反过来说“这纯属脱裤子放屁”。更准确的表述应该是:如果你只是想解决脏乱,它通常不是最优先的工具;但如果你真正想把“哪些状态应该存在、为什么应该存在、它们如何跨重启延续”这件事声明式地固定下来,那么它就是一条值得认真评估、也值得小心落地的路线。