- 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 的问题?
其实我真正想要的就是,启用某个服务,该服务的相应配置项就自动symlink到 target path,当我禁用该服务后,相应 target path 的配置文件就自动移除掉了。那么真的有方案可以实现这个需求吗?相应的代价又是什么呢?该方案本身的实现是否简洁?维护成本高吗?
其实这个就是一切相关问题的开始,下文也是围绕该问题展开。
上面说的这个问题其实就是“到处拉屎”问题,这个痛点毫无疑问是真实的。只不过有些人介意,而有些人不介意罢了。
但是巧了,我对这个问题就非常介意,我确实有“系统洁癖”,我也很讨厌那些乱七八糟的各种“清理软件”,各种这个“大师”、那个“助手”之类的,都是不懂。
所以我选择
当前对“”问题的,但 impermanence 不是合适的主方案,preservation 也仅在特定前提下才有价值。
Preservation/Impermanence解决的并不是“自动打扫卫生”,而是“把哪些状态允许跨重启保留”这件事显式化。- 只有当根文件系统本身是 volatile root 或可 rollback root 时,才接近“白名单之外,重启后全部消失”的语义;如果
/仍是普通持久分区,这个理解就是错的。 - 对我当前这套配置而言,它在
nixos-vps这条线上开始有工程价值,但前提仍然是先盘清状态边界,而不是直接沉迷于工具本身。 - 真正更适合我仓库的落地方式,不是纯 host-local,也不是把 preservation 原语散落到每个模块里,而是“模块声明状态 ownership,顶层统一装配 preservation”。
动机/需求
开篇名义,我确实有系统洁癖,并且对这种“到处拉屎”的问题非常难受、非常讨厌,这也是我最初尝试使用NixOS的初衷(当然目前证明我一开始确实想多了)
但写到今天,我已经不太愿意把这个问题简单理解成“系统不够干净”。更准确一点说,我真正不舒服的不是单个缓存目录的存在,而是系统里不断产生一批我并没有明确选择、也没有清晰语义边界的状态。这种不适感,一开始会很自然地把人推向 Impermanence、Preservation 这类工具,因为它们看起来提供了一种足够彻底的治理方式。
问题也恰恰在这里。越往后看,我越觉得这类方案的诱惑,不只是“我想把机器弄得干净”,而是“我想重新定义哪些状态有资格存在”。它不是简单的清理,而是一种更激进的系统观。
所以这篇文章想回答的,其实不是一个单纯的工具选型问题,而是几个缠在一起的问题:
- 这类工具到底在解决什么问题?
- 它们是不是有点“脱裤子放屁”,只对系统洁癖用户有吸引力?
Preservation和Impermanence有什么关键差异?- 对我自己的仓库和
nixos-vps现实而言,它到底有没有工程价值? - 如果真的要做,结构上应该怎么做,才不会把系统搞成另一种失控?
决策过程
约束条件
- 当前先只讨论 NixOS,不讨论 nix-darwin 复用问题。
- 短期的评估对象聚焦在
nixos-vps,而不是整个仓库一次性改造。 - 长期目标并不是让某一台机器单独实验
preserve,而是希望所有 NixOS host 最终都能走到这一套状态治理模型。 - 我不希望把所有路径白名单都堆到 host 层做成一个越来越大的清单;更理想的是“谁产生态,谁顺手维护对应的状态声明”。
k3s这类重量级状态域先单独看,不和一般服务目录一视同仁。
这类工具实际解决的是什么问题?
我后面越来越倾向于用“状态边界显式化”来描述这类工具,而不是“系统变干净了”。Preservation / Impermanence 真正解决的,是把哪些状态允许持久化、哪些状态应该随着重启丢弃,明确地写进系统配置里。
这点听起来很像“白名单治理”,但需要补一个前提:只有根文件系统本身就是易失的,或者能在开机时回滚到干净快照时,这种白名单语义才成立。如果 / 仍然是普通持久化分区,那么它们更像是在声明“哪些路径是必须认真对待的持久状态”,而不是自动定义“其余所有路径都会被清理掉”。
换句话说,preservation 里定义的路径,并不天然等于“系统上唯一能活下来的东西”。只有同时满足下面两个条件时,才接近这种理解:
- 根文件系统本身会在重启后回到干净状态。
Preservation/Impermanence只把白名单路径重新接回来。
如果没有这个前提,它们最多只能表达“这些路径我要认真建模、认真保留”;不能表达“其余路径都将在重启后归零”。
所以,如果只是单纯想解决“目录太脏、缓存太多、软件到处写文件”这种烦躁感,这类工具其实并不是第一选择。更直接的手段仍然是 XDG 收口、systemd.tmpfiles、定时清理、服务状态目录约束,以及快照/备份。也正因为如此,我现在不会再把它们当成通用卫生工具,而更愿意把它们看作一种更强、更激进、也更有前提的状态管理策略。
Preservation > Impermanence
这部分用来对二者做个简要对比,并说明为啥更推荐用 Preservation 而非 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_serverzsh历史和 dotdir 还在 home 根atuinzoxidenushell 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.nixmodules/nixos/vps/singbox-server.nixmodules/nixos/extra/singbox-client.nix
这些模块内部可以顺手维护自己的状态声明,例如:
- 这个模块的 preserve candidates 是哪些 path。
- 哪些是 boot-critical。
- 哪些更适合 symlink,哪些更适合 bindmount。
- 哪些路径只有在某个 option 打开时才存在。
但我不希望模块直接决定最终的 preservation.preserveAt."/state" 之类的布局。因为 /state 这种前缀,不是模块事实,而是顶层布局决策。
对于 HM modules,这种 ownership 下沉反而更重要。像:
home/base/core/ssh.nixhome/base/core/zsh.nixhome/base/core/nushell.nixhome/extra/zed-remote.nix
这些模块本身最清楚:
~/.ssh到底是配置还是关键状态。zsh history、atuin、zoxide、nushell 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
但还有一类状态不是我自己显式写死的,而是上游模块和服务偷偷生成的。典型就是:
tailscaledk3s- 以及它们关联的 runtime、socket、generated config、certificate 之类的周边状态
尤其 k3s,我现在仍然维持同一个判断:它不是小 path 集合,而是一整块状态域,大概率就是 /var/lib/rancher/k3s 级别的东西。它应该整体决策,而不是零敲碎打。
4. HM 用户态很容易在 preserve 时变丑
最危险的不是漏掉某个 cache,而是把“历史上随手落在 home 根目录里的东西”误判为关键状态,最后把 preservation 写成了另一个版本的整盘搬家。
也就是说,在真正把 HM 用户态大面积纳入之前,我很可能先需要一轮“状态语义清洗”。否则看起来像是在做声明式治理,实际上只是把历史残留原封不动搬进白名单。
5. bindmount / symlink 的差异不是实现细节
这不是“选哪个写起来更顺手”的问题,而是行为差异。
不同程序对 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”,也不会反过来说“这纯属脱裤子放屁”。更准确的表述应该是:如果你只是想解决脏乱,它通常不是最优先的工具;但如果你真正想把“哪些状态应该存在、为什么应该存在、它们如何跨重启延续”这件事声明式地固定下来,那么它就是一条值得认真评估、也值得小心落地的路线。