Сначала мы научимся исследовать установленные в компьютере устройства прямо во время загрузки с помощью 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. Systemd при загрузке параллельно запускает службы, а udev параллельно инициализирует устройства, в связи с чем проявляются некоторые особенности. Например, если в компьютере установлены две видеокарты, то сначала первой может быть найдена одна из них, а после перезагрузки — другая. То же самое может произойти с накопителями и сетевыми картами, и от загрузки к загрузке будет меняться их имя. Поэтому для udev придуманы правила, которые должны внести порядок в этот хаос, и хранятся они в /etc/udev/rules.d/ (на самом деле, как и в случае обработчиков (hooks), есть ещё одна папка с правилами /usr/lib/udev/rules.d/, имеющая более низкий приоритет). Udev применяет подходящие правила к обнаруженным устройствам, сортирует и распределяет полученную информацию в каталогах /sys и /dev.
С точки зрения xorg видеокарты относятся к подсистеме или классу drm, поэтому для его удобства сведения о них дублируются в каталоге /sys/class/drm. Первая обнаруженная видеокарта по-умолчанию получает имя «card0», если в ней имеется несколько видеовыходов, то они получают имена вида «card0-CON-n», где «CON» — тип разъема (VGA, HDMI, DVI и др.), а «n» — порядковый номер разъема (причём одни производители нумеруют разъёмы начиная с «0», а другие — с «1»). Чтобы узнать то же самое о видеокарте, что знает про неё udev, введём команду:
udevadm info -a -p /sys/class/drm/card0
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"
Каждое правило записывается в новой строке. Первая часть служит для идентификации устройства, к которому нужно применить действие, указанное в конце строки. Для опознания используются данные, которые получаем в выводе команды «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». Программа будет срабатывать для каждого выхода каждой видеокарты, к которому подключен монитор. Для упрощения задачи заставим работать последний найденный вариант. Это не самый оптимальный способ настройки, но он рабочий и подходит для изучения правил udev в действии (было бы лучше проверить графическую подсистему один раз перед достижением graphical.target). Создаём файл программы со следующим содержанием:
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
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 сделаем переключение раскладки клавиатуры комбинацией 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=0nvm.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 (для работы Firefox этого достаточно). Сравните её размер с размером всего остального, и получится, что копировать придётся не так много, а если при этом исключить всё лишнее… Вы тоже подумали о rsync?
Переделываем файловую систему на лету
Устанавливаем rsync на клиента:
pacman -S rsync
Заниматься копированием нам придётся на этапе работы intramfs, следовательно, понадобится новый обработчик, назовём его «live». Для начала сохраним все необходимые параметры монтирования корневого каталога, а анализ оригинального файла /etc/fstab проведём с помощью утилиты findmnt. Новый корневой каталог внутри initramfs всегда монтируется в /new_root, откуда мы его отмонтируем, и на его месте создадим ramfs с возможностью записи. Подготовим точку монтирования /srv/new_root внутри ramfs, куда вернём оригинальный корневой каталог. Перепишем в ramfs все файлы и каталоги, за исключением папки /usr, которую забиндим в режиме только для чтения.
nano $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 очень много (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
Исходная файловая система:
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"
cat $root/etc/mkinitcpio.conf
HOOKS="base udev net_nfs4 live usr"
arch-chroot $root mkinitcpio -p habr
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
Возможно выбрано не самое изящное решение, но оно достаточно понятно и замечательно работает.
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, как в самой первой статье. Разметьте её пометив загрузочной, отформатируйте с той же меткой и примонтируйте к /mnt.
Создадим новый файл с правилами для rsync:
nano /root/clone_filter
+ /boot/*
+ /etc/*
+ /home/*
+ /srv/*
+ /usr/*
+ /var/*
- /*/*
Дождитесь выполнения команды:
rsync -aAX /* /mnt --filter="merge /root/clone_filter"
Остаётся отмонтировать флешку и можно с неё загружаться.
PS Решение разрабатывалось для автоматизации компьютерных классов. Система одинаково работает на пожертвованных и новых компьютерах с самыми разнообразными конфигурациями. Восстановление системы на клиенте к первоначальному состоянию производится обычной перезагрузкой. Нужные данные можно сохранять на диске загрузочного сервера или на любом другом сетевом или локальном накопителе.
Поделитесь своими идеями применения.
Автор: Roshalsky