Туториал по контейнеризации при помощи LXC

17 февраля 2016

Пришло время научиться работать с Linux Containers, более известными под названием LXC. Далее мы будем рассматривать LXC в контексте Debian и Ubuntu, но написанное во многом будет применимо и для других дистрибутивов Linux. Мы коснемся не только основ использования LXC, но и узнаем, как настроить bridged сеть, ограничить количество ресурсов, используемых контейнером, и даже осилим unprivileged containers.

Коротко о главном

LXC — технология контейнерной виртуализации, как и OpenVZ. Накладные расходы на создание контейнеров очень невелики, и ничто не мешает нам запускать их сотнями или тысячами. Что невозможно при использовании честной и полноценной виртуализации, такой, как KVM. С другой стороны, запустить в контейнере ОС, отличную от Linux, мы не можем. На самом деле, не совсем корректно называть LXC отдельной технологией. Все работает сразу на нескольких фичах ядра Linux. Как минимум, это cgroups и namespaces.

Control Groups, они же cgroups, позволяют организовывать процессы в группы и для каждой группы задавать лимиты. Например, лимиты на использование CPU, объем используемой памяти и дисковый ввод-вывод.

Namespaces бывают разные — PID namespace, IPC namespace, UTS namespace, user namespace, mount namespace, network namespace. Неймспейсы изолируют друг от другая процессы таким образом, что процессы в одной группе не могут видеть ресурсы другой группы. Например, PID namespace предоставляет уникальные идентификаторы процессов в рамках группы. Внутри одной группы может быть процесс с pid’ом 1 и внутри второй группы тоже может быть процесс с pid’ом 1, хотя это два совершенно разных процесса, которые друг о другие ничего не знают. Притом, все процессы все также имеют уникальные id в рамках ОС. Просто, если смотреть на процессы из группы, то эти id отображаются в другие.

В отличие от Docker, который заточен на создание PaaS, в LXC нет никаких слоеных неизменяемых файловых систем. Контейнеры очень похожи на VDS’ы, как и в OpenVZ. Но в отличие от OpenVZ, в LXC далеко не все есть из коробки. Как мы скоро убедимся, для ограничения места на диске, используемого контейнером, приходится прибегать к дополнительным ухищрениям. Повторюсь, связано это с тем, что LXC не совсем одна полноценная технология. С другой стороны, LXC не нуждается в пропатченном ядре. Все необходимое уже и так есть в ванильном ядре Linux. Как следствие, LXC превосходно работает на самой обычной Ubuntu, чем OpenVZ похвастаться не может.

Примечание: Кстати, в заметке Начало работы с Vagrant и зачем он вообще нужен рассказывается, как работать с LXC через Vagrant. Использование LXC через Vagrant кому-то может показаться удобнее, чем работа с LXC напрямую.

Установка

Как было отмечено выше, все необходимое уже есть в ядре. Тем не менее, для хождения в ядро понадобятся некоторые дополнительные утилиты. Также не повредит обновить до последней версии кое-какие, казалось бы, не особо связанные с LXC пакеты.

Итак:

sudo apt-get update
sudo apt-get install lxc lxc-templates systemd-services cgroup-bin \
  bridge-utils debootstrap

Очень важно реально сделать update, чтобы поставились наиболее свежие версии указанных пакетов. И, не поверите, но реально лучше сразу сделать reboot. Иначе, работая с LXC, вы в какой-то момент можете получить странную ошибку вроде такой:

call to cgmanager_create_sync failed: invalid request

… и будете потом через Google выяснять, что же пошло не так.

Свои данные LXC хранит в следующих местах:

  • /var/lib/lxc — тут лежат контейнеры, их настройки, ФС и так далее;
  • /etc/lxc — прочие настройки;
  • /etc/default/lxc-net — тут можно поменять настройки сети;
  • /var/cache/lxc — кэш шаблонов;
  • /var/lib/lxcsnaps — сюда пишутся снапшоты;
  • /var/log/lxc — логи;

Теперь рассмотрим команды для управления контейнерами.

Основные команды

Создание контейнера с именем test-container из шаблона Ubuntu:

sudo lxc-create -n test-container -t ubuntu

Увидим что-то вроде:

##
# The default user is 'ubuntu' with password 'ubuntu'!
# Use the 'sudo' command to run tasks as root in the container.
##

