Loading...

Day 29:系統維護與最佳化 — 讓你的 NixOS 跑得又快又乾淨

學習 NixOS 的日常保養,包含用 nix-tree 分析依賴關係、管理磁碟空間、最佳化編譯速度,以及建立系統健康檢查流程。

Day 29:系統維護與最佳化 — 讓你的 NixOS 跑得又快又乾淨

🗓 系列:NixOS 30 天學習之旅
📦 階段:第四階段 — 工程師進階實務(Day 22 – Day 30)
🎯 階段核心目標:佈署、自動化、安全性與貢獻


前言:NixOS 也需要保養

經過這 28 天的旅程,你的 NixOS 上大概已經累積了不少東西——各種 generation、開發環境的 derivation、測試用的套件、甚至好幾個版本的 kernel。這些東西全都乖乖地躺在 /nix/store 裡,一個都沒被刪掉。

這正是 NixOS immutable 設計的特性:舊版本不會被覆蓋,只會持續疊加。好處是你隨時可以 rollback,壞處是……磁碟空間會被慢慢吃光。

今天我們來聊聊 NixOS 的「日常保養」:如何用 nix-tree 分析空間占用與依賴關係、如何有效管理磁碟空間、如何最佳化編譯速度,以及建立一套可持續的系統健康檢查流程。


nix-tree 分析依賴關係與空間占用

安裝 nix-tree

nix-tree 是一個互動式的 terminal 工具,讓你用樹狀結構瀏覽 /nix/store 中的依賴關係與空間占用。先裝起來:

# configuration.nix
environment.systemPackages = with pkgs; [
  nix-tree
];

或者臨時用 nix-shell 跑一下也行:

nix-shell -p nix-tree

基本用法

最直覺的用法是直接對當前系統 profile 進行分析:

# 分析目前系統的依賴樹
nix-tree /run/current-system

執行後會進入一個互動式介面,你可以用方向鍵瀏覽各個 dependency,畫面上會顯示每個套件的大小(包含自身大小與 closure 大小)。

幾個常用的操作:

按鍵功能
Enter展開 / 收合子節點
w切換顯示模式(Why depends)
s依 closure size 排序
q離開

找出空間大戶

想知道哪些套件佔用最多空間?用 --derivation flag 搭配排序:

# 列出目前系統所有 dependency 的大小,依 closure size 排序
nix path-info -rsSh /run/current-system | sort -k2 -hr | head -20

這條指令的意思是:

  • -r:遞迴列出所有 dependency
  • -s:顯示 NAR size(套件自身大小)
  • -S:顯示 closure size(含所有依賴的總大小)
  • -h:人類可讀的大小格式

輸出大概會長這樣:

/nix/store/abc123...-linux-6.6.30     156.2M   156.2M
/nix/store/def456...-gcc-13.2.0        65.4M   198.7M
/nix/store/ghi789...-llvm-17.0.6       42.1M   310.5M
...

看到哪些套件的 closure size 特別大,就可以進一步分析它拉進了哪些依賴,思考是否有替代方案。

分析特定套件的依賴

如果想深入了解某個套件為什麼這麼「重」:

# 查看 firefox 的完整依賴樹
nix-tree $(which firefox)

# 或者直接指定 store path
nix-tree /nix/store/xxx...-firefox-125.0

這在你 debug「為什麼我的 container image 這麼大」的時候特別有用。


磁碟空間管理策略

了解目前的空間使用狀況

先來看看 /nix/store 到底吃掉了多少空間:

# 查看 /nix/store 的總大小
du -sh /nix/store

# 查看可以被回收的空間(dead store paths)
nix store gc --dry-run 2>&1 | tail -1

手動清理 garbage

NixOS 的 garbage collection(GC)會清除所有沒有被任何 root(如 system profile、user profile)參照的 store path。最基本的清理指令:

# 清除所有 GC roots 未參照的 store path
sudo nix-collect-garbage

不過,這只會清掉「已經沒有任何 generation 參照」的東西。如果你有 50 個 generation,每個 generation 各自參照了不同版本的套件,那這些套件都不會被清掉。

所以,更實際的做法是先刪除舊的 generation,再執行 GC

# 刪除 30 天以前的所有 generation,然後執行 GC
sudo nix-collect-garbage --delete-older-than 30d

你也可以指定保留最近幾個 generation:

# 只保留最近 5 個 generation
sudo nix-env --delete-generations +5
sudo nix-collect-garbage

清理 user profile

別忘了 user-level 的 profile 也會佔空間:

# 清理當前使用者的舊 generation
nix-collect-garbage --delete-older-than 14d

# 搭配 home-manager 使用時,也要清理 home-manager 的 generation
home-manager expire-generations "-14 days"

清理開發用的 Flake 與 devShell

如果你大量使用 nix developnix build,Nix 預設會把 build result 的 GC root 存放在專案目錄下的 result symlink。清除這些 symlink 後再執行 GC:

# 在專案目錄中刪除 result symlink
rm -f result

# 清理 flake registry cache
rm -rf ~/.cache/nix/

自動化垃圾回收設定

