Когда вы вводите в командную строку docker run nginx — кажется, что произошло какое-то волшебство: за считанные секунды появляется полностью изолированная среда. Но здесь нет никакой магии, а просто инженерия ядра Linux. Давайте подробнее разберём эту тему подробнее и изучим, что именно происходит внутри ядра, когда Docker создаёт контейнер.
❯ Иллюзия контейнера: это не виртуальная машина, а умно изолированный процесс
Для начала давайте устраним одно большое недоразумение: на самом деле, контейнеры — это никакие не виртуальные машины. Запуская контейнер, вы не поднимаете целую новую операционную систему. На самом деле вот что происходит: вы просто запускаете у вас на хосте обычный процесс Linux, но, с применением умно организованной изоляции. Изоляция среды достигается благодаря двум ключевым фичам ядра: это пространства имён и контрольные группы (cgroups).
Контейнеры по-настоящему сильны в том, каким образом «заставляют» отдельно взятый процесс «думать», что он работает на полноценной собственной системе. На самом же деле, этот процесс использует ресурсы ядра совместно со всеми другими процессами, работающими на этом хосте.
❯ Как Docker развился от команды до контейнера
Когда вы выполняете docker run, вот какая цепочка событий происходит на уровне ядра
1. Клиент Docker обращается к демону
Когда вы вводите команду docker run, она подключается к демону Docker (dockerd) через REST API. Можно считать, что именно демон отвечает за управление всеми операциями, необходимыми для координации процессов, но сам никакой тяжеловесной работы не выполняет.
2. Containerd принимает на себя управление
Далее dockerd передаёт задачу демону containerd, который является в контейнере более высокоуровневой средой выполнения. Демон containerd занимается общими вопросами жизненного цикла контейнера — например, подтягиванием образов, пуском и остановкой контейнеров, но саму задачу создания процесса контейнера передаёт более низкоуровневому инструменту.
3. runc создаёт изолированный процесс
Вот как начинается реальная магия в ядре. Демон containerd вызывает runc — это низкоуровневая среда выполнения вида OCI (Open Container Initiative), которая общается напрямую с ядром Linux. Именно на данном этапе настраивается изолированная среда и, собственно, допускается процесс контейнера.

runc делает особый системный вызов clone(), совершенно не похожий на обычный fork(). Когда ядро применяет вызов clone(), тем самым оно создаёт новый процесс. Но флаги CLONE_NEW* сообщают: «Эй, этот процесс нужно создавать в совершенно новых пространствах имён».
4. Ядро собирает границы пространства имён
Каждый флаг пространства имён задаёт свой особый слой изоляции, благодаря которому из контейнера получается маленькая «вещь в себе»:
Пространство PID (CLONE_NEWPID): внутри контейнера первый процесс фигурирует как PID 1, пусть даже на самом деле он может находиться на хосте с идентификатором PID 15234. Выполнив внутри контейнера команду
ps, вы увидите лишь те процессы, которые относятся именно к этому пространству имён.

Сетевое пространство имён (CLONE_NEWNET): контейнер приказывает своей сети настроить отдельные интерфейсы, таблицы маршрутизации и правила брандмауэра. Вот почему контейнер может слушать порт 80, не конфликтуя при этом ни с одним из других процессов, работающих на данном хосте.
Пространство имен монтирования (CLONE_NEWNS): контейнер видит компоновку каталогов, которые воспринимает как собственную файловую систему. Он не видит / хоста, а видит только те каталоги, которые были специально смонтированы именно для него.
Пространство имён UNIX с разделением времени (CLONE_NEWUTS): позволяет контейнеру обзавестись собственным хост-именем, совершенно не зависящим от хост-машины.
Пространство имён межпроцессного взаимодействия (CLONE_NEWIPC): изолируются семафоры и разделяемая память, поэтому процессы, работающие внутри контейнера, не могут помешать процессам, работающим вне его.
Пользовательское пространство имён (CLONE_NEWUSER): позволяет соотносить пользовательские и групповые ID, действующие внутри контейнера, иначе, чем это делается внутри контейнера. Поэтому рут (процесс с правами администратора) внутри контейнера не обязательно совпадает с рутом вне контейнера.
Пространство имён Cgroup (CLONE_NEWCGROUP): контейнер видит только собственный сегмент иерархии
cgroup, а не иерархию всей системы.

В результате такого вызова clone() у вас получается п��оцесс, который технически существует на хосте, но действует внутри собственного изолированного пузыря пространств имён.
5. Процесс выполняет команду вашего контейнера
Наконец, runc при помощи вызова exec() подставляет на место замещающего процесса ту команду контейнера, которая вам сейчас требуется — например, nginx. Теперь при отсчёте внутри контейнера этот процесс становится PID 1, то есть, «init» своего крошечного мира.
❯ Контрольные группы: ресурсная полиция
На уровне пространств имён обрабатывается, что именно видит контейнер, но нужны другие механизмы, которые бы регулировали, сколько ресурсов может использовать контейнер. Здесь в дело вступают cgroups (контрольные группы).
Если устанавливать лимиты память, например, memory=256m, cpus=0.5, то Docker обновит конфигурационные файлы в каталоге /sys/fs/cgroup/. Эти контрольные группы указывают ядру, сколько именно процессорного времени, памяти и других ресурсов контейнеру разрешено израсходовать, чтобы распределение оставалось честным, и ни один одиночный контейнер не мог потребить всю мощность системы.