Посмотреть список доступных шаблонов можно так:

ls /usr/share/lxc/templates/

Список скачанных шаблонов:

sudo ls /var/cache/lxc/

Удалить шаблон можно просто удалив соответствующий ему каталог.

Запуск контейнера в бэкграунде:

sudo lxc-start -d -n test-container

Для отладки — запуск с логированием:

sudo lxc-start -l debug -o 1.log -d -n test-container

Список контейнеров:

sudo lxc-ls -f

Подробная информация о контейнере:

sudo lxc-info -n test-container

Заходим в контейнер:

sudo lxc-console -n test-container

В нашем случае логин и пароль — ubuntu. Для выхода из контейнера жмем Ctr+A, затем Q.

Остановка контейнера:

sudo lxc-stop -n test-container

Создание клона контейнера (test-container должен быть остановлен):

sudo lxc-clone -o test-container -n test-container-clone

Удалить контейнер:

sudo lxc-destroy -n test-container

Заморозить/разморозить контейнер:

sudo lxc-freeze -n test-container
sudo lxc-unfreeze -n test-container

Создать снапшот (контейнер должен быть остановлен):

sudo lxc-snapshot -n test-container

Список снапщотов:

sudo lxc-snapshot -n test-container -L

Восстановление из снапшота:

sudo lxc-snapshot -n test-container -r snap0

Если нужно пошарить каталог, проще всего сделать это при помощи sshfs:

sshfs ubuntu@10.110.0.10: ./shared-dir

Пока ничего сложного.

Автозапуск

Чтобы контейнер запускался при старте системы, в конфиг контейнера (в нашем случае это файл /var/lib/lxc/test-container/config) дописываем:

lxc.start.auto  =  1 # enabled
lxc.start.delay = 15 # delay in seconds
lxc.start.order = 50 # higher value means starts earlier

Если все было сделано верно, команда sudo lxc-ls -f покажет YES в колонке autostart.

Ограничение на использование ресурсов

Попробуем ограничить количество памяти, которое может отъедать контейнер.

Останавливаем контейнер:

sudo lxc-stop -n test-container

Затем правим /var/lib/lxc/test-container/config:

lxc.cgroup.memory.limit_in_bytes = 256M

Говорим:

sudo lxc-start -d -n test-container -l debug -o test.log
cat test.log  | grep -i memory

Должны увидеть что-то вроде:

lxc-start 1449237148.829 DEBUG lxc_cgmanager - cgmanager.c:
cgm_setup_limits:1385 - cgroup 'memory.limit_in_bytes' set to '256M'

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

sudo lxc-cgroup -n test-container memory.limit_in_bytes

Можно менять лимиты на лету, но они потеряются с рестартом:

sudo lxc-cgroup -n test-container memory.limit_in_bytes 100M

Также можно следить, сколько ресурвов потребляет контейнер прямо сейчас:

cat /sys/fs/cgroup/memory/lxc/test-container/memory.usage_in_bytes

Вот еще интересные параметры:

  • cpu.shares — сколько единиц процессорного времени отдавать контейнеру, если у одного 2000 единиц, а у второго 1000, второй получит в два раза меньше времени;
  • cpuset.cpus — какие ядра может использовать контейнер, номера начиная с нуля, например 0,1 или 0-3;
  • memory.memsw.limit_in_bytes — ограничение на память и своп в сумме, если своп вообще есть;
  • blkio.weight — как cpu.shared, только про дисковый ввод-вывод;

Тут больше параметров — есть, к примеру, про сеть.

Ограничение на использование дискового пространства

Эту задачу можно решить несколькими способами. Например, многие советуют использовать LVM. И хотя LVM, бесспорно, крутая штука, использовать его для решения такой простой задачи мне лично кажется оверкилом. Предлагаю пойти более простым путем.

Допустим, мы хотим создать контейнер с именем new-container, который будет занимать на диске не более 10 Гб.

sudo truncate -s 10G /var/lib/lxc/new-container.img
sudo mkfs.ext4 -F /var/lib/lxc/new-container.img
sudo mkdir /var/lib/lxc/new-container
sudo mount -t ext4 -o loop /var/lib/lxc/new-container.img \
  /var/lib/lxc/new-container

Теперь создаем контейнер, как обычно:

sudo lxc-create -t ubuntu -n new-container

