...

вторник, 31 марта 2015 г.

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

Сначала мы научимся исследовать установленные в компьютере устройства прямо во время загрузки с помощью 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



вывод команды
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"




Каждое правило записывается в новой строке. Первая часть служит для идентификации устройства, к которому нужно применить действие, указанное в конце строки. Для опознания используются данные, которые получаем в выводе команды «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




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 сделаем переключение раскладки клавиатуры комбинацией 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 http://ift.tt/1ac6mWT
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



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 http://ift.tt/1ac6plv
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, как в самой первой статье. Разметьте её пометив загрузочной, отформатируйте с той же меткой и примонтируйте к /mnt.


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



nano /root/clone_filter

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




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

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



Остаётся отмонтировать флешку и можно с неё загружаться.

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


Поделитесь своими идеями применения.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий