Colaboot

Материал из ALT Linux Wiki
50px-Gnome globe current event.png
Данная статья периодически обновляется.
Последнее обновление
22:58 21 марта 2018

CoLaBoot (Compressed Layers Boot) - система загрузки, при которой корневая файловая система может быть представлена в виде отдельных функциональных сжатых слоёв.

Основные идеи

  • корневая файловая система собирается из отдельных слоёв в OverlayFS, и при этом каждый образ слоя сжат в SquashFS.
  • использование слоёв позволяет нарезать файловую систему функциональными слоями, используя "наследственность" и "переиспользование":
Допустим, у нас есть слой базовой системы. Поверх него мы можем сделать небольшие дополнительные слои installer, rescue, и слой побольше live-system. А поверх live-system еще extended-live-system с тяжёлыми мультимедиа и офисными пакетами. И всё это в сумме займёт меньше места, чем набор "самостоятельных" образов.
  • использование squashfs минимизирует используемую для кеширования образов слоёв память, а так же позволяет монтировать слои непосредственно с места, где они расположены.
  • образы слоёв могут как скачиваться по сети (http/ftp/tftp/...), так и монтироваться напрямую с локальных носителей (cdrom/hdd). Или скачиваться с cdrom, чтобы не держать его в использовании. Или монтироваться напрямую с NFS/iSCSI. Как удобно, в общем.
  • слои могут быть optional - очень удобно для накатывания оперативных обновлений, которых может и не быть. Выпустили ISO с инсталлятором, а последние обновления ищутся при загрузке хоста на ftp.altlinux.org, а если не нашлись (интернета нет) - ничего страшного, продолжим без них.
  • стремительность разработки: оперативные изменения могут вноситься только в последний, относительно небольшой по размеру слой, и цикл "нашли проблему - пофиксили слой - экспортировали слой в squashfs - перезагрузили тестовую виртуалку - проверили" реально укладывается в пределах одной минуты.
  • полное разделение собственно системы от используемой версии ядра и всех его модулей - загрузочные модули встраиваются в initrd или подключаются к нему отдельной initramfs по ситуации, полный набор модулей подключается как отдельный слой, совершенно независимый от остальной системы. Таким образом, необходимость обновить ядро не затрагивает все остальные слои (похоже на контейнерную виртуализацию, системе в контейнере всё равно, что там за ядро в хост-системе), так же, используемая система не ограничивается ALTLinux. Главное, чтобы была совместима с ядром.
  • слои можно делать вручную. Но вообще в качестве источников слоёв крайне органически подходит Докер, он сам основан на той же идеологии. При использовании драйвера хранения overlay/overlay2 каждый образ Докера легко экспортируется в отдельный образ слоя.
  • возможность использования уже готовых докерных образов, коих миллиарды - главное, чтобы /sbin/init было и желательно udev.
  • просто создано для тощих клиентов! (собственно, для них изначально и разрабатывалось).

Текущая реализация

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

  • make-initrd-colaboot с соответствующей фичей для Make-initrd (по мотивам make-initrd-netboot),
  • colaboot-utils cо скриптами для изготовления образов слоёв и образов с модулями ядра.

Там же и немного документации ([1][2]), и разнообразные примеры изготовления-использования ([3][4]).

Quckstart

