Установкой пакетов в Debian занимается… нет, не apt. Apt только скачивает пакеты из репозитория, а установкой занимается dpkg. Если вы интересовались разными linux-дистрибутивами, то могли заметить, что dpkg работает медленно, особенно в сравнении с pacman из Archlinux. Тому есть несколько причин, и одна из них — dpkg ну очень осторожно пишет на диск: на каждый чих дёргает системный вызов fsync
, который заставляет ОС сбросить на диск данные, ожидающие своей очереди в кэше. И пока ОС не убедится в том что данные записаны, dpkg будет бездействовать. Но не спешите критиковать разработчиков. Так сделано из благих намерений, чтобы данные не потерялись в случае внезапной потери питания. База данных dpkg — дико хрупкая штука, её невозможно восстановить автоматически.
Бывает так, что скорость установки пакетов важнее устойчивости к сбоям. Как ускорить dpkg? Часть дисковых синхронизаций отключается ключом --force-unsafe-io
или аналогичной опцией в конфиге dpkg. Можно пойти ещё дальше и позвать на помощь библиотеку libeatmydata, которая напрочь отшибает всю осторожность. Жизнь слишком коротка чтобы делать fsync!
Библиотека libeatmydata делает одну единственную вещь: перехватывает системный вызов fsync
и тут же возвращает управление назад. Это приводит к тому, что:
- программа, которая осторожно пишет на диск (база данных), начинает работать гораздо быстрее.
- падение такой программы может привести к потере данных.
В этой статье я покажу как использовать libeatmydata и устрою бенчмарк (спойлер: ускорение до 14 раз). Кстати, этот способ подойдёт не только для установки пакетов, но и для любой БД.
Как использовать libeatmydata
- Через обёртку
eatmydata
, напримерeatmydata apt upgrade
. - Через переменную окружения
LD_PRELOAD
.
Переменную окружения можно устанавливать индивидуально для процесса
# LD_PRELOAD=/usr/lib/i386-linux-gnu/libeatmydata.so apt upgrade
или для группы процессов, например для всей системы:
Вариант с переменной окружения хорош тем, что не требует переделки скриптов автоматизации: достаточно добавить одну строчку в один конфигурационный файлик и всё летает :) Единственный нюанс: путь к библиотеке отличается на разных архитектурах.
Бенчмарк
Разумеется, я захотел выяснить какое ускорение даёт libeatmydata. В голову пришла идея такого бенчмарка:
- берём контейнер с каким-нибудь Debian,
- включаем один из ускорителей (force-unsafe-io или libeatmydata),
- ставим некий фиксированный набор пакетов и замеряем время установки,
- возвращаем всё в исходное состояние.
Для полноты эксперимента я решил проводить тесты как на жёстком диске, так и на SSD, используя файловую систему ext4 с разными опциями монтирования:
- включенное/выключенное журналирование,
- режимы журналирования journal, ordered, writeback,
- включенный/выключенный барьер на запись (write barrier),
- разная периодичность коммита в журнал.
Главный вопрос — сколько и какие пакеты ставить? Меня интересуют накладные расходы на установку пакетов, значит надо набрать много мелких пакетов. Я взял openbox
, devscripts
и build-essential
+ их зависимости. Получилось 388 пакетов размером преимущественно до 1 MiB.
Вот результат тестирования на HDD:
Как видим, ускорение сильно зависит от настроек параноидальности файловой системы: чем они жёсче, тем медленнее устанавливаются пакеты и тем больше эффект от ускорителей. Лично для меня стал неожиданным факт, что ФС с выключенным журналом работает медленнее, чем с журналом в режиме ordered или writeback и что барьер на запись даёт такую большую просадку в скорости. Что ж, учту на будущее.
Результат тестирования на SSD, в том же масштабе:
В некоторых режимах unsafeio проигрывает обычному режиму, но скорее всего это погрешность. Для сравнения: на рам-диске пакеты устанавливаются за 33.8 секунды. Это минимальное время, которого в принципе можно достичь на моём железе.
Как получить эти графики
Чтобы автоматизировать процесс тестирования и дать всем желающим возможность повторить эксперимент, я написал три скрипта. Они доступны в репозитории на GitHub.
Непосредственно за тестирование отвечает скрипт dpkg-benchmark.sh
. Ему надо передать два аргумента: архив с контейнером и раздел диска под файловую систему. Архив с контейнером готовится ручками в три команды:
# mkdir stretch
# debootstrap stretch ./stretch http://mirror.yandex.ru/debian
# tar cpJf stretch-deboostrapped.tar.xz --one-file-system -C stretch .
Раздел для тестирования будет неоднократно форматироваться, поэтому ничего ценного там быть не должно. В идеале он должен находиться на отдельном диске, который во время тестирования больше ничем не нагружен.
На выходе скрипта имеем лог с результатами измерений. Типа такого:
Первые три поля — это время real, user, sys. Для построения графика используется только первое, а остальные так… на всякий случай. Последнее поле — параметры теста. Каждый тест прогоняется 5 раз, чтобы нивелировать погрешность.
Сразу построить графики по этим данным не получится, надо сперва усреднить результаты. Этим занимается скрипт benchmark-analyze.py
. Скармливаем ему лог и получаем три файла: normal.dat
, unsafeio.dat
, eatmydata.dat
. Они содержат упорядоченные данные для рисования в Gnuplot в виде таблицы:
Чётные поля — это усреднённые значения real, user, sys. Нечётные — их стандартные отклонения. Для построения графика используются только первое и второе поле. За рисование отвечает скрипт draw.plt
, он создаёт картинку в plot.svg
.
Итого, последовательность команд такая:
# ./dpkg-benchmark.sh stretch-deboostrapped.tar.xz /dev/sdXY
$ ./benchmark-analyze.py dpkg-benchmark.sdXY.log
$ ./draw.plt
Напоследок
Для тех, кто ещё не понял: не надо везде включать libeatmydata и считать это “оптимизацией Linux” или “ускорением компьютера”. Нафиг вам такая оптимизация, от которой всё сломается после первого сбоя? Включать libeatmydata стоит в процессах автоматизации, когда в случае сбоя вы не станете ничего восстанавливать, а просто запустите заново. Например, так можно поступить при сборке пакетов через pbuilder, в одноразовых docker-контейнерах, во временных тестовых средах. Подходите к оптимизации с умом, друзья.