Configuring Linux
System administration
Network technologies
March 2015 31

Стань повелителем загрузки Linux

Tutorial
Сначала мы познакомимся с udev и научимся с его помощью исследовать установленные в компьютере устройства прямо во время загрузки: в качестве примера будем автоматически выбирать настройки видеокарт для Xorg. Затем решим задачу работы с одним образом на десятках компьютеров одновременно путём внедрения собственного обработчика в initramfs, а заодно оптимизируем систему для сетевой загрузки. Чтобы дополнительно уменьшить время загрузки и снизить нагрузку на сеть попробуем NFS заменить на NBD, и помочь TFTP с помощью HTTP. В конце вернёмся в начало — к загрузочному серверу, который также переведём в режим «только для чтения».



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

Начало смотрите здесь:
Первоначальная настройка сервера
Подготовка образа для загрузки по сети

Мы остановились на том, что загрузили по сети машину VirtualBox и запустили Firefox. Если сейчас попытаться сделать то же самое с настоящим компьютером, то на экране появится циклическая авторизация пользователя username и безуспешные попытки запустить графическое окружение — Xorg не находит нужный драйвер.

Запускаем видеокарты


Для работы графического режима в VirtualBox у нас установлено всё необходимое. Изначально планировалось, что наша бездисковая система будет функционировать на любом «железе», но из-за лени мы не станем пытаться объять необъятное, поэтому ограничимся поддержкой графических решений следующих доминирующих производителей: nVidia, Intel и AMD.

Переключимся на машине-клиенте во второй терминал нажатием Ctrl+Alt+F2 и установим открытые драйверы:
pacman -S xf86-video-ati xf86-video-nouveau xf86-video-intel

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

Простейший способ узнать какие видеоустройства имеются в системе, это ввести в консоли команду:
lspci | grep -i vga

00:02.0 VGA compatible controller: InnoTek Systemberatung GmbH VirtualBox Graphics Adapter

Но мы не будем искать лёгких путей, а в награду получим новую порцию знаний.

Ближе знакомимся с udev


Раньше я уже упоминал, что менеджер устройств в Archlinux называется udev. Он входит в пакет systemd под именем systemd-udevd.

По мере обнаружения новых устройств в загружаемой системе, ядро создаёт их иерархию в каталоге /devices. Сначала появляется сама система PCI, затем в ней обнаруживаются шины, на которых «сидят» конечные устройства, а их драйверы рассортировывают устройства по классам. Устройства внутри классов обнаруживаются параллельно, точно как systemd параллельно запускает службы для достижения следующей цели.

Асинхронный поиск устройств приводит к тому, что если в компьютере одновременно присутствует несколько устройств, относящихся к одному классу, то они могут обнаруживаться в разном порядке от включения к включению, например, сначала одна видеокарта, а потом — другая, и их имена при этом будут меняться между собой. К счастью, появление нового элемента в иерархии устройств является событием udev, которое можно отследить и принять необходимые меры.

Для менеджера udev придуманы правила, призванные упорядочить хаос, и упростить жизнь установленных программ. Правила хранятся в папках /usr/lib/udev/rules.d/ и /etc/udev/rules.d/ (последняя, как и в случае обработчиков (hooks), имеет более высокий приоритет и файлы оттуда проверяются первыми). Появление нового элемента в иерархии устройств сопровождается проверкой всех установленных правил udev, и автоматическим выполнением указанных там действий, в случае совпадения. Обычно эти действия заключаются в переименовании устройств и установки на них ссылок в каталогах внутри /dev и /sys для удобства использования в программах.

Драйверы видеокарт относят их к подсистеме (классу) drm, поэтому сведения о подобных устройствах дублируются в каталоге /sys/class/drm. Первая видеокарта, обнаруженная в системе, по-умолчанию получает имя «card0», если в ней имеется несколько видеовыходов, то они получают имена вида «card0-CON-n», где «CON» — тип разъема (VGA, HDMI, DVI и др.), а «n» — порядковый номер разъема (причём одни производители нумеруют разъёмы начиная с «0», а другие — с «1»). Следующая видеокарта становится “card1” и т. д.

Если ничего не предпринять, то в виду параллельности обнаружения при следующем включении card1 может стать card0 и наоборот. Udev станет добавлять такие устройства в /dev то с одним, то с другим именем. Случаи, когда такое поведение udev нежелательно, подробно описаны в Интернете, и в них в основном обсуждаются различные USB устройства. Нам же требуется при обнаружении видеокарт запускать определённую программу, которую напишем чуть позже, а пока выясним, что известно udev.

Чтобы узнать то же самое, что знает про видеокарту udev, введём команду на клиенте:
udevadm info -a -p /sys/class/drm/card0

вывод команды
Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

looking at device '/devices/pci0000:00/0000:00:02.0/drm/card0':
KERNEL==«card0»
SUBSYSTEM==«drm»
DRIVER==""