Заходим внутрь контейнера, и через df видим, что отъесть от диска он может только 10 Гб.

Чтобы не приходилось каждый раз монтировать раздел руками, в /etc/fstab дописываем:

/var/lib/lxc/new-container.img /var/lib/lxc/new-container ext4 loop 0 0

Вы можете заметить, что количество loop-устройств в системе ограничено. В Ubuntu, например, их по умолчанию только 8. Если попытаться создать много контейнеров описанным выше образом, вы получите ошибку:

mount: could not find any free loop device

Эту проблему можно решить следующим скриптом:

#!/bin/sh

for i in $(seq 0 64); do
  [ -b /dev/loop$i ] || (mknod -m 660 /dev/loop$i b 7 $i && \
     chown root:disk /dev/loop$i)
done

Чтобы loop-устройства создавались при загрузке системы, создаем файл /etc/init.d/more-loop-devices следующего содержания:

#!/bin/bash

### BEGIN INIT INFO
# Provides:          more-loop-devices
# Required-Start:    $syslog
# Required-Stop:     $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: More Loop Devices
# Description:       More Loop Devices
### END INIT INFO

PATH=/sbin:/usr/sbin:/bin:/usr/bin

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
. /lib/lsb/init-functions

case "$1" in
  start)
    for i in $(seq 0 64); do
      [ -b /dev/loop$i ] || (mknod -m 660 /dev/loop$i b 7 $i && \
         chown root:disk /dev/loop$i)
    done
    ;;
  *)
    echo "Usage: more-loop-devices start" >&2
    exit 1
    ;;
esac

Говорим:

sudo chmod a+x /etc/init.d/more-loop-devices
sudo update-rc.d more-loop-devices defaults

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

Настройка bridged сети

Ранее в заметке Контейнерная виртуализация при помощи OpenVZ мы научились настраивать bridged сеть под CentOS. Настало время узнать, как то же самое делается в Debian и Ubuntu.

Правим /etc/network/interfaces — часть про eth0 убираем, вместо нее пишем:

auto eth0
iface eth0 inet manual

Далее, если у вас DHCP:

auto br0
iface br0 inet dhcp
        bridge_ports eth0
        bridge_stp off
        bridge_fd 0
        bridge_maxwait 0

Если же у вас используются статические адреса:

auto br0
iface br0 inet static
        address 192.168.0.123
        network 192.168.0.0
        netmask 255.255.255.0
        broadcast 192.168.0.255
        gateway 192.168.0.1
        bridge_ports eth0
        bridge_stp off
        bridge_fd 0
        bridge_maxwait 0

Перезагружаемся.

Если все было сделано правильно, в ifconfig’е увидим интерфейс br0.

Далее останавливаем контейнер и правим /var/lib/lxc/(container)/config:

lxc.network.link = br0

Если хотим присвоить контейнеру статический IP, также дописываем:

lxc.network.ipv4.gateway = 192.168.0.1
lxc.network.hwaddr = 00:16:3e:6b:c7:5b
lxc.network.ipv4 = 192.168.0.124/24

Запускаем контейнер. Если все было сделано правильно, в контейнер можно будет ходить из локалки и sudo lxc-ls -f покажет у него соответствующий IP.

Резервное копирование и восстановление

Как уже отмечалось, все лежит в /var/lib/lxc. Поэтому просто нужно аккуратно заархивировать все данные оттуда. Тут каждый использует, что ему больше нравится. Для примера рассмотрим решение задачи при помощи tar.

Останавливаем контейнер, под рутом делаем:

cd /var/lib/lxc/test-container
tar -cvzf backup.tgz ./

Резервная копия готова! Для восстановления же говорим:

mkdir -p /var/lib/lxc/test-container
cd /var/lib/lxc/test-container
scp example.ru:path/to/backup.tgz ./
tar -xvzf backup.tgz
rm backup.tgz

Теперь контейнер виден в lxc-ls и может быть запущен через lxc-start.

Если описанным образом вы создаете копию контейнера (вам чем-то не подошел lxc-clone) или при восстановлении контейнера решили его переименовать, придется чуть-чуть подправить файл config. Там просто нужно поменять пути и изменить имя контейнера, плюс я бы сменил на всякий случай MAC.

В общем-то, это все. Ну разве что у tar’а еще флаг --numeric-owner рекомендуют использовать.

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

