Сборщик мусора

Добро пожаловать на одиннадцатую пилюлю Nix. В предыдущей десятой пилюле, мы провели параллель между изолированной средой сборки, предоставляемой nix-build и изолированной оболочкой разработки, предоставляемой nix-shell.

С этого момента мы больше не будем концентрироваться на на создании пакетов и вместо этого исследующим критический важный компонент Nix: сборщик мусора. Используя инструменты Nix, мы часто строим деривации, включая файлы .drv и выходные пути. Эти артефакты попадают в хранилище Nix и занимают место на нашем носителе так что мы можем захотеть освободить немного места, удалив деривации, которые нам больше не нужны. На решении этого вопроса мы сфокусируемся в одиннадцатой пилюле. По умолчанию, Nix придерживается консервативной стратегии, решая, какие из дериваций являются «нужными». В этой пилюле мы познакомимся с техникой выполнения более «опасных» операций обновления и удаления.

Как работает сборщик мусора?

Языки программирования со сборщиком мусора (garbage collector) используют концепцию множества корневых элементов (корней сборщика мусора или GC-корней) для отслеживания «живых» объектов. GC-корень — это объект, который всегда считается «живым» (пока не будет явным образом удалён из списка GC-корней). Процесс сборки мусора начинается с GC-корней, и рекурсивно помечает объекты, до которых может добраться, как «живые» Все остальные объекты могут быть собраны и удалены.

Вместо объектов, сборщик мусора Nix оперирует путями в хранилище, причём GC-корни также являются путями в хранилище. Этот подход гораздо более принципиален, чем у традиционных менеджеров пакетов, таких как dpkg или rpm, которые могут оставлять неиспользуемые пакеты или висящие файлы.

Реализация очень проста и прозрачна для пользователя. Первичные GC-корни хранятся в /nix/var/nix/gcroots. Если существует символическая ссылка на путь в хранилище, то связанный путь в хранилище является корнем GC.

Nix позволяет этому каталогу иметь подкаталоги: он просто рекурсивно перебирает подкаталоги в поиске символических ссылок на пути в хранилище. При обнаружении символической ссылки, её цель добавляется в список живых путей.

Подводя итог, можно сказать, что Nix ведёт список корней СМ. Эти корни могут быть использованы для вычисления списка всех живых путей в хранилище. Любой другой путь считается мёртвым. Удаление такие путей — простая операция. Сначала Nix переносит мёртвые пути в каталог /nix/store/trash, что является атомарной операцией. После этого корзина чистится.

Экспериментируем со сборщиком мусора

Прежде чем начать, запустим сборщик мусора Nix, чтобы получить чистую систему для наших экспериментов:

$ nix-collect-garbage
finding garbage collector roots...
[...]
deleting unused links...
note: currently hard linking saves -0.00 MiB
1169 store paths deleted, 228.43 MiB freed

Если мы запустим сборщик мусора снова, он, как и ожидается, не найдёт объектов для удаления. После завершения сборки мусора, хранилище Nix содержит только пути с ссылками из корней GC.

Теперь установим новую программу, bsd-games, исследуем её путь в хранилище, и убедимся, что он является GC-корнем. Команда nix-store -q --roots нужна, чтобы запросить корни GC, которые ссылаются на заданную деривацию. В нашем случае, окружение текущего пользователя ссылается на bsd-games:

$ nix-collect-garbage
finding garbage collector roots...
[...]
deleting unused links...
note: currently hard linking saves -0.00 MiB
1169 store paths deleted, 228.43 MiB freed

Теперь удалим его, запустим сборщик мусора и убедимся,что bsd-games всё ещё находится в хранилище Nix.

$ rm /nix/var/nix/profiles/default-9-link
$ nix-env --list-generations
[...]
   8   2014-07-28 10:23:24
  10   2014-08-20 12:47:16   (current)
$ nix-collect-garbage
[...]
$ ls /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17
ls: cannot access /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17: No such file or directory

Старое поколение всё ещё в хранилище Nix, потому что оно является корнем GC. Как вы увидим ниже, все профили и их поколения — это корни GC.

Удаление корня СМ — простая операция. В нашем случае мы удаляем поколение, которое ссылается на bsd-games, запускаем сборщик мусора и убеждаемся, что bsd-games больше недоступен в хранилище Nix:

ℹ️ Замечание: nix-env --list-generations не опирается на какие-либо метаданные. Он получает поколения, основываясь исключительно на именах файлов в каталоге профилей.

