Tux с новой игрушкой
Tux с новой игрушкой

Технологии контейнеризации, возможно, как и у большинства читателей данной статьи, плотно засели в моей голове. И казалось бы, просто пиши Dockerfile и не выпендривайся. Но всегда же хочется узнавать что-то новое и углубляться в уже освоенные темы. По этой причине я решил разобратьсяв реализации контейнеров в ОС на базе ядра linux и в последствие создать свой «контейнер» через cmd.

На ком держатся контейнеры в Linux?

На страже
На страже

Для начала необходимо понять на чем именно основана технология контейнеризации. В ядре Linux существуют два механизма: namespace (пространство имен), cgroups (control groups). Они обеспечивают изоляцию и масштабируемость, за которые мы все так любим контейнеры. Давайте разберем по порядку оба механизма.

Namespace

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

Рассмотрим контейнер поднятый из образа alpine. Запустим его и интерактивную оболочку в нем:

docker run -it alpine /bin/sh

Теперь создадим новый процесс в контейнере, и проверим вывод команды ps:

sleep 1000 &
ps -a

Получаем:

PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   29 root      0:00 sleep 1000
   30 root      0:00 ps -a

Обратите внимание, что PID процесса равен 29. Теперь попробуем найти этот же процесс, но на хостовой машине. Для этого определим ID контейнера и воспользуемся командой для отображения процессов, запущенных внутри docker:

docker top <ID контейнера>

В результате получаем:

UID     PID       PPID      C    STIME    TTY      TIME        CMD
root    172147    172124    0    Feb05    pts/0    00:00:00    /bin/sh
root    173602    172147    0    Feb05    pts/0    00:00:00    sleep 1000

Обратим внимание на 2 столбца: PID и PPID(parent PID). Они указывают на PID самого процесса и родительского, но уже в хостовой системе. Давайте проверим это:

ps aux | grep -E '173602|172147'

Получаем:

root      172147  0.0  0.0   1736   908 pts/0    Ss+  Feb05   0:00 /bin/sh
root      173602  0.0  0.0   1624   980 pts/0    S    Feb05   0:00 sleep 1000

Что и требовалось доказать! Если подытожить, то можно сделать вывод, что контейнер ничего не знает про хостовую машину. Он считает, что является самостоятельной системой. Однако, в действительности все процессы запущены на хосте, просто они находятся в пространстве имен данного контейнера. Это и создает иллюзию отдельной независимой системы.

Надеюсь, данный пример чуть прояснил ситуацию с namespace. В нем мы разобрали один из 8 видов пространств имен. Теперь хотелось бы кратко пройтись по каждому:

  1. Mount - изоляция точек монтирования файловой системы. Позволяет установить свою иерархию фс;

  2. UTS - изоляция имени хоста. Позволяет для каждого контейнера указать свое хостовое имя;

  3. PID - изоляция идентификаторов процессов. Позволяет создавать отдельное дерево процессов;

  4. Network - изоляция сетевых интерфейсов, таблиц маршрутизации;

  5. IPC - изоляция IPC(межпроцессные взаимодействия);

  6. User - изоляция пользователей системы. Позволяет создавать отдельных пользователей для каждого контейнера, в том числе и root;

  7. Cgroup - изоляция доступа к cgroup. Позволяет ограничивать ресурсы контейнера и предотвращает вмешательства других контейнеров;

  8. Time - изоляция системного времени.

Для создания нового namespace в Linux существует команда unshare. С ней мы чуть позже познакомимся ближе.

Cgroups

Control groups - механизм ядра Linux, позволяющий управлять ресурсами процессов. С его помощью можно ограничить и изолировать использование CPU, памяти, сети, диска.

Существует две версии cgoups: v1 и v2. В большинстве современных систем вы встретите вторую версию, которая используется в работе systemd. Основное отличие версий в построении дерева ограничений. В первой версии создавались узлы для каждого вида ограничений, а в них уже добавлялись группы. Во второй версии для каждой группы свой узел, внутри которого все необходимые ограничения. Чтобы лучше понять, давайте взглянем на визуализацию деревьев v1 и v2:

#v1
/sys/fs/cgroup/
├── cpu
│   ├── group1/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── cpu.shares
│   │   └── ...
│   ├── group2/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── cpu.shares
│   │   └── ...
│   └── ...
├── memory
│   ├── group1/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── memory.limit_in_bytes
│   │   └── ...
│   ├── group2/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── memory.limit_in_bytes
│   │   └── ...
│   └── ...
└── ...