Непривилегированные контейнеры

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

При этом каталоги немного меняются:

  • ~/.local/share/lxc — тут лежат контейнеры, их настройки, ФС и так далее;
  • ~/.config/lxc — прочие настройки;
  • ~/.cache/lxc — кэш шаблонов;
  • ~/.local/share/lxcsnaps — сюда пишутся снапшоты;

Первым делом говорим:

sudo usermod --add-subuids 100000-165536 eax
sudo usermod --add-subgids 100000-165536 eax
sudo chmod +x /home/eax

… где eax — имя вашего пользователя в системе.

Только что мы выделили 65536 uid’ов и gid’ов пользователю eax. В контейнере будут использоваться айдишники от 0 до 65535, которые будут отображаться в айдишники от 100000 до 165535 в хост-системе.

Далее:

mkdir -p ~/.config/lxc

Копируем /etc/lxc/default.conf в ~/.config/lxc/default.conf и дописываем в конце:

lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

В файл /etc/lxc/lxc-usernet пишем:

# USERNAME TYPE BRIDGE COUNT
eax        veth lxcbr0 100

Создаем контейнер (как видите, без sudo):

lxc-create -t download -n test-container -- -d ubuntu \
  -r trusty -a amd64

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

Теперь запускаем контейнер, как обычно:

lxc-start -d -n test-container

В случае возникновения проблем:

lxc-start -d -n test-container --logfile t.log --logpriority debug

Дополнение: Чтобы непривилегированные контейнеры заработали, может потребоваться перезагрузка. Если вы используете encfs, команды sudo и ping могут не работать. Это баг. Здесь рассказывается, как примерно можно его обойти. Идея в том, чтобы при помощи симлинков положить контейнеры и конфиги за пределы encfs.

Шаблоны непривилегированных контейнеров немного отличаются от тех, что мы использовали раньше. В частности, никакого SSH по умолчанию в них не крутится. Но это легко исправить. Заходим в контейнер под рутом при помощи lxc-attach:

lxc-attach -n test-container

… и говорим:

apt-get install ssh
passwd ubuntu

Ну вот, а в остальном все как обычно.

Заключение

Не могу не отметить ряд косяков в LXC:

  • У LXC довольно высокий порог вхождения и вообще все как-то неудобно. Я просто хочу, чтобы контейнер не отжирал больше 1 Гб памяти. Я не хочу ничего знать ни о каких cgroups и читать документацию по ним на сайте RedHat! Про то, как приходится плясать для банального ограничения места на диске, я вообще молчу. OpenVZ в этом плане намного, НАМНОГО лучше;
  • В LXC все очень сыро и плохо документированно. Я собирал представленную в этом посте информацию по крупицам, наверное, около месяца, попутно гугля по поводу багов, на которые я натыкался в процессе. Вам, уважаемые читатели, в этом плане будет намного проще;
  • На мой взгляд, интерфейс продуман плохо. Например, первое время я постоянно забывал флаг -d у lxc-start. Правда, потом я уже как-то то ли привык, то ли запомнил. Да и если контейнеры прописаны на автозагрузку, такой проблемы нет;
  • Внутри контейнера мы видим занятую и свободную память хост-системы, а не контейнера. В результате, из контейнера не ясно, кто же отожрал всю память. К тому же, некоторые программы могут работать из-за этого некорректно;
  • Неизбежны накладные расходы в случае ограничения используемого места на диске;
  • Плохие сообщения об ошибках. Шаг влево, шаг вправо — и тут же получаем что-то в стиле «No such file or directory — execlp»;

Тем не менее, если вы хотите запихнуть что-то в контейнер под Ubuntu, и при этом вы не пишите PaaS’ов, LXC может быть для вас неплохим вариантом. Особенно теперь, когда вы знаете о большинстве подвобных граблей этой технологии.

Больше полезной информации вы найдете на сайте linuxcontainers.org, а также в списке рассылки lxc-users@. Если вас интересует, как запускать GUI-приложения под LXC, обратите внимание на заметку Осилил запуск GUI-приложений в Docker. Она без единого изменения применима и к LXC.

Дополнение: Также вас могут заинтересовать статьи Мой первый опыт использования Proxmox VE и Управление VirtualBox из консоли с помощью vboxmanage.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.