Ядро «за кулисами» автоматически обязывает систему выдерживать эти лимиты ресурсов.
Тротлинг ЦП: если ваш контейнер израсходует процессорное время сверх выделенной ему квоты, то планировщик ядра приостановит его работу до начала следующей порции времени, таким образом, принудительно оставив его почти в пределах выделенной доли.
Ограничения памяти: если контейнер попытается израсходовать память сверх выделенного ему лимита, например, более 256 МБ, то в дело вступит имеющийся в ядре OOM-киллер (OOM означает «Out of Memory», «нехватка памяти»). Он завершит один или более процессов, чтобы освободить пространство.
Тротлинг ввода/вывода: при помощи контроллера ввода/вывода ядро может ограничивать скорость, с которой контейнер считывает информацию с диска или пишет информацию на диск.
При желании можете наблюдать, как это происходит в режиме реального времени: просто запустите процесс, сильно нагружающий ЦП, причём, сделайте этого в контейнере со строгими лимитами. Статистика docker покажет, что контейнер израсходовал всё выделенное ему процессорное время, хотя, на самом хосте этого ресурса будет ещё очень много. Здесь я просто хочу задавить контейнер, который ограничил 50 процентами ЦП и 256 МБ памяти.

❯ Файловая система OverlayFS: слои на слоях
В контейнерах используется объединяющая файловая система OverlayFS, благодаря которой кажется, что каждый контейнер обладает полной файловой системой, пусть на самом деле она и состоит из множества слоёв.
Когда вы запускаете контейнер из образа:
Слои образа (только для чтения): они сложены в каталоге
/var/lib/docker/overlay2/и используются совместно всеми контейнерами, которые работают с этим образом. Такое совместное использование позволяет значительно экономить дисковое пространство.Слой контейнера (для чтения и для записи): поверх слоёв, предназначенных только для чтения, Docker добавляет слой с возможностью записи, выделенный для данного конкретного контейнера. Любые вносимые в него изменения, например, редактирование
/etc/nginx/nginx.conf, запускают копирование при записи: файл копируется из предназначенного только для чтения слоя образа в слой, доступный для записи, и все операции редактирования происходят уже там.
Если изменить /etc/nginx/nginx.conf внутри контейнера, то OverlayFS выполняет копирование при записи: копирует файл из нижележащего слоя, предназначенного только для чтения, в расположенный выше слой, доступный для записи, и именно там вносит изменения.

❯ Почему отличаются операции «остановить контейнер» и «удалить контейнер»
Остановка контейнера (docker stop)
Процесс контейнера получает сигнал SIGTERM, а затем SIGKILL, если не удалось его аккуратно остановить.
Пространства имён уничтожаются (они существуют, только пока выполняются процессы).
Слой, доступный для записи, остаётся на диске по адресу
/var/lib/docker/overlay2/<container-id>/.Ограничения контрольных групп удаляются.
При перезапуске контейнера командой docker start:
Пространства имён воссоздаются.
Повторно монтируется вышеупомянутый слой, доступный для записи.
Процесс снова работает.
Результат: изменения, сделанные вами внутри контейнера, сохраняются на протяжении цикла остановки/перезапуска.

Как видите, после остановки контейнера наблюдаем файл с данными.
Удаление контейнера (docker rm)
Слой, доступный для записи, удаляется из
/var/lib/docker/overlay2/.Теряются все изменения, сделанные в период жизни контейнера.
Слои образа при этом остаются незатронутыми, их продолжают совместно использовать другие контейнеры.
Результат: исчезают только те изменения, которые касаются именно удалённого контейнера. Исходный образ остаётся нетронутым.
Вот почему существующие тома — это каталоги, монтированные командой bind с хоста. Это делается путём обхода OverlayFS, поэтому изменения сохраняются, независимо от того, что происходит с контейнером.
❯ Красота разделяемых ядер
Эксплуатируете 100 контейнеров? Все они совместно используют одно и то же ядро, не несут издержек, связанных с использованием гипервизора, а также запускаются почти мгновенно всего двумя командами: clone() + exec().
Замечание о безопасности: при совместном использовании ядро становится уязвимым, и эта уязвимость затрагивает все контейнеры. Ситуация с виртуальными машинами другая — у них ядра изолированные.
❯ Заключение: Контейнеры — это просто возможности ядра
Контейнеры — это не изобретение Docker, а просто механизм для умного использования возможностей ядра Linux, которые существовали задолго до Docker:
Пространства имён (2002–2013)
Контрольные группы (2006)
OverlayFS (2014)
Docker гениален тем, что все эти инструменты в нём удалось упаковать в простой и легко воспроизводимый рабочий поток. При выполнении команды docker run:
Ядро клонирует процесс, придавая ему новые пространства имён.
Присваивает процесс ограниченным контрольным группам.
Монтирует многослойную файловую систему.
Выполняет вашу команду.
«Иллюзия» контейнера возникает потому, что ядро загоняет в нужные рамки обычный процесс. Это лёгкое, быстрое и элегантное решение, демонстрирующее, что самые лучшие абстракции выстраиваются на прочном фундаменте.
Когда в следующий раз будете поднимать контейнер Docker – помните: в этом нет никакого волшебства, есть только максимально рафинированная инженерия Linux.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