#v2
/sys/fs/cgroup/
├── group1/
│   ├── cgroup.procs
│   ├── cpu.max
│   ├── cpu.weight
│   ├── memory.current
│   ├── memory.max
│   └── ...
├── group2/
│   ├── cgroup.procs
│   ├── cpu.max
│   ├── cpu.weight
│   ├── memory.current
│   ├── memory.max
│   └── ...
└── ...

Теперь давайте взглянем на работу cgroup на примере контейнера docker. Для начала запустим контейнер, ограничив его ресурсы(2 ядра и 512МБ):

docker run -d --cpus="2" --memory="512m" nginx 

Далее найдем группу для этого контейнера, воспользовавшись командой find :

find /sys/fs/cgroup -name '*<ID контейнера>*'

Далее проверим содержание файлов cpu.max и memory.max в найденной директории:

# cpu.max
200000 100000

# memory.max
536870912

Что и требовалось доказать!

Создание контейнера без docker

Tux волшебник?!
Tux волшебник?!

Мы разобрались с основной необходимой нам теорией. Теперь перейдем к практике и прибегнем к волшебству командной строки.

Для начала создадим структуру файловой системы контейнера, установим busybox в директорию /bin :

# Создаем корневую дирекотрию контейнера и переходим в нее
mkdir ~/container && cd ~/container
# Создаем основные системные директории и переходим в /bin
mkdir -p ./{proc,sys,dev,tmp,bin,root,etc} && cd bin
# Устанавливаем busybox
wget https://www.busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
# Выдаем право на исполнение
chmod +x busybox
# Создаем симлинки для всех команд, которые есть в busybox 
./busybox --list | xargs -I {} ln -s busybox {}
# Возвращаемся в корневую директорию контейнера
cd ~/container
# Добавляем переменную PATH в файл /etc/profile
echo 'export PATH=/bin' > ~/container/etc/profile

Также добавим в файлы /etc/passwd и /etc/group, чтобы внутри изолированной системы мы были рутом:

echo "root:x:0:0:root:/root:/bin/sh" > ~/container/etc/passwd
echo "root:x:0:" > ~/container/etc/group

Далее смонтируем системные директории:

# Монтируем устройства, используя уже существующие
sudo mount --bind /dev ~/container/dev
# Монтируем процессы
sudo mount -t proc none ~/container/proc
# Монтируем файловую систему sysfs
sudo mount -t sysfs none ~/container/sys
# Монтируем файловую систему tmpfs
sudo mount -t tmpfs none ~/container/tmp

Примечание: для того чтобы потом размонтировать можно воспользоваться командой:

sudo umount ~/container/{proc,sys,dev,tmp}  

Мы подготовили файловую систему для нашего контейнера. Теперь перейдем к созданию изоляции. Для этого мы воспользуемся командой:

unshare -f -p -m -n -i -u -U --map-root-user --mount-proc=./proc \
    /bin/chroot ~/container /bin/sh -c "source /etc/profile && exec /bin/sh"

Давайте разберем ее подробнее:

  • -f - fork. Создаем новый процесс для изоляции от родительского;

  • -p - PID namespace;

  • -m - mount namespace;

  • -n - Network namespace;

  • -i - IPC namespace;

  • -u - UTS namespace;

  • -U - User namespace;

  • --map-root-user - маппинг uid и gid активного пользователя на root внутри контейнера;

  • -mount-proc - монтируем proc внутри контейнера;

  • /bin/chroot ~/container - меняем корневую директорию;

  • /bin/sh -c "source /etc/profile && exec /bin/sh" - запускаем shell и исполняем команду, которая применит файл /etc/profile и запустит интерактивный shell.

Отлично! Мы получили свой контейнер. Теперь осталось ограничить ресурсы. Для этого откроем новую сессию на хосте и выполним ряд действий:

# Создаем новую группу. В моей системе используется cgroups v2, поэтому
# директория автоматически будет настроена для работы с ресурсами
sudo mkdir /sys/fs/cgroup/my_container
# Записываем ограничение на 2 ядра процессора
echo "200000 100000" | sudo tee /sys/fs/cgroup/my_container/cpu.max
# Выделяем максимум 512MB памяти
echo 536870912 | sudo tee /sys/fs/cgroup/my_container/memory.max

Далее необходимо определить PID контейнера, для этого воспользуемся командой:

ps aux | grep -E '/bin/sh$' 

Берем PID из второго столбца и добавляем в файл cgroup.procs :

echo <PID> | sudo tee /sys/fs/cgroup/my_container/cgroup.procs

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

# Создаем пару виртуальных интерфейсов
sudo ip link add veth-host type veth peer name veth-container
# Поднимаем интерфейс на хосте
sudo ip link set veth-host up
# Назанчаем любой свободный адрес в вашей сети на интерфейс хоста
# Я использую 192.168.1.123/24
sudo ip addr add 192.168.1.123/24 dev veth-host
# Перемещаем veth-container в пространство имен контейнера
# Здесь нужно указать PID контейнера, который использовали до этого
sudo ip link set veth-container netns <PID>
# Поднимаем интерфейс внутри контейнер
sudo nsenter --net=/proc/<PID>/ns/net ip link set veth-container up
# Назанчаем любой свободный адрес в вашей сети на интерфейс контейнера
# Я использую 192.168.1.124/24
sudo nsenter --net=/proc/<PID>/ns/net ip addr add 192.168.1.124/24 dev veth-container
# Настраиваем шлюз по умолчанию для маршрутизации трафика
sudo nsenter --net=/proc/<PID>/ns/net ip route add default via 192.168.1.123

Мы подняли все нужные интерфейсы. Теперь необходимо настроить маршрутизацию:

# Разрешаем пересылку пакетов
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# Добавляем правило NAT для маскардинга для исходящих пакетов из сети 
# 192.168.1.0/24 через интерфейс который смотрит во внешнюю сеть. У меня это enp3s0.
# Маскардинг маскирует пакеты исходящие их контейнера так, чтобы они выглядели,
# как пакеты отправленные с хоста
sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o enp3s0 -j MASQUERADE
# Добавляем парвило на разрешение форвардинга пакетов
sudo iptables -A FORWARD -s 192.168.1.0/24 -o enp3s0 -j ACCEPT
# Добавляем парвило, разрешающее входящие пакеты.
sudo iptables -A FORWARD -d 192.168.1.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT

Отлично! Мы создали свой первый контейнер. Понятно, что в нем еще много чего можно настроить, тот же DNS, который сейчас не работает. Но это уже каждый сам решит, как с этим играть.

Спасибо за прочтение!

Комментарии (12)


  1. Ascard
    11.02.2025 13:42

    Было бы неплохо ещё в конец статьи добавить как это дело всё прибить если что-то внутри пойдёт не так. И как удалить, не снеся случайно половину хостовой системы. Докер как-то это сам делает, а тут есть риск появления упсь!-моментов у начинающих контейнероводов.
    А за статью спасибо, небезынтересно было узнать как оно работает.


    1. lantonov
      11.02.2025 13:42

      Согласен как-то с докером спокойней это делать тан как в докер есть готовые файловые системы и методы удаления и создания контейнера есть ещё более взрослый вариант то lxd



  1. vikarti
    11.02.2025 13:42

    Интересно а кто нибудь еще помнит что такое Virtuozzo и позднее - OpenVZ и какое это отношение имеет с тем же cgroups/namespaces ?


    1. datacompboy
      11.02.2025 13:42

      Такое же как и linux-vservers. Прородители :)


    1. Tsimur_S
      11.02.2025 13:42

      Туда же freebsd jail и solaris zones. ЕМНИП это еще прошлый век был.


    1. Self_Perfection
      11.02.2025 13:42

      Ну и про LXC и lxd/incus не забываем


  1. mnnoee
    11.02.2025 13:42

    Жаль что мало кто уделяет всему этому внимание, но кхе кхе. Есть отличный проект для этих нужд. Rurima. Скм и пользуюсь, может что-то можно будет подметить для себя в качестве запуска докера без докера.


  1. Stanislavvv
    11.02.2025 13:42

    Что-то никто не вспоминает https://github.com/p8952/bocker — Docker implemented in around 100 lines of bash.


    1. datacompboy
      11.02.2025 13:42

      1. Stanislavvv
        11.02.2025 13:42

        Кластер, однако. На момент отправки ещё не видел.


  1. domix32
    11.02.2025 13:42

    Меня интересует несколько иной вопрос - вот у нас есть образа с alpine, void, arch, ubuntu - как они выглядят с точки зрения докера и исполняются в контексте конкретных дистров, а не как кусок хостовой системы.