looking at parent device '/devices/pci0000:00/0000:00:02.0':
KERNELS==«0000:00:02.0»
SUBSYSTEMS==«pci»
DRIVERS==""
ATTRS{irq}==«18»
ATTRS{subsystem_vendor}==«0x0000»
ATTRS{broken_parity_status}==«0»
ATTRS{class}==«0x030000»
ATTRS{driver_override}=="(null)"
ATTRS{consistent_dma_mask_bits}==«32»
ATTRS{dma_mask_bits}==«32»
ATTRS{local_cpus}==«00000000,00000000,00000000,00000001»
ATTRS{device}==«0xbeef»
ATTRS{enable}==«1»
ATTRS{msi_bus}==«1»
ATTRS{local_cpulist}==«0»
ATTRS{vendor}==«0x80ee»
ATTRS{subsystem_device}==«0x0000»
ATTRS{boot_vga}==«1»
ATTRS{numa_node}=="-1"
ATTRS{d3cold_allowed}==«0»

looking at parent device '/devices/pci0000:00':
KERNELS==«pci0000:00»
SUBSYSTEMS==""
DRIVERS==""

Обратите внимание на древовидную структуру с использованием парадигмы родительских и дочерних устройств. В строках, начинающихся с «looking at ...» указан путь к данному устройству относительно каталога /sys, т. е. обратившись к видеокарте по пути /sys/class/drm/card0, мы обнаружили, что на самом деле это ссылка на /sys/devices/pci0000:00/0000:00:02.0/drm/card0.

У родительского устройства /devices/pci0000:00/0000:00:02.0 есть атрибут vendor с идентификатором производителя. Udev располагает доступом к обширной базе данных и может перевести этот код в удобоваримый вид:
udevadm info -q property -p /sys/devices/pci0000:00/0000:00:02.0

DEVPATH=/devices/pci0000:00/0000:00:02.0
ID_MODEL_FROM_DATABASE=VirtualBox Graphics Adapter
ID_PCI_CLASS_FROM_DATABASE=Display controller
ID_PCI_INTERFACE_FROM_DATABASE=VGA controller
ID_PCI_SUBCLASS_FROM_DATABASE=VGA compatible controller
ID_VENDOR_FROM_DATABASE=InnoTek Systemberatung GmbH
MODALIAS=pci:v000080EEd0000BEEFsv00000000sd00000000bc03sc00i00
PCI_CLASS=30000
PCI_ID=80EE:BEEF
PCI_SLOT_NAME=0000:00:02.0
PCI_SUBSYS_ID=0000:0000
SUBSYSTEM=pci
USEC_INITIALIZED=24450

Сравните с выводом команды:
lspci | grep -i vga
00:02.0 VGA compatible controller: InnoTek Systemberatung GmbH VirtualBox Graphics Adapter.


Динамическая настройка видеокарты с помощью udev


Подключитесь к загрузочному серверу. И создайте файл с правилами:
export root=/srv/nfs/diskless
nano $root/etc/udev/rules.d/10-graphics.rules

KERNEL=="card[0-9]*", SUBSYSTEM=="drm", RUN+="/etc/default/xdevice %n"
KERNEL=="card*", SUBSYSTEM=="drm", ATTR{enabled}=="enabled", ATTR{status}=="connected", RUN+="/etc/default/xdevice %n %k"

Каждое правило записывается в новой строке. Первая часть служит для идентификации события udev, к которому нужно применить действие, указанное в конце строки. Для опознания события используются данные, которые можно получить в выводе команды «udevadm info -a -p /sys...».

Правило из первой строки срабатывает для всех устройств с именем (ядром) card0, card1… подсистемы drm. Второе правило сработает только для активных устройств из подсистемы drm, к которым в данный момент подключен монитор (оно не сработает для card0, card1, а только для имен вида card0-HDMI-1, т. к. только у таких устройств есть атрибуты enabled и status). При совпадении события с его описанием выполняется одна и та же программа, в которую в первом случае передаётся один параметр %n (порядковый номер ядра, который для card0 будет «0»), а во втором — дополнительный параметр %k (само имя ядра «card0»).

Программа /etc/default/xdevice будет изменять содержимое файла в папке /etc/X11/xorg.conf.d/, в котором содержится информация о настройках видеоадаптера для xorg. Для разных производителей мы подготовим разные шаблоны, учитывающие особенности реализации. Достаточно указать минимально необходимую информацию для однозначной идентификации устройства, а остальное xorg сделает сам:
Section "Device"
    Identifier "уникальный идентификатор устройства"
    Driver     "используемый драйвер"
    Option     "AccelMethod" "метод ускорения"
    BusID      "PCI:идентификатор шины PCI, куда физически установлен адаптер"
EndSection

Необходимые данные мы укажем в самом шаблоне или получим исследуя вывод команды «udevadm info».

Программа будет срабатывать для каждого выхода каждой видеокарты, к которому подключен монитор. Для упрощения задачи заставим работать последний обнаруженный вариант, чтобы на мультимониторных системах работал хотя бы один монитор. Это не самый оптимальный способ настройки, и было бы лучше проверить графическую подсистему один раз перед достижением graphical.target, но наш вариантрабочий и подходит для изучения правил udev в действии.
Создаём файл программы со следующим содержанием:
nano $root/etc/default/xdevice

Скрытый текст
#!/bin/sh

# в этом файле будем хранить настройки устройства для xorg
CONF_FILE=/etc/X11/xorg.conf.d/20-device.conf

