Работая над более менее сложным проектом, приходится поддерживать окружение необходимое для корректного функционирования системы. Нередко это выливается в ситуацию, когда на машине установлено такое количество пакетов и зависимостей, что возникает масса проблем:
- окружение становится тяжёло воспроизвести на другой машине (например, новому разработчику);
- окружение становится хрупким. Обновляя зависимости для какой-нибудь вспомогательной утилиты, ломается основной проект;
- окружение тяжело поддаётся изменениям. Хочется поэксперементировать с какой-нибудь библиотекой, но из за опасности накосячить с зависимостями, от этой затеи приходится отказаться.
На Java-платформе эти проблемы менее заметны за счёт развитых инструментов управления зависимостей, а также замкнутости платформы. В экосистеме Java не очень жалут C/C++ библиотеки. Все популярные драйвера и библиотеки написаны непосредственно на Java, поэтому не возникает проблем с управлением системными зависимостями. А для управления библиотеками в Java есть масса зрелых инструментов.
На других платформах PHP/Python проблема более актуальна, даже не смотря на наличие таких инструментов как composer
и virtualenv
.
Но в той или иной степени эти проблемы характерны для всех платформ, и разница лишь в том, при каком уровне сложности системы вы начнёте эти проблемы испытывать. Где-то раньше, где-то позже.
Виртуализация отчасти помогает решить проблему. Установив разные приложения в разные виртуальные машины, в изолируете их друг от друга, тем самым избавляя себя от лишних проблем. Разделяй и влавствуй, так сказать. Виртуализация так же позволяет стандартизировать окружение в котором работает приложение. На одной операционной системе проще поддерживать работоспособность приложения, чем на трех.
Что не так с VirtualBox? Link to heading
VirtualBox отличная, полноценная система виртуализации, которую я могу рекомендовать всем, кому она нужна. Но для меня полная аппаратная виртуализация это overkill, так как в подавляющем большинстве случаев я работаю с одной и той же операционной системой, — Linux.
Со временем, я сформулировал свои хотелки:
- хочу иметь возможность быстро создавать новые виртуальные машины для экспериментов. Быстро по времени и количеству команд, которые надо ввести;
- хочу чтобы виртуальные машины быстро загружались (1-2 секунды, не дольше);
- хочу чтобы создание новой виртуальной машины не требовало сотен мегабайт на моём и так не очень большом SSD;
- хочу чтобы у виртуальной машины был низкий memory footprint, чтобы было возможным держать параллельно 5-6 запущенных машин без существенных издержек для host-операционной системы.
Используя VirtualBox’овский differencing image можно вполне адекватно закрыть третий пункт. В остальном же, это решение как из пушки по воробям.
Отчасти Vagrant позволяет решить задачу автоматизация создания новых машин и их первоначальной настройки. Цена за это — отсутствие поддержки differencing images, и как следствие, много занятого места под образы виртуальных машин.
Решение к которому я пришёл Link to heading
Решением для меня стал LXC (Linux Containers). Это система виртуализация уровня ОС, которая позволяет внутри хост операционной системы запустить дочерние ОС, у каждой из которых:
- своя виртуальная память;
- своя файловая система
- свой сетевой стек.
При этом ядро остаётся от операционной системы хоста. Последний аспект для меня имеет 2 важных преимущества:
- молниеносная скорость загрузки и останова (менее 1 секунды);
- отсутствие накладных расходов на обслуживание отдельного для каждой виртуальной машины ядра (в первую очередь, по памяти).
Также LXC умеет работать с BTRFS, которая поддерживает Copy-on-Write. Это позволяет мгновенно скопировать образ базового контейнера, который я использую как шаблон для создания новых, и при этом объем занятого места на SSD не увеличивается.
Таким образом, я закрыл все 4 своих хотелки:
- контейнер создается одной командой, которая выполняется менее секунды;
- загружается контейнер тоже не более секунды;
- за счёт BTRFS на SSD не дублируются общие между контейнерами системные файлы, коих большинство;
- контейнеры сравнительно эффективны по памяти, что позволяет их держать запущенными в фоновом режиме без особых проблем для хоста.
VirtualBox | Vagrant | LXC | |
---|---|---|---|
Время создания (с.) | <1 | ~3 | <1 |
Время запуска (с.) | 3-5 | 3-5 | <1 |
Накладные расходы на VM (Мб.) | ~0 | >100 | ~0 |
Что у меня стоит в контейнерах Link to heading
По большому счёту “разработчищеский шмурдяк”. На данный момент у меня контейнеризированы следующие сервисы:
- MySQL, для тестирования приложений;
- MongoDB, по аналогичной причине;
- Jekyll, который я использую для написания этого блока;
- IPython, который я в последнее время всё чаще испольщую как бесплатный аналог Mathematica;
- несколько проектов с настроенным окружением для прогона интеграционных тестов, над которыми я работаю.
Как настроить LXC? Link to heading
Так как я работаю на Mac OS X, то мне всё равно требуется промежуточная виртуальная машина с Linux’ом, и как следствие, VirtualBox никуда не пропадает. Из двух зол выбирают меньшее, что поделать. Вся схема выглядит следующим образом:
Для упрощения дальнейшего повествования определимся с терминологией:
- хост — Mac OS X, которая установлена непосредственно на ноутбуке;
- VM – аппаратно виртуализированный Ubuntu, который стоит под VirtualBox’ом на Mac OS X;
- контейнер — LXC-контейнер, который стоит на виртуализированном Ubuntu под VirtualBox’ом,
в доме, который построил Джек.
В кратце, процедура подготовки “полигона” для контейнеризации сводится к следующему:
- поставить VirtualBox;
- создать новую виртуальную машину со следующими особенностями:
- два сетевых интерфейса: NAT и Host-only network. Первый нужен для доступа в интернет, второй для того, чтобы на виртуальную машину можно было попасть с хоста;
- два HDD. Первый непосредственно под ОС, второй для хранения образов контейнеров. Второй HDD будет размечен под BTRFS. Теоретически, можно обойтись одним HDD на BTRFS. Проверка работоспособности такой схемы остаётся на совести читателя.
- на виртуальную машину поставить любимый Ubuntu. Я использовал 14.04 Server;
После успешной установки, на Linux необходимо поставить все необходимые пакеты.
# apt-get install bridge-utils btrfs-tools lxc
Подготовка BTRFS тома Link to heading
Для быстрого и бездупликатного клонирования контейнеров нам необходим BTRFS-том. Если вы подключили к VM два жестких диска, то второй будет доступен под именем /dev/sdb
. Создаём на нём файловую систему:
# mkfs.btrfs -m single /dev/sdb
После успешного завершения команды добавляем запись в /etc/fstab
, чтобы том автоматически монтировался в /var/lib/lxc
(директория по-умолчанию с образами LXC-контейнеров) при старте системы.
UUID=476c0e89-f2cb-4660-89b1-f639354ea402 /var/lib/lxc btrfs defaults 0 1
В этом снипете требуется поменять UUID блочного устройства на свой. Узнать его можно командой blkid
:
# blkid /dev/sdb
/dev/sdb: UUID="476c0e89-f2cb-4660-89b1-f639354ea402" UUID_SUB="3c20d5df-4e53-4aea-9749-b1e79e3204aa" TYPE="btrfs"
Вместо UUID=476c0e89-f2cb-4660-89b1-f639354ea402
можно использовать /dev/sdb
. Первый вариант предпочтителен, так как UUID не зависит от порядка регистрации блочных устройств в системе.
Настройка сети на VM Link to heading
Наиболее удобный, на мой взгляд, способ организации сетевого взаимодействия хоста и виртуальных машин для задач разработки следующий. На каждой виртуальной машине и каждом контейнере имеется два интерфейса:
eth0
– который NAT’ится и обеспечивает доступ машины в Интернет;eth1
– который привязан к Host-only сети VirtualBox и обеспечивает связь всех виртуальных машин между собой и с хостом.
Обратите внимание, инструкции в этой статье опираются на то, что интерфейсы сконфигурированны именно с такими именами (eth0
для NAT и eth1
для Host-only Network).
Для того, чтобы контейнеры могли получить доступ к Host-only сети VirtualBox’а, необходимо сконфигурировать сетевой мост поверх eth1
. Делается это в файле /etc/network/interfaces
.
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
#auto eth1
#iface eth1 inet dhcp
auto br1
iface br1 inet dhcp
bridge_ports eth1
bridge_fd 0
bridge_maxwait 0
Обратите внимание, eth1
закоментирован специально.
Создание и настройка базового контейнера Link to heading
Для того чтобы контейнеры можно было создавать быстро, необходимо подготовить базовую копию, которая и будет клонироваться. Делается это командной:
# lxc-create -t download -n base
Системе понадобится некоторе время, чтобы скачать необходимые пакеты из Интернета и подготовить образ базового контейнера.
Теперь необходимо пробросить в LXC интерфейсы, которые мы сконфигурировали на VM. Редактируем /etc/default/lxc-net
:
USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_ADDR="192.168.10.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="192.168.10.0/24"
LXC_DHCP_RANGE="192.168.10.2,192.168.10.254"
LXC_DHCP_MAX="253"
Я выбрал сеть 192.168.10.0/24 произвольно, это решение можно без опаски изменить исходя из вашего контекста.
Необходимо также проверить что оба интерфейса корректно прокинуты в базовый контейнер, а также корректно скофигурированы внутри контейнера. Для этого, во-первых, убедитесь что в файле /var/lib/lxc/base/config
есть следующие строки:
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = lxcbr0
lxc.network.hwaddr = 00:16:3e:13:85:c8
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = br1
lxc.network.hwaddr = 00:16:3e:c0:e6:94
MAC-адреса в вашем случае будут отличаться. Интерфейс lxcbr0
отвечает за доступ к Интернету посредством NAT, а br1
за доступ в Host-only сеть созданную VirtualBox’ом.
Во-вторых, проверьте, что интерфейсы сконфигурированы в файле /var/lib/lxc/base/rootfs/etc/network/interfaces
.
auto lo
iface lo inet loopback
#auto eth0
iface eth0 inet dhcp
auto eth1
iface eth1 inet dhcp
Интерфейс eth0
внутри контейнера у меня отключен, то есть контейнер при загрузке не имеет доступа к Интернету. Причины сугубо лично-параноидальные. Если вам это не подходит, раскоментируйте строку auto eth0
. Я же включаю Интернет когда мне необходимо:
# ifup eth0
Запуск базового контейнера Link to heading
В данный момент контейнер готов к запуску. Для работы с контейнерами используются команды lxc-*
. Первая и самая простая выводит список контейнеров, а также их статус:
# lxc-ls -f
NAME STATE IPV4 IPV6 AUTOSTART
-----------------------------------------------
base STOPPED - - NO
Обратите внимание, что все LXC команды надо выполнять от root’а. Есть возможность работать с контейнерами от непривилегированного пользователя, но это несколько сложнее. Для запуска контейнера достаточно выполнить:
# lxc-start -n base -d
# lxc-attach -n base
Первая команда запустит контейнера, а вторая запустить shell в namespace’е этого контейнера. Вуаяля, вы в контейнере. Я советую всегда передавать ключ -d
в lxc-start
, в противном случае ваш текущий терминал будет привязан к терминалу контейнера и единственный вариант как его можно отвязать, — потушить контейнер.
На данном этапе целесообразно проверить что все системные утилиты, которые вам требуются в повседневной работе (например, curl
, strace
, sar
и прочие), установлены, и если нет, установить их.
Клонирование и запуск дочерних контейнеров Link to heading
Для быстрого создания нового контейнера, необходимо ввести следующую команду:
# lxc-clone -o base -n test -s
Created container test as snapshot of base
Эта команда клонирует контейнер base
под именем test
. Обязательно надо указывать ключ -s
, иначе магия BTRFS не сработает и будет выполняться полное копирование образа контейнера.
Если вы всё правильно настроили контейнер склонируется моментально. На моей машине это занимает десятки миллисекунд!
# time lxc-clone -o base -n test -s
Created container test as snapshot of base
real 0m0.027s
user 0m0.004s
sys 0m0.010s
Подключение к контейнеру и его останов Link to heading
Для подключения к контейнеру можно воспользоваться следующей командой:
# lxc-attach -n test
Эта команда запустит shell по-умолчанию в контейнере. Из этого shell’а вы уже можете делать всё что вам необходимо: устанавливать ПО, настраивать сам контейнер и т.д. Эту же команду можно использовать для запуска произвольных процессов в контейнере:
# lxc-attach -n test -- hostname
test
Ещё одна полезная команда lxc-start-ephemeral
. Она позволяет запустить контейнер, который будет уничтожен как только будет остановлен. Очень удобно её использовать следующим образом:
# lxc-start-ephemeral -o base -- bash
root@base-m58tibcs:~# exit
В данном случае, на основании контейнера base
создаётся новый временный контейнер с произвольным именем. Как только вы выходите из ассоциированного с ним shell’а, — контейнер уничтожается. Для различного рода небольших экспериментов, самое оно.
Доводка окружения Link to heading
Для того, чтобы вся эта схема не просто работала, а работала удобно, я дополнительно устанвливаю Avahi и OpenSSH.
Установка и настройка Avahi Link to heading
Для того, чтобы к машинам можно было обращаться по имени и не заниматься поиском их IP-адерсов, как на виртуальной машине, так и в базовом контейнере ставим Avahi. Это реализация mDNS протокола поддержка которого есть в том числе и в Mac OS X (Bonjour).
# apt-get install avahi-daemon
В файл /etc/avahi/avahi-daemon.conf
в базовос контейнере прописываем:
allow-interfaces=eth1
deny-interfaces=eth0
В тот же файл на виртуальном машине:
allow-interfaces=eth1,br1,eth0,lxcbr0
Так же прописываем в /etc/default/avahi-daemon
на обоих машинах:
AVAHI_DAEMON_DETECT_LOCAL=0
После изменений не забываем перезапускать avahi-daemon
:
# service avahi-daemon restart
Теперь каждая машина будет доступна по своему имени (см. hostname
) в зоне .local
. Например, контейнер с именем foo
будет доступен по имени foo.local
.
Установка и настройка SSH Link to heading
Обычно я использую OpenSSH для администрирования всей этой чехарды. Для этого, на VM и в базовый образ контейнера ставится OpenSSH:
# apt-get install openssh-server
Убеждаемся, что в /etc/ssh/sshd_config
прописано:
UseDNS no
В базовый образ и виртуальную машину добавляем свой публичный ключ, чтобы не надо было постоянно вводить пароль:
$ ssh-copy-id ubuntu@containers.local
$ ssh-copy-id ubuntu@base.local
Это существенно убыстрит процесс авторизации на машине. Утилита ssh-copy-id
, к сожалению, недоступна в базовой поставке Mac OS X. Её можно поставить через brew
. Если у вас нет brew
, вы можете добавить свой публичный ключ вручную.
После этого конфигурирем SSH-клиент на хосте для удобного доступа на виртуальную машину и контейнеры. Для этого в ~/.ssh/config
прописываем:
Host *.local
User ubuntu
PreferredAuthentications publickey
Здесь задаётся имя пользователя по-умолчанию, и предпочитаемый способ аутентификации. В купе с тем что Avahi автоматически сообщает всем доменные имена контейнеров, для того чтобы попасть на контейнер с именем foo
теперь достаточно ввести команду:
$ ssh foo.local
Управление контейнерами с хоста Link to heading
Для управления контейнерами на виртуальной машине доступны команды lxc-*
. Ещё раз приведу краткий их список (полный список вы можете получить набрав в консоли виртуальной машины lxc-
и нажав Tab):
lxc-start
— запуск контейнера;lxc-stop
— останов контейнера;lxc-clone
— клонирование контейнера;lxc-destroy
— удаление контейнера;lxc-attach
— подключение к консоли контейнера/запуск процесса в namespace’е контейнера;lxc-ls
— список контейнеров и их статусы;lxc-start
— запуск контейнера;lxc-wait
— позволяет подождать пока контейнер не перейдёт в определённый статус (например, запустится).
Для выполнения этих команд, необходимо сначала зайти на виртуальную машину с Ubuntu. Это не всегда удобно всегда не удобно. Поэтому, я добавил в ~/.zshrc
(~/.bashrc
для Bash) следующие строки:
CONTAINERS_HOST="containers.local"
function lxc-ls() { ssh -q $CONTAINERS_HOST sudo lxc-ls "$@" }
function lxc-clone() { ssh -q $CONTAINERS_HOST sudo lxc-clone -s "$@" }
function lxc-start() { ssh -q $CONTAINERS_HOST sudo lxc-start -d "$@" }
function lxc-stop() { ssh -q $CONTAINERS_HOST sudo lxc-stop "$@" }
function lxc-destroy() { ssh -q $CONTAINERS_HOST sudo lxc-destroy "$@" }
Теперь 5 указанных команд становятся доступны на хосте.
Послесловие Link to heading
Схема была бы намного проще, если бы моей основной операционной системой был бы Linux. В этом случае, нет необходимости в промежуточном VirtualBox’е и становится возможным запускать в контейнерах GUI приложения. По сумме факторов, Mac OS X для меня работает лучше, как компромиссное решение одновременно рабочих и личных задач. Поэтому, приходится мириться с такой матрёшкой.