Обратите внимание, что мы удалили ссылку из каталога /nix/var/nix/profiles, не из /nix/var/nix/gcroots. Помимо прочего, Nix трактует /nix/var/nix/profiles как корень GC. Это полезно, поскольку это значит, что любой профиль и его поколения являются корнями GC. Другие пути также считаются корнями GC, например, /run/booted-system в NixOS. Команда nix-store --gc --print-roots печатает все пути, которые считаются корнями GC при запуске сборщика мусора.

Косвенные корни

Вспомните, что построение пакета GNU hello с помощью nix-build создаёт в текущем каталоге символическую ссылку result. Несмотря на выполненную выше сборку мусора, программа hello всё ещё работает. Следовательно, она не была собрана. Так как не существует других дериваций, которые зависят от пакета GNU hello, значит, он и является корнем GC.

Фактически, nix-build автоматически делает символическую ссылку result корнем GC. Обратите внимание, что речь идёт не о собранной деривации, а только о символической ссылке. Эти корни GC добавляются в каталог /nix/var/nix/gcroots/auto.

$ ls -l /nix/var/nix/gcroots/auto/
total 8
drwxr-xr-x 2 nix nix 4096 Aug 20 10:24 ./
drwxr-xr-x 3 nix nix 4096 Jul 24 10:38 ../
lrwxrwxrwx 1 nix nix   16 Jul 31 10:51 xlgz5x2ppa0m72z5qfc78b8wlciwvgiz -> /home/nix/result/

Нам пока не важно, какое имя носит символическая ссылка, ставшая корнем GC. Значение имеет то, что она существует и указывает на /home/nix/result. Это называется косвенным корнем GC. Корень GC считается косвенным, если он определён где-то вне каталога /nix/var/nix/gcroots. В нашем случае это значит, что цель символической ссылки result не будет удаляться сборщиком мусора.

Есть два способа удалить деривацию, считающуюся «живой» из-за косвенного корня GC:

  • Удалить косвенный корень GC из /nix/var/nix/gcroots/auto.
  • Удалить символическую ссылку result.

В первом случае деривация будет удалена из хранилища Nix в течение сборки мусора и result станет висячей символической ссылкой. Во втором случае деривация удаляется вместе с косвенным корнем в /nix/var/nix/gcroots/auto.

Запуск nix-collect-garbage после удаления корня GC или косвенного корня GC удалит деривацию из хранилища.

Чистим всё

Главный источник дублирования программ в хранилище Nix возникает из-за корней GC, связанных с nix-build и поколениями профиля. Запуск nix-build приводит к созданию корня GC для сборки, который ссылается на определённые версии определённых библиотек, например, glibc. После обновления, мы должны удалить предыдущую сборку, если мы хотим, чтобы сборщик мусора удалил соответствующие деривации, а так же, если мы хотим очистить старые зависимости.

То же касается и профилей. Манипулирование профилем nix-env создаёт дополнительные поколения. Старые поколения ссылаются на старые программы, увеличивая таким образом дублирование после обновления в хранилище Nix.

Другие системы обычно «забывают» всё о своём предыдущем состоянии сразу после обновления. В Nix мы тоже можем выполнить такой тип обновления (заставив Nix удалить старые деривации, включая старые поколения), но это надо делать вручную. Потребуются четыре шага:

  • Во-первых, мы скачиваем новую версию канала nixpkgs, который содержит описание всех программ. Это делается с помощью nix-channel --update.
  • Затем мы обновляем установленные пакеты, запустив nix-env -u. Так у нас появится новое поколение с обновлёнными программами.
  • Затем мы удаляем все косвенные корни, созданные nix-build: будьте осторожны, потому что это приводит к появлением висячих системных ссылок. Более умной стратегией будет одновременное удаление целей этих символических ссылок.
  • Наконец, с помощью опции -d при запуске nix-collect-garbage удаляем старые поколения всех профилей с последующей сборкой мусора. После этого к предыдущим поколениям вернуться нельзя. Прежде, чем запускать эту команду, важно убедиться, что новое поколение корректно работает.

Вот эти четыре команды:

$ nix-channel --update
$ nix-env -u --always
$ rm /nix/var/nix/gcroots/auto/*
$ nix-collect-garbage -d

Заключение

Сборка мусора в Nix — мощный механизм для чистки вашей системы. Команды nix-store позволяют нам узнать, почему определённая деривация присутствует в хранилище Nix, независимо от того, подлежит ли она сборке, или нет. Также мы выяснили, как выполнить более деструктивные операции удаления и обновления.

В следующей пилюле

В следующей пилюле мы запакуем другой проект и объясним паттерн проектирования «Входы». До сих пор мы работали только с одной деривацией; теперь нам предстоит организовать маленький репозиторий для программ. Паттерн «Входы» широко используется в nixpkgs; он позволяет нам разделять деривации из одного репозитория и обеспечивает тонкую настройку.