# получаем первое слово в названии производителя в "человеческом" виде
# лучше было бы использовать идентификаторы, но для наглядности оставим как есть
get_vendor(){
    local card=$(get_path $1)
    udevadm info -q property -p ${card%\/drm*} | \
        awk '/^ID_VENDOR_FROM_DATABASE/{split($1,a,"=");print tolower(a[2])}'
}

# получаем идентификатор шины PCI из пути устройства и приводим его к виду x:y:z
get_bus(){
    local bus=$(get_path $1)
    echo ${bus%\/drm*} | \
        sed 's\:\.\g' | \
        awk '{n=split($0,a,".");printf "%i:%i:%i",a[n-2],a[n-1],a[n]}'
}

# получаем полный путь к устройству
get_path(){
    udevadm info -q path -p /sys/class/drm/$1
}

# Выбираем шаблон на основании имени производителя и заполняем его данными.
make_conf(){
    local filename="xorg-device-$(get_vendor $1).conf"
    cat /etc/X11/$filename | \
        sed 's\%BUS%\'$(get_bus $1)'\g'| \
        sed 's\%ID%\'$1'\g' > $CONF_FILE
}

# если мы в virtualbox, то запускаем для него службу
check_vbox(){
    local vendor=$(get_vendor $1)
    [ "$vendor" == "innotek" ]] && systemctl start vboxservice
}

#начало программы
card_numb=$1

if [ -z "$2" ] # обрабатываем исключение для virtualbox
then
    card_name="card$card_numb"
    check_vbox $card_name && make_conf $card_name
else
    card_name=$2
    make_conf $card_name
fi


Сделаем файл исполняемым:
chmod +x $root/etc/default/xdevice

Отключаем автоматическую загрузку службы VirtualBox, т. к. теперь она будет запускаться нашей программой только при необходимости:
systemctl disable vboxservice

Добавляем шаблоны конфигурационных файлов xorg, с оптимизированными под основных производителей настройками:
nano $root/etc/X11/xorg-device-intel.conf

Section "Device"
    Identifier "Intel %ID%"
    Driver     "intel"
    Option     "AccelMethod" "uxa"
    BusID      "PCI:%BUS%"
EndSection

AMD, nVidia, VirtualBox
nano $root/etc/X11/xorg-device-innotek.conf

Section "Device"
    Identifier "VirtualBox %ID%"
    Driver     "vboxvideo"
    BusID      "PCI:%BUS%"
EndSection


nano $root/etc/X11/xorg-device-advanced.conf

Section "Device"
    Identifier "AMD %ID%"
    Driver     "radeon"
    Option     "AccelMethod" "exa"
    BusID      "PCI:%BUS%"
EndSection


nano $root/etc/X11/xorg-device-nvidia.conf

Section "Device"
    Identifier "nVidia %ID%"
    Driver     "nouveau"
    Option     "AccelMethod" "exa"
    BusID      "PCI:%BUS%"
EndSection


Добавляйте свои шаблоны и не забывайте устанавливать драйверы для этих устройств. Укажите в комментариях проверенные комбинации.

В завершение настройки xorg сделаем ”Windows like” переключение раскладки клавиатуры комбинацией Alt+Shift:
nano $root/etc/X11/xorg.conf.d/50-keyboard.conf

Section "InputClass"
       Identifier "keyboard-layout"
       MatchIsKeyboard "on"
       Option "XkbLayout" "us,ru"
       Option "XkbVariant" ",winkeys"
       Option "XkbOptions" "grp:alt_shift_toggle"
EndSection


Оптимизируем систему


Логи работы всех составляющих Archlinux сохраняются в журнале. Если всё оставить как есть, то журнал может довольно сильно раздуть, поэтому ограничим его размер, скажем 30Мб (добавьте или раскомментируйте строку):
nano $root/etc/systemd/journald.conf
...
SystemMaxUse=30M
...

Каждое действие протоколируется в папку /var/log/journal. В нашем случае передача данных осуществляется по сети, которая на практике имеет невысокую пропускную способность. Можно удалить папку с журналом, то он будет сохраняться только в оперативной памяти, что идеально подходит для бездискового клиента:

rm -r $root/var/log/journal

При различных ошибках в работе приложений в папке /var/lib/systemd/coredump создаются автоматические дампы ядра. Мы их отключим по той же причине:

nano $root/etc/systemd/coredump.conf
...
Storage=none
...


Отключаем SWAP:

echo -e 'vm.swappiness=0\nvm.vfs_cache_pressure=50' > $root/etc/sysctl.d/99-sysctl.conf


Удалим ненужные локализации. Это простое действие поможет сэкономить более 65 Мб. Заодно посмотрим, как устанавливаются программы из AUR (фактически они собираются из исходников). Зайдите на загрузочный сервер с правами обычного пользователя и выполните следующие действия:
curl -o localepurge.tar.gz https://aur.archlinux.org/packages/lo/localepurge/localepurge.tar.gz
tar -xvvzf localepurge.tar.gz
cd localepurge
makepkg -s