手動清理終究不是長久之計。NixOS 內建了自動化 GC 的機制,直接在 configuration.nix 裡設定即可:

# 自動化 garbage collection
nix.gc = {
  automatic = true;
  dates = "weekly";           # 每週執行一次
  options = "--delete-older-than 30d";  # 刪除 30 天前的 generation
};

這會建立一個 systemd timer,定期自動執行 nix-collect-garbage --delete-older-than 30d

搭配 nix-optimise-store

Nix store 中有很多重複的檔案(例如同一份 source file 出現在不同 derivation 裡)。nix store optimise 會透過 hard link 來 deduplicate 這些重複檔案,節省額外的空間:

# 定期最佳化 store(deduplicate)
nix.optimise = {
  automatic = true;
  dates = [ "weekly" ];
};

或者手動執行:

# 手動執行 store 最佳化
sudo nix store optimise

第一次跑可能會花比較久的時間,但後續執行會快很多。根據經驗,在一個使用了一段時間的系統上,這通常可以省下 20-40% 的空間。

完整的自動維護配置

把 GC 和 optimise 寫在一起,這是建議的標準配置:

{
  # 自動垃圾回收
  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-older-than 30d";
  };

  # 自動 deduplicate
  nix.optimise = {
    automatic = true;
    dates = [ "weekly" ];
  };

  # 也可以設定 store 大小上限(Nix 2.19+)
  # 當 store 超過指定大小時,GC 會自動清理最舊的 path
  nix.settings.min-free = 1073741824;       # 至少保留 1 GB 可用空間
  nix.settings.max-free = 5368709120;       # 可用空間達 5 GB 時停止清理
}

編譯速度最佳化

NixOS 的「什麼都從 Nix expression 建置」哲學很美好,但也意味著編譯時間可能會讓你崩潰。以下幾個技巧可以顯著加速你的日常體驗。

善用 Binary Cache

最有效的加速方式是根本不要自己編譯。Nix 預設會從 cache.nixos.org 下載預編譯好的 binary(稱為 substitution)。確保你的 nix.conf 沒有關掉這個功能:

nix.settings = {
  # 預設就是開啟的,但明確寫出來確保不會漏掉
  substituters = [
    "https://cache.nixos.org"
  ];
  trusted-public-keys = [
    "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
  ];
};

如果你使用了第三方的 Flake(例如 nix-community 的套件),可以加入它們的 binary cache:

nix.settings = {
  substituters = [
    "https://cache.nixos.org"
    "https://nix-community.cachix.org"
  ];
  trusted-public-keys = [
    "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
    "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
  ];
};

設定 Cachix 作為私有 Binary Cache

如果你的團隊經常 build 自己的套件,強烈建議使用 Cachix 來建立團隊專屬的 binary cache:

# 安裝 cachix
nix-env -iA cachix -f https://cachix.org/api/v1/install

# 將 build result 推到你的 cache
cachix push your-team-cache ./result

這樣團隊裡只需要一個人 build 過,其他人就可以直接從 cache 下載,省下大量編譯時間。

平行編譯

Nix 預設會使用所有 CPU core 來編譯,但你可以更精細地控制:

nix.settings = {
  # 同時可以進行多少個 build job
  max-jobs = "auto";        # "auto" = CPU core 數量

  # 每個 build job 可以使用多少 core
  cores = 0;                # 0 = 不限制,使用所有可用 core
};

💡 小提醒max-jobscores 是相乘的關係。如果你有 8 個 core,設定 max-jobs = 4cores = 2,代表最多同時跑 4 個 build,每個 build 最多用 2 個 core(共 8 core)。

使用 Remote Builder

如果你的主力機器效能有限(例如筆電),可以把編譯工作 offload 到另一台更強的機器上:

nix.buildMachines = [{
  hostName = "build-server";
  systems = [ "x86_64-linux" "aarch64-linux" ];
  maxJobs = 16;
  speedFactor = 2;
  supportedFeatures = [ "nixos-test" "big-parallel" "kvm" ];
  mandatoryFeatures = [];
}];

nix.distributedBuilds = true;

# 建議搭配 SSH key 認證
programs.ssh.extraConfig = ''
  Host build-server
    HostName 192.168.1.100
    User builder
    IdentityFile /root/.ssh/builder_key
'';

這樣當你在筆電上執行 nixos-rebuild switch 時,Nix 會自動把 build job 分配到 build-server 上執行,完成後再把 result 拉回來。筆電的風扇終於可以安靜了。


nix.conf 調校

除了上面提到的設定之外,以下是一些實用的 nix.conf(透過 nix.settings 設定)調校建議:

nix.settings = {
  # 啟用 Flakes 和 nix command(如果你還沒開的話)
  experimental-features = [ "nix-command" "flakes" ];

  # 允許以 root 執行時自動信任的使用者
  trusted-users = [ "root" "@wheel" ];

  # HTTP 連線數量(加速從 cache 下載)
  http-connections = 50;

  # 啟用自動 store 最佳化(每次 build 後自動 deduplicate)
  auto-optimise-store = true;

  # 記錄 build log,方便事後 debug
  log-lines = 25;

  # 如果 substitution 失敗,fallback 到本地 build
  fallback = true;

  # 連線 timeout(秒),避免卡在很慢的 cache server
  connect-timeout = 10;
};

