Применение легковесных контейнеров LXC в настоящий момент довольно ограничено главным образом по причине их «сырости». Применять их в production – удел настоящих джедаев. Под production в данном случае подразумевается непосредственное предоставление услуг клиентам.
Однако для простого разделения сервисов и контроля ресурсов такие контейнеры вполне подходят с некоторыми допущениями. Например, мы полагаем, что root в контейнере равен root в целевой системе.
В статье будет показано, как можно быстро создавать легковестные контейнеры на локальном диске с общими файлами без использования LVM-снапшотов.
Кратко о сути контейнеров LXC
LXC является средством для реализации виртуальных контейнеров в ядре Linux. По сути своей LXC – это просто набор userspace утилит, которые эксплуатируют реализованные в ядре возможности. Как таковое понятие LXC в ядре Linux отсутствует.
Двумя основные составляющими контейнера являются пространства имен (namespaces) и контрольные группы (cgroups). Первые обеспечивают изоляцию процессов контейнера друг от друга, а вторые отвечают за ограничение ресурсов, выделенных контейнеру.
К настоящему моменту, действующими пространствами имен являются:
- pid – пространство имен идентификаторов процессов
- mount – пространство имен смонтированных файловых систем
- network – позволяет создавать изолированные сетевые стеки внутри контейнеров
- utsname – обеспечивает изоляцию структуры utsname. В первую очередь используется для установки разных hostname
- ipc – пространство имен SysV IPC. Разделяемая память, семафоры и очереди сообщению будут иметь разные id.
- user – пространство имен uid/gid
К слову сказать, последний namespace обещают окончательно допилить к версии ядра 3.9
Этих пространств вполне достаточно, чтобы контейнеры чувствовали себя независимыми.
Изначально все процессы в системе используют общие пространства имен. Создавая новый процесс, мы можем указать ядру склонировать нужные нам пространства имен для этого процесса. Это достигается путем указания специальных флагов CLONE_NEW* вызова clone(). Указывая этот флаг для определенного пространства имен при создании процесса, мы добиваемся того, что процесс будет создан в своем собственном пространстве имен. Именно так и работают утилиты LXC, создавая новый контейнер.
Отделить пространство имен существующего процесса можно с помощью вызова unshare(). Целиком заменить одно пространство имен процесса на другое можно с помощью setns(), но для этого вызова требуется поддержка новых ядер (>3.0).
Именно setns() используется для того, чтобы «впрыгнуть» в контейнер.
Контрольные группы, как и пространтсва имен, реализованы в ядре. В пространстве пользователя их использование доступно LXC с помощью интерфейса специальной файловой системы cgroup. LXC-утилиты создают каталог в этой файловой системе с именем контейнера, а затем записывают pid процессов в файлы контрольных групп. Поэтому имя контейнера по сути есть имя контрольной группы.
Подготавливаем систему к созданию контейнеров LXC
Пропустим этот шаг, поскольку о нем много где рассказано. Например, здесь. Суть конфигурации заключается в сборке ядра с нужными опциями и установке userspace утилит.
К счастью, многие ядра современных дистрибутивов уже собраны с этими опциями, поэтому пересборка скорее всего не понадобится.
Если Вы привыкли использовать libvirt для управления виртуализацией, то есть хорошая новость – libvirt полностью поддерживает LXC. В статье о нем рассказано не будет, чтобы быть «ближе к телу».
Создаем основу файловой системы для контейнеров
Обычно делают так: создают некое базовое устройство LVM, а уже на его основе создают отдельные снапшоты для файловых систем каждого контейнера. Таким образом, это позволяет экономить дисковое пространство за счет того, что снапшот занимает место только на величину измененных блоков.
Вместо lvm, как вариант, возможно, использование файловой системы поддерживающей снапшоты, например btrfs.
Но у этого метода есть существенный недостаток: дисковые операции на запись с lvm-снапшотами крайне медленные.
Поэтому для определенных задач можно использовать следующий способ:
- Создаем базовый образ контейнера
- Выделяем из него общую неизменяемую часть
- Создаем символические ссылки из образа на эту часть
- При создании контейнера монтируем эту часть внутрь контейнера
Приступим. В качестве базового контейнера будем использовать тот же LVM (хотя это совсем необязательно):
$ mkdir -p /lxc/base
$ mount /dev/mapper/lxc /lxc/base
$ cat /.exclude
/dev/*
/mnt/*
/tmp/*
/proc/*
/sys/*
/usr/src/*
/lxc
$ rsync --exclude-from=/.exclude -avz / /lxc/base/
$ DEV="/lxc/base/dev"
$ mknod -m 666 ${DEV}/null c 1 3
$ mknod -m 666 ${DEV}/zero c 1 5
$ mknod -m 666 ${DEV}/random c 1 8
$ mknod -m 666 ${DEV}/urandom c 1 9
$ mkdir -m 755 ${DEV}/pts
$ mkdir -m 1777 ${DEV}/shm
$ mknod -m 666 ${DEV}/tty c 5 0
$ mknod -m 600 ${DEV}/console c 5 1
$ mknod -m 666 ${DEV}/full c 1 7
$ mknod -m 600 ${DEV}/initctl p
$ mknod -m 666 ${DEV}/ptmx c 5 2
После окончания копирования, приступим к созданию неизменяемой части. Назовем ее common:
$ cd /lxc/base
$ mkdir common
$ mv bin lib lib64 sbin usr common/
$ ln -s common/bin
$ ln -s common/sbin
$ ln -s common/lib
$ ln -s common/lib64
$ ln -s common/usr
$ chroot /lxc/base
$ > /etc/fstab
После этого удаляем start_udev из /etc/rc.sysinit, отключаем ненужные сервисы, и по своему усмотрению проводим дополнительные настройки. Убираем hostname из конфигурационных файлов, чтобы он не переопределялся при старте контейнера.
Смонтируем файловую систему cgroup, с помощью которой будет происходить ограничение ресурсов контейнера. Данный процесс будет происходить путем создания директории с именем контейнера внутри файловой системы. Директорию будут создавать (и удалять) утилиты LXC.
$ mount -t cgroup -o cpuset,memory,cpu,devices,net_cls none /cgroup
Мы явно указываем контроллеры, которые хотим монтировать, поскольку по-умолчанию в дистрибутивах Centos6/RHEL6 монтиоруется контроллер blkio, который не поддерживает вложенные иерархии, необходимые для работы LXC. В Ubuntu/Debian с этим проблем нет.
Также полезной может оказаться утилита cgclear из состава libcgroup, которая не просто отмонтирует контрольные группы, но и уничтожает их на уровне ядра. Это поможет предотвратить ошибку -EBUSY при повторном монтировании отдельных контроллеров.
Теперь создадим сетевой мост, в который будут подключаться все контейнеры. Будьте внимательны, при выполнении операции пропадает сеть.
$ brctl addbr br0
$ brctl addif br0 eth0
$ ifconfig eth0 0.0.0.0
$ ifconfig br0 10.0.0.15 netmask 255.255.255.0
$ route add default gw 10.0.0.1
Все новые виртуальные интерфейсы контейнеров будут включаться в этот новый мост.
Не забудем отразить все изменения в стартовых конфигурационных файлах дистрибутива.
Создаем контейнер LXC
После подготовки базового образа системы мы можем приступить непосредственно к созданию первого контейнера в системе. Назовем его просто lxc-container.
Процедура создания контейнера включает в себя три простых этапа:
- Создание файла fstab контейнера
- Подготовка файловой системы контейнера
- Создание конфигурационного файла контейнера
Настроим fstab для нашего контейнера:
$ cat > /lxc/lxc-container.fstab << EOF
devpts /lxc/lxc-container/dev/pts devpts defaults 0 0
proc /lxc/lxc-container/proc proc defaults 0 0
sysfs /lxc/lxc-container/sys sysfs defaults 0 0
EOF
Теперь подготовим файловую систему для нашего первого контейнера lxc-container, используя ранее созданную неизменяемую часть базового образа.
$ mkdir /lxc/lxc-container && cd /lxc/lxc-container
$ rsync --exclude=/dev/* --exclude=/common/* -avz /lxc/base/ .
$ mount --bind /lxc/base/dev /lxc/lxc-container/dev
$ mount --bind /lxc/base/common /lxc/lxc-container/common
$ mount -o remount,ro /lxc/lxc-container/common
Последние две строчки не получается объединить в одну. Ну и ладно.
Как видно, здесь выявляется главный недостаток (или главное преимущество) описываемого метода. Базовая часть файловой системы внутри контейнера – read-only.
И наконец самое главное – конфигурационный файл контейнера. В указанном примере мы полагаем, что lxc-утилиты установлены в корень системы
$ mkdir -p /var/lib/lxc/lxc-container
$ cat > /var/lib/lxc/lxc-container/config << EOF
# hostname нашего контейнера
lxc.utsname = lxc-name0
# количество псевдо tty
lxc.tty = 2
# пути к файловой системе и fstab
lxc.rootfs = /lxc/lxc-container
lxc.rootfs.mount = /lxc/lxc-container
lxc.mount = /lxc/lxc-container.fstab
# настройки виртуального интерфейса
lxc.network.type = veth
lxc.network.name = eth0
lxc.network.link = br0
lxc.network.flags = up
lxc.network.mtu = 1500
lxc.network.ipv4 = 10.0.0.16/24
# настройка прав доступа к устройствам в /dev
lxc.cgroup.memory.limit_in_bytes = 128M
lxc.cgroup.memory.memsw.limit_in_bytes = 256M
lxc.cgroup.cpuset.cpus =
lxc.cgroup.devices.deny = a
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 4:0 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 5:2 rwm
lxc.cgroup.devices.allow = c 254:0 rwm
EOF
Обратите внимание, мы запретили доступ ко всем устройствам, кроме явно указанных, а также ограничили память и своп. Несмотря на это действующее ограничение утилиты free, top внутри контейнера будут показывать полную физическую память.
Не забудем отразить все изменения в стартовых конфигурационных файлах дистрибутива, иначе они все потеряются после перезагрузки!
Запускаем контейнер LXC
Настало время запустить наш свежесозданный контейнер. Для этого воспользуемся утилитой lxc-start, передав ей в качестве аргумента имя нашего контейнера:
$ lxc-start --name lxc-container
Подключаемся к контейнеру LXC
В LXC существует проблема с прыжком в контейнер из физического сервера.
lxc-attach, предназначенная для этого, работает только с патченным ядром. Патчи реализуют функционал для определенных пространств имен (а именно, mount-namespace и pid-namespace). Сами патчи можно скачать по ссылке.
Функционал прыжка реализуется специальным системным вызовом setns(), который привязывает сторонний процесс к существующим пространствам имен.
Заменить прыжок в контейнер может lxc-console, которая подключается к одной из виртуальных консолей контейнера
$ lxc-console --name lxc-container -t 2
И перед нами консоль контейнера /dev/tty2
CentOS release 6.3 (Final)
Kernel 2.6.32 on an x86_64
lxc-container login: root
Password:
Last login: Fri Nov 23 14:28:43 on tty2
$ hostname
lxc-container
$ tty
/dev/tty2
$ ls -l /dev/tty2
crw--w---- 1 root tty 136, 3 Nov 26 14:25 /dev/tty2
Устройство /dev/tty2 имеет major-номер 136, и не является «настоящим tty». Это устройство обслуживается драйвером псевдотерминала, мастер которого, читается на физическом сервере, а слейв – на контейнере. То есть наш /dev/tty2 является обычным устройством /dev/pts/3
И, конечно, можно подключаться по ssh:
$ ssh root@lxc-container
Эксплуатация LXC
Это очень интересная, но отдельна тема обсуждения. Здесь можно отметить, что часть задач по администрированию контейнеров берут на себя утилиты LXC, но можно вполне обойтись и без них. Так, например, можно посмотреть список процессов в системе с разбиением на контейнеры:
$ ps ax -o pid,command,cgroup
cgroup в данном случае совпадает с именем контейнера
Автор: am83