Автор Rewind VM показал отладчик сборок Nix, где Nix сделал самую трудную часть
Автор (на GitHub, fzakaria) несколько дней назад писал про Rewind VM: детерминированную виртуальную машину, в которой каждый запуск сборки Nix, чистая функция входов, включая расписание потоков. Он пользуется ею, чтобы находить, воспроизводить и устранять многочисленные гонки (race conditions) в сборках Nix, и спрашивает, почему этим больше никто не пользуется. Инструмент быстро обзавёлся панелью исходников, стеком вызовов, закладками, вкладкой Compare (сравнение), расширенной поддержкой gdb, «дорожками потоков» (кто держал процессор на каждом шаге) и функцией «Check from here» (проверка от выбранного шага). Автор пишет, что ожидал большой работы над каждой функцией, но раз за разом выяснялось: самую трудную часть уже сделал Nix. Отладчику нужны точные входы программы, отладочные символы, исходники самой программы и всех библиотек под ней, а также способ передать всё это на чужую машину, именно это и есть derivation (описание сборки в Nix).
Демонстрация идёт на классической гонке: два «кассира» (потока) вносят деньги на один счёт. Каждый вносящий читает баланс, пишет строку в журнал и сохраняет баланс плюс взнос. Если второй поток влез между чтением и записью первого, он затирает чужой взнос устаревшим значением. На 16-ядерном ноутбуке автора программа теряла деньги в 396 из 1000 запусков, а при привязке к одному ядру через taskset, ни в одном из 1000: одно ядро редко переключает потоки посреди взноса. Поэтому Rewind намеренно меняет расписание. В его виртуальной машине один процессор, и первый запуск проходит успешно. Команда rewind check прогоняет сборку под изменёнными расписаниями, в каждом из которых гостевое ядро просит переключить поток на разных шагах, и сужает первый сбой до одного шага. Здесь это шаг 3237: пересчёт расписания в нём приводит к провалу. Проход занял 11 секунд на ноутбуке автора.
Дальше автор показывает, что именно даёт Nix. Во-первых, входы: derivation (источники, компилятор, библиотеки, ядро, настройки ВМ) определяет всё, что нужно для воспроизводимости. rewind nix реализует входы, упаковывает замыкание в образ erofs только для чтения и загружает на нём ВМ; идентификатор запуска, хэш входов, как у пути в хранилище, а rewind show печатает команду, которая воссоздаёт этот запуск. Хэш NAR выходов, который сообщает гость, сверяется с копией на хозяине и с бинарными кэшами, загружается только .narinfo. Во-вторых, символы и исходники: nixpkgs собирает пакеты с separateDebugInfo, отладочная информация лежит в cache.nixos.org как выход debug, а debuginfod получает её и исходники по build ID, так видны исходники любого бинарника в ВМ, включая ядро Linux. Панель в приложении или команда rewind where показывает исходник на позиции воспроизведения и стек вызовов.
rewind gdb открывает gdb на ответвлении (форке) запуска на выбранном шаге, со всеми потоками процесса: можно ставить точки останова и наблюдения, смотреть память, регистры и переменные. Ничего из сделанного в gdb не меняет запись, так что можно вернуться на тот же шаг или ответвиться на любом другом. Команда rewind shell --with nixpkgs#strace открывает оболочку в ВМ на шаге с любым пакетом из nixpkgs. Вкладка Compare (или rewind compare) показывает последние общие события двух запусков и первое расходящееся: в провальном запуске второй кассир начинает со 150, а в успешном, с 200. Дорожки потоков восстанавливаются без новой записи: каждый запуск воспроизводится точно, поэтому можно пройти участок шаг за шагом и спросить у гостевого ядра, какой поток на процессоре. В провальном запуске поток 140 (кассир 1) прерывается на шаге 3238 посреди взноса, поток 141 (кассир 2) получает процессор на один шаг 3239, читает баланс, после чего первый возвращается и заканчивает.
«Check from here» отвечает на вопрос, насколько вероятен сбой и не повезло ли успешному запуску. С ключом --run команда стартует от уже имеющегося запуска на выбранном шаге, ответвляет его по разу на каждое расписание и считает, сколько завершились иначе; всё до шага остаётся неизменным. Если взять шаг 3221, где два запуска разошлись, и прогнать 16 расписаний, все 16 теряют деньги (расписание 0, сам успешный запуск). Закладки (клавиша b) хранятся вместе с запуском и едут в его экспорте .rwd, так что получатель открывает запуск с заметками автора на временной шкале. В репозитории есть и другие примеры, у каждого по одной ошибке и одному исправлению: philosophers (взаимная блокировка из прошлой статьи), bank, waiter (сигнал SIGCHLD приходит между проверкой флага и pause, и родитель спит до срабатывания десятисекундного таймаута make check) и config-reload (один процесс переписывает конфиг на месте, другой его перечитывает). Вывод автора: если начинать с чего-то герметичного, вроде derivation Nix, отладчик можно получить бесплатно.
Ключевые факты
- Rewind VM, детерминированная виртуальная машина: каждый запуск сборки Nix является чистой функцией входов, включая расписание потоков.
- Автор утверждает, что трудную часть новых функций отладчика (входы, отладочные символы, исходники, передача на чужую машину) уже сделал Nix; отладочную информацию и исходники отладчик получает через debuginfod по build ID, включая ядро Linux.
- На примере гонки двух потоков в банковском счёте на 16-ядерном ноутбуке автора деньги терялись в 396 из 1000 запусков, а при привязке к одному ядру, ни в одном из 1000.
- rewind check за 11 секунд на ноутбуке автора нашёл решающий шаг 3237; «Check from here» от шага 3221 показал, что все 16 из 16 изменённых расписаний заканчиваются потерей денег.
- В набор функций входят gdb на ответвлении любого шага, сравнение запусков, дорожки потоков, закладки с экспортом в .rwd и примеры philosophers, bank, waiter, config-reload.
Почему это важно
Гонки в сборках трудно воспроизводить: сбой появляется лишь при определённом порядке потоков. Rewind VM делает порядок частью входов запуска, а значит, один и тот же запуск можно перепроигрывать точно и искать шаг, где всё ломается. Автор показывает, что поверх такой основы отладчик получается во многом из того, что Nix уже умеет: учёт входов, отладочные символы, исходники и доставка всего этого другому человеку.
Кому это важно
Тем, кто собирает проекты через Nix и сталкивается с нестабильными сборками и тестами, которые падают «иногда». Также разработчикам, интересующимся детерминированным воспроизведением и отладкой параллельных программ: пример с банковским счётом и набор примеров в репозитории показывают, как это выглядит на практике.
Как это применить
Автор приводит команду запуска: nix run github:fzakaria/rewindvm -- check github:fzakaria/rewindvm#bank. Аргументом check может быть derivation или ссылка на flake. В репозитории есть готовые примеры philosophers, bank, waiter и config-reload, у каждого по одной ошибке и одному исправлению. Для разбора найденного запуска служат rewind where, rewind gdb, rewind compare, rewind shell и «Check from here» с приложением Rewind. О лицензии, версиях и условиях использования в тексте не сказано.
Можно ли доверять
Это личная заметка автора инструмента, а не независимая проверка: он сам пользуется Rewind и сам делает выводы о его пользе. Числа (396 из 1000, ни одного из 1000, 11 секунд) получены на одном 16-ядерном ноутбуке автора, другого оборудования и более широких замеров в тексте нет. Зато все команды и вывод показаны прямо в тексте, а пример с банковским счётом простой и проверяемый.
Риски и подводные камни
В тексте не приведены накладные расходы на запуск сборок внутри Rewind VM, нет списка конкретных гонок, найденных и исправленных с её помощью, и не названы номер версии, лицензия и круг пользователей. Не сказано и то, работает ли Rewind со сборками, не являющимися derivation Nix. Один процессор в виртуальной машине означает, что первый запуск, как отмечает сам автор, обычно проходит успешно: найти сбой удаётся только изменением расписания, поэтому результат зависит от того, на каких шагах оно применено.
«Трудная часть каждой функции уже была сделана, и сделал её Nix.»
— автор поста (fzakaria)