(на примере сборки образа ISO. Для загрузки по сети всё то же самое, только нужна будет инфраструктура для этой загрузки - DHCP сервер, HTTP/TFTP/etc; и модули для сетевой карты, а не устройства CDROM)

  1. Первым делом нам нужны образы слоёв корневой файловой системы (может быть и в единственном числе). Штатным способом образы добываются из Докера, с помощью скрипта docker2squash. Но можно так же вручную сжать какую-то директорию с помощью mksquashfs, или взять уже готовый образ (убедитесь, что его модули будет совместимы по версии ядра).
    Если вы экспортируете образ Докера, и это не base image, вам необходимо так же экспортировать всех его предков - или вручную вызывая скрипт для каждого образа, или воспользовавшись режимом chain. В случае, если вы хотите получить один неделимый образ, при экспорте можно объеденить образ Докера со всеми его предками, использовав режим merged.
    # docker2squash altlinux-p8-clb-base
    # docker2squash altlinux-p8-clb-network
    
  2. Далее, делаем initrd с поддержкой colaboot (надо не забыть сначала установить пакет make-initrd-colaboot). Загляните в конфиг /etc/initrd.mk.d/colaboot.mk.example, всё ли в нём устраивает. Основная идея - класть в сам initrd как можно меньше модулей для универсальности и минимизации его размера, а все необходимые по ситуации модули предоставлять в качестве дополнительной initramfs.
    # make-initrd -N -c /etc/initrd.mk.d/colaboot.mk.example -b ./ 2>mkinitrd.log
    
  3. Теперь нам нужен образ initramfs с модулями ядра для обретения устройства /dev/sr0 в загружаемой системе. Для QEMU подойдёт такой вот список модулейcdrom.modlist, для железного хоста он может отличаться (а выяснить точный их список всегда можно с помощью make-initrd guess-modules /dev/sr0.
    Для приготовления такого образа воспользуемся скриптом modlist2image:
     # modlist2image `uname -r` cpio /path/to/cdrom.modlist
    
    и полученный результат переименовываем по схеме 8.3 (только буквы-цыфры), чтобы isolinux его осилило. Например, в cdrommod.img.
  4. Делаем директорию с проектом ISO (у вас должен быть установлен syslinux и mkisofs), кладём туда:
    • isolinux.bin, isolinux.cfg, и всякую syslinux-специфику (вроде menu.c32).
    • vmlinuz и initrd.img (который с фичей colaboot), эти имена файлов тоже в формате 8.3
    • дополнительные initramfs (в нашем случае - вышесделанный cdrommod.img)
    • все образы слоёв, полученный на первом этапе. Если слоёв несколько, должен быть так же создан файл манифеста с описанием их порядка загрузки (вроде такого), а если слой только один, то его можно описать в манифесте как единственный слой, или обойтись вообще без манифеста, указав путь к образу такого слоя в параметре загрузки root=. И, скорее всего, вам так же понадобится слой с полным необходимым набором модулей ядра (а не только требуемых для загрузки, как в initramfs).
  5. Отредактируйте isolinux.cfg под ваши нужды, правильно указав имена файлов для ядра, initrd, дополнительных initramfs, и параметр root=. Например:
    LABEL CLB-TEST
    MENU LABEL CLB TEST
        KERNEL vmlinuz
        INITRD initrd.img,cdrommod.img
        APPEND root=file:///sr0/clb-test.manifest rootdelay=5
    
  6. Изготавливаем из директории проекта образ iso:
    # mkisofs -o clb-test.iso -b isolinux.bin -c boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -J -r /path/to/iso-project-dir
    

Референсный образец демо-ISO можно скачать здесь.

Зачем слои?

Деление на слои имеет смысл там, где какой-то слой может быть переиспользован (для другого потомка или в качестве самостоятельного).

Так, в моих примерах:

  • был изначальный-неделимый base, от которого произошёл network (ума не приложу, зачем нужен без-сетевой хост, так что наверное имело бы смысл как раз network сделать базовым, ну да ладно).
  • network породил xorg. А почему их не объеденить? network уже сам по себе пригоден в качестве самостоятельной минимальной системы, для быстрого обзора загруженного хоста например, и может быть родителем для всяких rescue, server и прочих образов-без-гуи. Поэтому отдельно.
  • xorg сам по себе не особо полезен, но является универсальным родителем для:
    • rdp-shell
    • firefox kiosk
    • lightdm
  • а поверх lightdm, который сам по себе - тоже бесполезный-универсальный-родитель, всякие xfce/lxde/fluxbox.

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

И конфиги, которые могут прискорбно-часто меняться, тоже удобно оформлять в качестве последнего микро-слоя, чтобы его можно было очень быстро изменить-распространить.

Или создавать последний микро-слой для дебага какой-нибудь вредной ошибки, опять же для скорости.

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

Думаю, тут можно еще раз упомянуть про возможность брать при загрузке каждый образ из своего индивидуального месторасположения. Может оказаться удобным какие-то слои хранить в сети (оперативные обновления, конфиги), какие-то на локальных дисках, какие-то таскать из интернета. Или это может быть тщательно продуманной политикой использования multi-tiered storage, когда разные слои лежат на дисках с разным быстродействием.

Для ограниченных по ресурсам хостов (тонких клиентов), слои могут быть средством минимизации итоговой файловой системы; если конкретный хост используется как веб-киоск, зачем ему RDP и все его библиотеки?

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

Ну и можно представить сценарий, когда слои не нужны - финальный там образ какой-то неделимой системы... ну ок, можно делать и такие, или экспортируя готовый base image, или в режиме merge объеденяя какой-то целевой слой со всеми его родительскими. Или вообще натравить mksquashfs на любую директорию с готовой системой вручную. Или взять готовый live.squash (или как он там называется) с официальных ISO-образов. Только нужно убедиться, что в таком монолитном образе есть все необходимые модули для вашего ядра и вашего хоста.

Докер как источник слоёв

Как я уже сказал, в качестве источника образов файловой системы оказалось очень удобным использовать Докер, который неразрывно связан с концепцией слоёв (погуглите: докер+слои. И очень рекомендую вот эту наглядную шпаргалку). Для получения squashfs-образов из Докера был сделан скрипт docker2squash, который позволяет экспортировать как образы Докера, так и контейнеры, в т.ч. работающие на момент экспорта, в трёх разных режимах:

  • diff - режим по умолчению. В squashfs экспортируется только та часть файловой системы, которая непосредственно принадлежит экспортируемому объекту. Если этот объект является частью цепочки, т.е. у него есть предки и/или потомки, то они так же должны быть экпортированы (предки - обязательно, потомки - по ситуации).
  • merged - объект при экспорте объединяется со всеми его предками (но не потомками!) и мы получаем полный самодостаточный образ. По идее ничем не отличается от тех squashfs-образов, которые можно найти на любом инсталляционном компакт-диске.
  • chain - экспортируется выбранный объект и все его предки поочерёдно и индивидуально, в режиме diff. Позволяет получить всю цепочку образов для указанного объекта. В качестве приятного дополнения так же генерируется шаблон файла манифеста с указанием всех экспортированных слоёв в правильной очерёдности.

Важно! Докер должен быть настроен на использование драйвера хранения overlayfs.

Эта возможность получать загрузочные образы из Докера позволяет высокоэффективно использовать его для разработки этих самых образов - такие операции, как создание контейнера из образа, его запуск, модификация, и сохранение в другой образ очень быстры и наглядны, и в интернете есть об этом множество тематических статей и документации. И, если взглянуть с другой стороны, раз мы можем запустить приготовленный образ Докера в виртуалке или на любом железном хосте, можно рассматривать CoLaBoot в качестве "плеера" для Докера, в devops целях.

Итак, самый первый образ в цепочке (base image в терминологии Докера), можно сделать путём импорта из .tar-образа. А в качестве источника этого базового .tar замечательно подходит Mkimage-profiles с его богатейшими наработками имиджестроения (но с лёрнинг-курвой овер 90°). Мне показалась наиболее подходящей цель ve/systemd-bare, которую я минимально модифицировал:

ve/clb: ve/systemd-bare use/deflogin/desktop
        @$(call add,BASE_PACKAGES,iconv htop)

Получившийся clb-latest-x86_64.tar я и импортировал в Докер:

# docker import -c 'CMD ["/bin/bash"]' clb-latest-x86_64.tar altlinux-p8-clb-base

Создание новых Докер-образов на основе имеющихся предусматривается двумя методами:

  • можно сделать из образа контейнер, войти в него, сделать все необходимые действия (установить-удалить пакеты, изменить конфиги, ...) и сохранить результат в дочерний образ коммандой docker commit.
  • или написать специальный Dockerfile с автоматически выполняемыми инструкциями, необходимыми для модификации образа A в образ B.

Казалось бы, Dockerfile является предпочтительным методом, гарантирующим повторяемость сборки. Однако у него есть один совершенно не подходящий для нас побочный эффект: там чуть ли не каждую строчку в этом файле происходит коммит нового образа, и получается, что между образами A и B он может создать еще несколько безымянных образов, которые невозможно удалить, так как они являются непосредственными предками для образа B. В интернетах встречается много рецептов по минимизации этого эффекта, но полностью от них не отделаться, да и Dockerfile при применении этих рецептов сильно теряет в читаемости и наглядности.

Поэтому, не решившись лезть в частичное объединение образов (это совсем не то, что export-import из рецептов в интернетах), я решил пойти по другому пути: эмулировать несколько нужных функций, написать парочку своих [5], и использовать вместо Dockerfile обычный bash-скрипт с похожим синтаксисом, который, по инструкциям внутри, автоматически делает всю ручную работу по созданию нового контейнера, его модификации и единственному финальному коммиту нового образа. Назовём такой скрипт .docker-make файлом.

Получилось замечательно и гораздо гибче, чем нативные методы. Можно делать такие штуки, как отложенный коммит - когда вместо образа мы останавливаемся на фазе полумодифицированного контейнера, в который можно войти и проделать там какие-то ручные операции (которые потом, при достижении идеала, зафиксировать в .docker-make), или же выполнять .docker-make в пошаговом режиме - импортировать себе в шелл все эти функции, и дёргать их по одной, указывая в переменной ID рабочего контейнера, и/или режим отладки:

# . docker-functions
# CURRENT_CID=deadbeef APT-INSTALL my-package
# DEBUG=1 CURRENT_CID=deadbeef RUN my-command --args

Обратите внимание, там используется самостоятельная временная конфигурация apt, нацеленная на использование из контейнера локального репозитария по http. Скорее всего, вам нужно будет изменить её под свои нужды. Перед коммитом контейнера эта временная конфигурация должна быть удалена из контейнера, это делает функция APT-CLEAN.

Ну и пересборка всех ваших образов (например, с новой пакетной базой) в нужной очерёдности (что обеспечивается правильной нумерацией .docker-make файлов):

# for DM in [0-9]*; do ./$DM; done

Несколько особняком идёт .docker-make для приготовления контейнера с модулями ядра, который потом может использоваться скриптом modlist2image в качестве четвёртого опционального параметра, чтобы брать модули не из хост-системы, а из этого-самого контейнера.