Пакет готов. Устанавливаем его из файла, а не из репозитория, поэтому ключ S заменяется на U (исправьте название файла, если версия собранной вами програмы не совпадает с моей):
sudo pacman --root $root --dbpath $root/var/lib/pacman -U localepurge-0.7.3.4-1-any.pkg.tar.xz

Теперь настроим. Закомментируйте строку «NEEDCONFIGFIRST» в начале файла и укажите используемые локализации в самом конце:
nano $root/etc/locale.nopurge
...
# NEEDSCONFIGFIRST
...
ru
ru_RU
ru_RU.UTF-8
en
en_US
en_US.UTF-8

Конфигурируем и запускаем программу:
arch-chroot $root /usr/bin/localepurge-config
arch-chroot $root localepurge


Переходим в read-only


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

Проблема в том, что для нормальной работы системы необходима возможность записывать данные в некоторые папки. Решение на поверхности — подключить эти папки через fstab как tmpfs — замечательно подойдёт для /var/log, например. Но как поступить, например, с каталогом /etc, ведь наше правило udev меняет там файлы, да и другие программы активно с ним работают? Можно где-то сохранить информацию перед монтированием, а потом переписать обратно. Или сразу перенести всё куда-то ещё, а потом вернуть. Ясно одно: придётся долго тестировать и следить за работой системы, чтобы понять какие ещё папки сделать доступными для записи, или же настроить все программы так, чтобы они оставляли продукты своей жизнедеятельности строго в отведённом месте. Слишком мудрёно. Предлагаю всю систему развернуть в RAM. Останется только предварительно переписать в неё всё самое нужное для работы.

Существует одна папка, в которую во время работы ничего не записывается, если мы ничего не устанавливаем — это /usr. Если подмонтировать её на позднем этапе работы initramfs с доступом только для чтения, то на работу Firefox это никак не повлияет. Обязательно сравните размер каталога /usr с размером всего остального, и получится, что копировать останется не так много, а если при этом исключить всё лишнее… Вы тоже подумали о rsync?

Переделываем файловую систему на лету


Устанавливаем rsync на клиента:

pacman -S rsync

Заниматься копированием предстоит на этапе работы intramfs, следовательно, понадобится новый обработчик, назовём его «live». Сначала сохраним все необходимые параметры монтирования оригинального корневого каталога, путём анализа файла /etc/fstab с помощью утилиты findmnt. Затем корневой каталог отмонтируем от папки /new_root, где он всегда находится внутри initramfs. На его месте создадим ramfs с возможностью записи и подготовим точку монтирования /srv/new_root, куда вернём оригинальный корневой каталог. Останется только переписать все самые нужные файлы и каталоги, за исключением папки /usr, которую забиндим в режиме только для чтения. Копии файлов в ramfs будут доступны для чтения и для записи.
nano $root/etc/initcpio/hooks/live

cat $root/etc/initcpio/hooks/live
#!/usr/bin/bash

run_latehook() {
    local source options fstype
    local target="/"
    local fstab=/new_root/etc/fstab
    local place=/new_root/srv/new_root
    local filter=${place}/etc/default/live_filter

    if source=$(findmnt -snero source --tab-file=$fstab -T $target); then
        options=$(findmnt -snero options --tab-file=$fstab -T $target)
        fstype=$(findmnt -snero fstype --tab-file=$fstab -T $target)

        umount /new_root
        mount -t ramfs none /new_root -o rw,defaults

        [ ! -d "$place" ] && mkdir -p $place
        mount ${fstype:+-t ${fstype}} ${options:+-o ${options}} $source $place
        mount -o remount,ro${options:+-,${options}} $source $place

        rsync -aAX ${place}/* /new_root --filter="merge $filter"

        ! findmnt -snero source --tab-file=$fstab -T /usr && bind_usr $place

        # чтобы не допустить перемонтирование "/" во время загрузки,
		# удаляем информацию по нему из fstab
		cat ${place}/etc/fstab | grep -v $source > $fstab
    fi
}

bind_usr(){
    local place=$1
    mount --bind ${place}/usr /new_root/usr
    mount -o remount,ro,bind ${place}/usr /new_root/usr
}


К файлу /etc/fstab мы обращаемся дважды: первый раз получаем информацию по параметрам монтирования корневого каталога, а второй раз проверяем, есть ли в fstab какая-нибудь информация по /usr. Для позднего монтирования /usr в Archlinux есть специальный обработчик usr, которому мы не будем мешать выполнять свою работу. Если /usr монтируется каким-то особым образом, то наш обработчик его пропускает.

В тексте упомянут файл /etc/default/live_filter с правилами фильтрации, предназначенными для rsync, нам нужно не забыть его подготовить. Сделаем это автоматически из установщика обработчика:

nano $root/etc/initcpio/install/live
#!/usr/bin/bash

build() {
    make_filter > /etc/default/live_filter
    add_binary "/usr/bin/rsync" "/bin/rsync"
    add_binary findmnt
    add_runscript
}

make_filter() {
    cat <<EOF
+ /etc/*
+ /home/*
+ /home/*/.config
- /home/*/*/
+ /var/*
- /var/cache/*/*
- /var/lib/pacman/*/*
- /var/lib/systemd/*/*
+ /var/log/*/
- /var/log/*
- /var/tmp/*
- /*/*
EOF
}

