LaurVas

Ускоряем установку пакетов в Debian (libeatmydata)

   linux debian

Установкой пакетов в Debian занимается… нет, не apt. Apt только скачивает пакеты из репозитория, а установкой занимается dpkg. Если вы интересовались разными linux-дистрибутивами, то могли заметить, что dpkg работает медленно, особенно в сравнении с pacman из Archlinux. Тому есть несколько причин, и одна из них — dpkg ну очень осторожно пишет на диск: на каждый чих дёргает системный вызов fsync, который заставляет ОС сбросить на диск данные, ожидающие своей очереди в кэше. И пока ОС не убедится в том что данные записаны, dpkg будет бездействовать. Но не спешите критиковать разработчиков. Так сделано из благих намерений, чтобы данные не потерялись в случае внезапной потери питания. База данных dpkg — дико хрупкая штука, её невозможно восстановить автоматически.

Бывает так, что скорость установки пакетов важнее устойчивости к сбоям. Как ускорить dpkg? Часть дисковых синхронизаций отключается ключом --force-unsafe-io или аналогичной опцией в конфиге dpkg. Можно пойти ещё дальше и позвать на помощь библиотеку libeatmydata, которая напрочь отшибает всю осторожность. Жизнь слишком коротка чтобы делать fsync!

Библиотека libeatmydata делает одну единственную вещь: перехватывает системный вызов fsync и тут же возвращает управление назад. Это приводит к тому, что:

  • программа, которая осторожно пишет на диск (база данных), начинает работать гораздо быстрее.
  • падение такой программы может привести к потере данных.

В этой статье я покажу как использовать libeatmydata и устрою бенчмарк (спойлер: ускорение до 14 раз). Кстати, этот способ подойдёт не только для установки пакетов, но и для любой БД.

Как использовать libeatmydata

  1. Через обёртку eatmydata, например eatmydata apt upgrade.
  2. Через переменную окружения LD_PRELOAD.

Переменную окружения можно устанавливать индивидуально для процесса

# LD_PRELOAD=/usr/lib/i386-linux-gnu/libeatmydata.so apt upgrade

или для группы процессов, например для всей системы:

/etc/environment
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libeatmydata.so

Вариант с переменной окружения хорош тем, что не требует переделки скриптов автоматизации: достаточно добавить одну строчку в один конфигурационный файлик и всё летает :) Единственный нюанс: путь к библиотеке отличается на разных архитектурах.

Бенчмарк

Разумеется, я захотел выяснить какое ускорение даёт 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 .

Раздел для тестирования будет неоднократно форматироваться, поэтому ничего ценного там быть не должно. В идеале он должен находиться на отдельном диске, который во время тестирования больше ничем не нагружен.

На выходе скрипта имеем лог с результатами измерений. Типа такого:

dpkg-benchmark.sdXY.log
234.380  33.001  10.118  writeback,barrier,300,normal
37.397   31.340   7.585  ordered,barrier,300,eatmydata
37.170   31.610   7.434  ordered,nobarrier,300,eatmydata
238.008  33.753   9.563  ordered,barrier,5,normal
37.868   31.853   8.084  no_journal,barrier,*,eatmydata
48.162   29.928   7.725  journal,nobarrier,30,unsafeio
37.267   31.985   7.156  ordered,nobarrier,5,eatmydata
37.632   31.307   7.706  ordered,barrier,5,eatmydata
245.536  33.271   9.584  journal,barrier,300,unsafeio
37.960   32.208   7.578  ordered,barrier,300,eatmydata
49.587   29.989   7.690  journal,nobarrier,5,unsafeio
59.768   31.315   8.173  no_journal,nobarrier,*,normal

Первые три поля — это время real, user, sys. Для построения графика используется только первое, а остальные так… на всякий случай. Последнее поле — параметры теста. Каждый тест прогоняется 5 раз, чтобы нивелировать погрешность.

Сразу построить графики по этим данным не получится, надо сперва усреднить результаты. Этим занимается скрипт benchmark-analyze.py. Скармливаем ему лог и получаем три файла: normal.dat, unsafeio.dat, eatmydata.dat. Они содержат упорядоченные данные для рисования в Gnuplot в виде таблицы:

normal.dat
ordered,nobarrier,30     43.726  0.164  29.639  0.172  7.494  0.227
ordered,nobarrier,5      43.744  0.159  29.532  0.116  7.582  0.086
writeback,nobarrier,300  43.793  0.187  29.536  0.200  7.556  0.113
writeback,nobarrier,30   44.071  0.216  29.704  0.463  7.678  0.259
no_journal,nobarrier,*   60.707  1.087  31.083  0.166  8.417  0.205
journal,nobarrier,300    67.495  0.536  30.120  0.206  9.730  0.201
journal,nobarrier,5      67.619  0.636  30.232  0.140  9.770  0.155
journal,nobarrier,30     67.718  0.214  30.091  0.316  9.631  0.305
writeback,barrier,300   236.560  3.253  33.531  0.411  9.852  0.316

Чётные поля — это усреднённые значения 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-контейнерах, во временных тестовых средах. Подходите к оптимизации с умом, друзья.