auto-optimise-store vs nix.optimise

你可能注意到這裡有兩種 optimise 機制:

機制說明
auto-optimise-store = true每次有新的 path 寫入 store 時,即時 deduplicate
nix.optimise.automatic透過 systemd timer 定期 對整個 store 做全量 deduplicate

建議兩個都開。auto-optimise-store 可以避免空間持續膨脹,nix.optimise 則可以在初次啟用時清理歷史累積的重複檔案。

完整的效能調校範例

{ config, pkgs, ... }:

{
  nix = {
    settings = {
      experimental-features = [ "nix-command" "flakes" ];
      trusted-users = [ "root" "@wheel" ];
      auto-optimise-store = true;
      max-jobs = "auto";
      cores = 0;
      http-connections = 50;
      substituters = [
        "https://cache.nixos.org"
        "https://nix-community.cachix.org"
      ];
      trusted-public-keys = [
        "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
        "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
      ];
      connect-timeout = 10;
      fallback = true;
    };

    gc = {
      automatic = true;
      dates = "weekly";
      options = "--delete-older-than 30d";
    };

    optimise = {
      automatic = true;
      dates = [ "weekly" ];
    };
  };
}

系統健康檢查流程

最後,來建立一套日常可以執行的系統健康檢查流程。你可以把以下指令整理成一個 shell script,定期跑一次:

1. 檢查磁碟空間

# 整體磁碟使用量
df -h /nix/store

# Nix store 的大小
du -sh /nix/store 2>/dev/null

# 目前有多少 store path
nix path-info --all | wc -l

2. 檢查 generation 數量

# 系統 generation 列表
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system

# 使用者 generation
nix-env --list-generations

過多的 generation 是磁碟空間的最大元兇之一。一般建議保留最近 5–10 個 generation,或 30 天以內的 generation。

3. 確認 channel 或 Flake input 是否需要更新

# 如果你使用 channel
sudo nix-channel --list
sudo nix-channel --update

# 如果你使用 Flakes
nix flake update

# 查看 flake.lock 的最後更新日期
git log -1 --format="%ai" -- flake.lock
# 檢查 /run/current-system 的完整性
nix store verify --all --no-trust 2>&1 | head -20

5. 檢查 systemd 服務狀態

# 列出所有失敗的 service
systemctl --failed

# 檢查最近的系統日誌
journalctl -p err --since "7 days ago" --no-pager | tail -30

整合成一個健檢 script

以下是一個簡單的系統健檢 script,你可以放進 configuration.nixenvironment.systemPackages 中,或是放在自己的 dotfiles 裡:

#!/usr/bin/env bash
set -euo pipefail

echo "========================================="
echo "  NixOS 系統健康檢查報告"
echo "  $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="

echo ""
echo "📦 NixOS 版本"
nixos-version

echo ""
echo "💾 磁碟使用量"
df -h / /nix/store 2>/dev/null || df -h /

echo ""
echo "📊 Nix Store 統計"
echo "  Store paths 數量: $(nix path-info --all 2>/dev/null | wc -l | tr -d ' ')"
echo "  Store 大小: $(du -sh /nix/store 2>/dev/null | cut -f1)"

echo ""
echo "🔄 System Generation"
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system 2>/dev/null | tail -5

echo ""
echo "⚠️  失敗的 Service"
systemctl --failed --no-pager --no-legend || echo "  全部正常!"

echo ""
echo "🗑️  可回收空間(預估)"
nix store gc --dry-run 2>&1 | tail -1

echo ""
echo "========================================="
echo "  檢查完成"
echo "========================================="

把它存成 nixos-health-check.sh,以後只要一個指令就能快速掌握系統狀態。


小結

今天涵蓋了 NixOS 系統維護的核心面向:

主題重點
依賴分析nix-treenix path-info 找出空間大戶
磁碟空間管理nix-collect-garbage --delete-older-than 30d 清理舊 generation
自動化維護設定 nix.gcnix.optimise 讓系統自動保養
編譯加速善用 binary cache、平行編譯、remote builder
nix.conf 調校auto-optimise-storehttp-connectionsfallback 等關鍵參數
健康檢查建立一套可重複執行的系統檢查流程

NixOS 的 immutable 設計帶來了穩定性和可重現性,但也需要你主動管理磁碟空間和編譯效率。好消息是,這些維護工作大多可以自動化——設定好 nix.gcnix.optimise,系統就會自己照顧自己。

養成定期跑健檢 script 的習慣,你的 NixOS 就能一直保持在最佳狀態。


明日預告

Day 30:回顧與展望 — 你的 NixOS 之旅才剛開始

30 天的旅程來到了最後一天。明天我們會回顧這趟學習之旅的核心觀念、盤點你已經掌握的技能,並展望接下來可以深入探索的方向——無論是貢獻 Nixpkgs、打造公司內部的 Nix infrastructure,還是參與社群活動。

最後一天,我們明天見! 🚀


📚 延伸閱讀