Rsync «не видит» дальше одной директории. Файлы и папки в директории проверяются каждым правилом по порядку до первого совпадения ("+" — объект копируется, "-" — объект не копируется). Если совпадений нет, то файл копируется, а директория создаётся пустой. Далее rsync заходит в «выжившую» директорию и снова применяет правила к её содержимому. Так повторяется до тех пор пока совсем ничего не останется.

В нашем случае корневой каталог не попадает ни под одно правило, поэтому его структура полностью переносится (копируются все файлы и создаются пустые каталоги). Каталоги /boot, /dev, /lost+found, /mnt, /opt, /proc, /root, /run, /srv, /sys, /tmp попадают под действие последнего правила "- /*/*", т.е. никакое их содержимое никуда не копируется, но сами они создаются. Каталог /etc сразу же попадает под правило "+ /etc/*", и всё его содержимое копируется, но сначала только в пределах одного каталога (в дальнейшем вся его структура будет перенесена по порядку, потому что для уровней вложенности /etc/*/ и далее никаких правил нет). Похожее начало ждёт каталог /home — папки всех пользователей попадают под правило "+ /home/*" и будут воссозданы в копии (пока пустыми). Следующее правило "+ /home/*/.config" копирует каталоги .config, вложенные в домашние папки каждого из пользователей, а "- /home/*/*/" исключает все остальные каталоги (правило идёт после «спасательного», поэтому для /home/*/.config не срабатывает). Про сами файлы из домашнего каталога ничего не говорится, поэтому они полностью переносятся. Файлы из исключённых вложенных каталогов не копируются, потому что эти каталоги не были созданы. Правило "- /var/cache/*/*" сохраняет всю структуру каталогов в /var/cache, но их содержимое не переносится. Остальные правила действуют аналогичным образом.

Замечания
Правила для rsync, находящиеся во внешнем файле /etc/default/live_filter, вы можете менять по своему усмотрению без необходимости заново создавать initramfs. Буду рад увидеть ваш вариант правил в комментариях.

Возможностей у rsync очень много (man rsync — почти 3000 строк). Предложите в комментариях какой-нибудь экзотический способ использования rsync внутри initramfs?

Теоретически rsync можно заменить на какой-нибудь torrent, и собирать корневую файловую систему с его помощью.

Добавляем обработчик в initramfs:

cat $root/etc/mkinitcpio.conf
...
HOOKS="base udev net_nfs4 live"

Генерируем initramfs:

arch-chroot $root mkinitcpio -p habr

nfs4+live
Сервер и клиент работают в VirtualBox.
Исходная файловая система:

cat $root/etc/fstab
# <file system>                  <dir> <type>   <options>           <dump> <pass>
192.168.1.100:/diskless          /     nfs4     defaults,noatime    0      0

Состояние файловой системы на загруженном клиенте после выполнения обработчика live:

mount
...
none on / type ramfs (rw,relatime)
192.168.1.100://diskless on /srv/new_root type nfs4 (ro,noatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.131,local_lock=none,addr=192.168.1.100)
192.168.1.100://diskless/usr on /usr type nfs4 (ro,noatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.131,local_lock=none,addr=192.168.1.100)
...

Во время загрузки клиента на сервере были собраны следующие данные:

vnstat -l
...
eth0  /  traffic statistics

                           rx         |       tx
--------------------------------------+------------------
  bytes                     7,23 MiB  |      252,33 MiB
--------------------------------------+------------------
          max            5,11 Mbit/s  |   235,23 Mbit/s
      average            1,48 Mbit/s  |    51,68 Mbit/s
          min               0 kbit/s  |        1 kbit/s
--------------------------------------+------------------
  packets                      82060  |          199036
--------------------------------------+------------------
          max               6550 p/s  |       21385 p/s
      average               2051 p/s  |        4975 p/s
          min                  0 p/s  |           0 p/s
--------------------------------------+------------------
  time                    40 seconds



Разгоняем сеть


Физически, естественно, разгон сети сейчас невозможен без замены оборудования, зато программные оптимизации не запрещаются. Нам нужно передавать содержимое связанной папки /usr по сети. Не отправлять эти данные мы не можем, зато способны уменьшить объём занимаемого ими места — заархивировать. На сервере сжимаем, а на клиенте — распаковываем, и через ту же самую сеть теоретически передаётся больше данных за единицу времени.

Файловая система squashfs совмещает в себе возможности архиватора и монтирования архивов через fstab, как обычную файловую систему. Основной недостаток данной файловой системы — невозможность работать в режиме записи (только для чтения) — для нас недостатком не является:

pacman -S squashfs-tools && mksquashfs $root/usr $root/srv/source_usr.sfs -b 4096 -comp xz


Монтировать будем так:
nano $root/etc/fstab
# <file system>                  <dir> <type>   <options>           <dump> <pass>
192.168.1.100:/diskless          /     nfs4     defaults,noatime    0      0
/srv/new_root/srv/source_usr.sfs /usr  squashfs loop,compress=xz    0      0

На позднем этапе работы initramfs монтированием папки /usr занимается обработчик usr, который нужно немного подправить:

cp $root/{usr/lib,etc}/initcpio/install/usr && cp $root/{usr/lib,etc}/initcpio/hooks/usr

Нужно, чтобы строка монтирования выглядела так:

nano $root/etc/initcpio/hooks/usr
mount "/new_root$usr_source" /new_root/usr -o "$mountopts"

Чем не устраивает usr?
Обработчик требует указание поля «file system» в файле fstab в виде "/new_root/srv/new_root/usr/source_usr.sfs". Этот файл обрабатывается systemd на раннем этапе загрузки целевой системы, и производится перемонтирование всех указанных там директорий. Папка /new_root cуществует только на этапе ранней загрузки initrams, поэтому systemd фиксирует ошибку. Ошибка ни на что не влияет, но убрать её не сложно.

cat $root/etc/mkinitcpio.conf
HOOKS="base udev net_nfs4 live usr"
arch-chroot $root mkinitcpio -p habr

nfs4+live+squashed /usr
Исходная файловая система:

cat $root/etc/fstab
# <file system>                  <dir> <type>   <options>           <dump> <pass>
192.168.1.100:/diskless          /     nfs4     defaults,noatime    0      0
/srv/new_root/srv/source_usr.sfs /usr  squashfs ro,loop,compress=xz 0      0

Состояние файловой системы на загруженном клиенте после выполнения обработчиков live и usr:

mount
...
none on / type ramfs (rw,relatime)
192.168.1.100://diskless on /srv/new_root type nfs4 (ro,noatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.131,local_lock=none,addr=192.168.1.100)
/srv/new_root/srv/source_usr.sfs on /usr type squashfs (ro,relatime)
...

Во время загрузки клиента на сервере были собраны следующие данные:

vnstat -l
...
 eth0  /  traffic statistics

                           rx         |       tx
--------------------------------------+------------------
  bytes                     5,07 MiB  |      205,67 MiB
--------------------------------------+------------------
          max            4,02 Mbit/s  |   191,82 Mbit/s
      average            1,04 Mbit/s  |    42,12 Mbit/s
          min               0 kbit/s  |        1 kbit/s
--------------------------------------+------------------
  packets                      65524  |          159941
--------------------------------------+------------------
          max               5954 p/s  |       17170 p/s
      average               1638 p/s  |        3998 p/s
          min                  0 p/s  |           0 p/s
--------------------------------------+------------------
  time                    40 seconds



Данных пришлось передать примерно на 20% меньше, чем в предыдущий раз. Можно упаковать весь корневой каталог в один файл, тогда обработчик live для заполнения ramfs будет забирать с сервера данные в сжатом виде.

Можно скопировать файл /srv/source_usr.sfs в ramfs поменяв правила в фильтре rsync, а потом примонтировать его через fstab из нового места, и, когда вся система целиком окажется в RAM, попробовать отключиться от загрузочного сервера.

Убираем лишнее


Если вы заглядывали сюда, то у вас не возникнет вопрос: «Как мы будем отдавать с сервера файл?». Можно, конечно, передавать данные squashfs посредством NFS (что и происходило выше), но существует менее документированное решение Network Block Device, с которым можно работать как с обычным диском. Поскольку это «блочное устройство», а не «файловая система», мы можем использовать на нём любую файловую систему с возможностью сжатия данных. Для доступа на чтение и запись подойдёт btrfs с архивацией zlib, но нам не нужна запись и squashfs вполне устраивает.

Чтобы из initramfs можно было подключиться к NBD-серверу при загрузке понадобится скачать из AUR пакет mkinitcpio-nbd (нужно скачивать и собирать с правами обычного пользователя):

curl -o mkinitcpio-nbd.tar.gz https://aur.archlinux.org/packages/mk/mkinitcpio-nbd/mkinitcpio-nbd.tar.gz
tar -xvvzf mkinitcpio-nbd.tar.gz
cd mkinitcpio-nbd
makepkg -s
sudo pacman --root $root --dbpath $root/var/lib/pacman -U mkinitcpio-nbd-0.4.2-1-any.pkg.tar.xz


Добавляем в конец файла $root/boot/grub/grub.cfg новый пункт меню:

cat $root/boot/grub/grub.cfg

menuentry "NBD" {
    load_video
    set gfxpayload=keep
    insmod gzio
    echo "Загружается ядро..."
    linux vmlinuz-linux \
         add_efi_memmap \
         ip="$net_default_ip":"$net_default_server":192.168.1.1:255.255.255.0::eth0:none \
         nbd_host="$net_default_server" nbd_name=habrahabr root=/dev/nbd0
    echo "Загружается виртуальный диск..."
    initrd initramfs-linux.img
}

Как видите, поменялась только одна строчка:

nbd_host="$net_default_server" nbd_name=habrahabr root=/dev/nbd0

После подключения к NBD серверу в клиенте появляется блочное устройство с именем /dev/nbd0, поэтому поступаем с ним как с обычным диском:

nano $root/etc/fstab

# <file system>            <dir>  <type>    <options>           <dump> <pass>
/dev/nbd0                  /      squashfs  ro,loop,compress=xz 0      0


В последних версиях NBD сервера появилась непрятная особенность (скорее всего это баг). Когда клиент NBD устанавливает соединение с сервером, а потом внезапно выключается не завершая соединение корректно, и оно продолжает «болтаться» на сервере в виде незавершенного процесса. Если клиент во время загрузки попробует подключиться к NBD заново, то есть вероятность, что сервер не станет создавать новое соедиенение считая старое активным. Предлагаю непосредственно перед подключением к NBD отправлять свой IP адрес через netcat на сервер, чтобы тот закрыл старые подключения, связанные с этим IP адресом:

cp $root/{usr/lib,etc}/initcpio/install/nbd
cp $root/{usr/lib,etc}/initcpio/hooks/nbd

Нужно отредактировать только один файл. Вставьте между строками следующий фрагмент:

nano $root/etc/initcpio/hooks/nbd

                modprobe nbd # вставляете после этой строки
                msg "closing old connections..."
                echo ${ip} | nc ${nbd_host} 45678
                local ready=$(nc -l -p 45678)
                [ "$ready" -ne 1 ] && reboot
                msg "connecting..." # и перед этой строкой

В initramfs сетью по-прежнему заведует наш модифицированный net_nfs4, после которого вставляем nbd:

nano $root/etc/mkinitcpio.conf

MODULES="loop squashfs"
HOOKS="base udev net_nfs4 keyboard nbd live"

Генерируем initramfs:

arch-chroot $root mkinitcpio -p habr

Перед выполнением следующей команды удалите или переместите файл $root/srv/source_usr.sfs за пределы $root — не имеет смысла помещать архив /usr внутрь архива, содержащего оригинал /usr:

mksquashfs $root/* /srv/new_root.sfs -b 4096 -comp xz

Переходим к настройке сервера

Устанавливаем пакет:
pacman -S nbd

Настраиваем NBD сервер:

mv /etc/nbd-server/{config,config.old} && nano /etc/nbd-server/config

[generic]
        user = nbd
        group = nbd
[habrahabr]
        exportname = /srv/new_root.sfs
        timeout = 30
        readonly = true
        multifile = false
        copyonwrite = false

Всё достаточно просто. Мы создаём шару с именем habrahabr, ссылаемся на наш файл, устанавливаем таймаут соединения, раздаём в режиме «только для чтения», отдаём только один файл и функция copyonwrite нам не нужна. Copyonwrite позволяет использовать одну и ту же раздачу несколькими клиентами одновременно, при этом каждому клиенту создаётся отдельный файл, куда будут записываться все произведённые им изменения оригинального файла. После отключения клиента файлы с изменениями удаляются автоматически. Использование этой функции замедляет сервер. Информации по NBD в интернете не так много, но man'ы решают.

Проверять и завершать процессы, связанные с незакрытыми соединениями будет вот этот файл:

nano /etc/default/close_passive_NBD_connections.sh

#!/bin/sh
# завершает все процессы с полученными PID
_kill(){
    local PID
    for PID in $*
        do kill $PID
    done
}
main(){
    local rIP PIDs
	# нам передают с клиента значение переменной ip из параметров ядра в grub.cfg
    rIP=$(netcat -l -p 45678 | cut -d: -f1)
	# фильтруем пакеты с полученного IP адреса и узнаём их PID
    PIDs=$(netstat -np | grep $rIP | awk '/^tcp.*nbd-server/{split($NF,a,"/");print a[1]}')
    _kill $PIDs && echo "1" | netcat -z $rIP 45678
}
# повторяем в бесконечном цикле
while [ 0 ]
do main
done

Файл делаем исполняемым:

chmod +x /etc/default/close_passive_NBD_connections.sh

Устанавливаем пакеты, в которых находятся утилиты netcat и netstat:

pacman -S gnu-netcat net-tools

Модифицируем запуск службы NBD:

mkdir -p /etc/systemd/system/nbd.service.d && nano /etc/systemd/system/nbd.service.d/close_passive.conf

[Service]
Type=oneshot
ExecStart=/etc/default/close_passive_NBD_connections.sh

Возможно, выбрано не самое изящное решение, но оно достаточно понятно и замечательно работает.

nbd + squashed live
Исходная файловая система:

cat $root/etc/fstab
# <file system> <dir> <type>   <options>           <dump> <pass>
/dev/nbd0       /     squashfs ro,loop,compress=xz 0      0

Состояние файловой системы на загруженном клиенте после выполнения обработчиков live и usr:

mount
...
none on / type ramfs (rw,relatime)
/dev/nbd0 on /srv/new_root type squashfs (ro,relatime)
/dev/nbd0 on /usr type squashfs (ro,relatime)
...

Во время загрузки клиента на сервере были получены следующие данные:

vnstat -l
...
 eth0  /  traffic statistics

                           rx         |       tx
--------------------------------------+------------------
  bytes                     1,97 MiB  |      198,92 MiB
--------------------------------------+------------------
          max            2,81 Mbit/s  |   138,60 Mbit/s
      average          575,63 kbit/s  |    58,20 Mbit/s
          min               2 kbit/s  |        1 kbit/s
--------------------------------------+------------------
  packets                      32473  |          100874
--------------------------------------+------------------
          max               5991 p/s  |        7576 p/s
      average               1159 p/s  |        3602 p/s
          min                  4 p/s  |           1 p/s
--------------------------------------+------------------
  time                    28 seconds


На этот раз мы сэкономили ещё всего лишь 3% трафика (в пределах погрешности). Разница во времени загрузки объясняется тем, что при использовании NFS перед подключением к серверу делается принудительная пауза в 10 секунд, а в случае сервера NBD такой задержки нет.

Педаль в пол


Давайте попробуем ускорить загрузку. Самое слабое звено в нашей цепочке загрузки — TFTP сервер. Полностью исключить его мы не сможем, но минимизировать его присутствие можно с помощью загрузчика iPXE, как посоветовал kvaps в комментариях к предыдущей статье.

Подключитесь к загрузочному серверу под именем username.

Меню с вариантами загрузки мы делать не будем, а автоматически загрузимся в самый быстрый на текущий момент:

nano ~/myscript.ipxe

#!ipxe

ifopen net0

set server_ip 192.168.1.100
set http_path http://${server_ip}
set kern_name vmlinuz-linux

kernel ${http_path}/${kern_name} || read void
initrd ${http_path}/initramfs-linux.img || read void
imgargs ${kern_name} add_efi_memmap ip=${net0/ip}:${server_ip}:${net0/gateway}:${net0/netmask}::eth0:none nbd_host=${server_ip} nbd_name=habrahabr root=/dev/nbd0 || read void
boot || read void

Мы планируем получать файлы vmlinuz-linux и initramfs по протоколу HTTP. Внедрим наш скрипт в загрузчик:

sudo pacman -S git && git clone git://git.ipxe.org/ipxe.git
cd ipxe/src/
make bin/undionly.kpxe EMBED=/home/username/myscript.ipxe

Возвращаемся в root на сервере и копируем загрузчик:

cp {/home/username/ipxe/src/bin,$root/boot}/undionly.kpxe

Исправим DHCP сервер таким образом, чтобы он предлагал скачивать новый файл:

nano /etc/dhcpd.conf
#if option architecture = 7 {
# filename "/grub/x86_64-efi/core.efi";
# } else {
# filename "/grub/i386-pc/core.0";
#}
filename "/undionly.kpxe";
systemctl restart dhcpd4


Устанавливаем HTTP сервер:

pacman -S apache

привязываем папку с загрузчиком к рабочей папки сервера:

mount --bind /srv/nfs/diskless/boot/ /srv/http/

Можно перемонтировать в режим «только для чтения»:

mount -o remount,ro,bind /srv/nfs/diskless/boot/ /srv/http/

Запускаем сервер:

systemctl start httpd


Смотрим, что происходит на сервере:

vnstat -l
...
                            rx         |       tx
--------------------------------------+------------------
  bytes                     1,50 MiB  |      206,73 MiB
--------------------------------------+------------------
          max            2,96 Mbit/s  |   191,95 Mbit/s
      average          684,08 kbit/s  |    94,08 Mbit/s
          min               5 kbit/s  |        1 kbit/s
--------------------------------------+------------------
  packets                      22762  |           90737
--------------------------------------+------------------
          max               5735 p/s  |        9871 p/s
      average               1264 p/s  |        5040 p/s
          min                  3 p/s  |           1 p/s
--------------------------------------+------------------
  time                    18 seconds


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

Возвращаемся на сервер


Попробуйте добавить обработчик live в наш загрузочный сервер. Сейчас правила rsync пропускают копирование содержимого /srv, где у нас находятся файлы клиента. Мы можем поменять правила или примонтировать директорию с помощью systemd:

nano /etc/fstab

LABEL=HABR        /    ext4 rw,relatime,data=ordered 0 1
/srv/new_root/srv /srv none bind                     0 1

В данном случае папки /srv/new_root/srv и /srv связываются в режиме полного доступа на чтение и запись, но мы знаем решения.

Тот факт, что загрузочный сервер может работать в режиме «только для чтения», будет весьма полезен для систем, установленных на недорогую USB флешку. С такого накопителя лучше побольше читать, и поменьше на него записывать. Если вы откроете его в интернет, то получите дополнительную степень защиты. Например, роутер открывает защищенный VPN канал, на другом конце которого находится загрузочный сервер…

Чтобы переписать систему на флешку (с жёстким диском принцип тот же самый), её нужно вставить в компьютер и подключить к VirtualBox (Меню Устройства > Устройства USB и выбрать нужную из списка). Список доступных блочных устройств проверяется командой lsblk, как в самой первой статье. Разметьте флешку пометив загрузочной, отформатируйте с той же меткой HABR и примонтируйте к /mnt.

Создадим новый файл с правилами для rsync:

nano /root/clone_filter

+ /boot/*
+ /etc/*
+ /home/*
+ /srv/*
+ /usr/*
+ /var/*
- /*/*

Дождитесь выполнения команды:

rsync -aAXv /* /mnt --filter="merge /root/clone_filter"

В моём случае флешка — /dev/sdb
 arch-chroot /mnt grub-install --target=i386-pc --force --recheck /dev/sdb

Остаётся отмонтировать /mnt и можно загружаться из копии.

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

Поделитесь своими идеями применения.
+39
72.7k 684
Comments 12
Top of the day