- Представление файловой системы в контейнеризированных процессах
- Роль пространства имён Mount Namespace
- Контейнерные образы и rootfs
- Общее ядро и зависимости приложений в контейнерах
- Понятие слоёв в контейнерных образах
- Слои в контейнерах
- Преимущества слоистого дизайна в контейнерных образах
- Влияние контейнерных образов на рабочие процессы разработки ПО
Представление файловой системы в контейнеризированных процессах
Как выглядит файловая система для процессов, запущенных внутри контейнера? Можно сразу подумать, что это связано с пространством имён Mount Namespace — процессы внутри контейнера должны видеть полностью независимую файловую систему. Таким образом, можно выполнять операции в каталогах контейнера, например /tmp, не оказывая никакого влияния на хост-машину или другие контейнеры. Так ли это на самом деле?
Роль пространства имён Mount Namespace
Mount Namespace работает несколько иначе, чем другие пространства имён: его влияние на представление файловой системы процесса в контейнере проявляется только при выполнении операции монтирования. Однако, как обычные пользователи, мы хотим более удобного сценария: при каждом создании нового контейнера мы хотим, чтобы процесс контейнера видел файловую систему, которая является изолированным окружением, а не унаследованной от хоста. Как этого добиться?Нетрудно догадаться, что мы можем перемонтировать весь корневой каталог “/” перед запуском процесса контейнера. Благодаря Mount Namespace это монтирование будет невидимо для хоста, что позволит процессу контейнера свободно работать внутри. В операционной системе Linux есть команда chroot, которая удобно выполняет эту задачу в оболочке. Как следует из названия, она помогает “изменить корневую файловую систему”, перенаправляя корневой каталог процесса в указанное место.
Контейнерные образы и rootfs
На самом деле Mount Namespace был изобретён на основе постоянных улучшений chroot и является первым Namespace в Linux. Чтобы этот корневой каталог казался более “реальным” для контейнеров, мы обычно монтируем полную файловую систему операционной системы под этим корнем, например ISO-образ Ubuntu 16.04. Следовательно, после запуска контейнера выполнение “ls /” внутри контейнера покажет все каталоги и файлы Ubuntu 16.04. Эта файловая система, смонтированная в корневой каталог контейнера и обеспечивающая изолированное окружение выполнения для процессов контейнера, называется “контейнерным образом”. У неё также есть более технический термин: rootfs (корневая файловая система).Исполняемый файл /bin/bash, запускаемый после входа в контейнер, — это исполняемый файл из каталога /bin, полностью отличный от /bin/bash на хосте. Теперь вы должны понимать, что основная суть проектов Docker, по сути, заключается в настройке Linux Namespace для создаваемого пользовательского процесса, установке определённых параметров Cgroups и изменении корневого каталога (Change Root) процесса. Таким образом рождается полноценный контейнер.
Общее ядро и зависимости приложений в контейнерах
Однако Docker предпочитает использовать системный вызов pivot_root для последнего шага переключения, возвращаясь к chroot, если система не поддерживает pivot_root. Хотя эти два системных вызова имеют схожие функции, между ними есть тонкие различия. Более того, важно уточнить, что rootfs включает только файлы, конфигурации и каталоги операционной системы, но не ядро. В Linux эти две части хранятся отдельно, и образ ядра конкретной версии загружается только при загрузке системы. Таким образом, rootfs включает только “оболочку” операционной системы, а не её “душу”.Итак, где же “душа” операционной системы для контейнеров? На самом деле все контейнеры на одной машине используют ядро хост-системы. Это означает, что если вашему приложению нужно настроить параметры ядра, загрузить дополнительные модули ядра или напрямую взаимодействовать с ядром, следует помнить, что эти операции и зависимости относятся к ядру хост-системы, которое является “глобальной переменной” для всех контейнеров на этой машине. Это один из главных недостатков контейнеров по сравнению с виртуальными машинами: последние не только имеют смоделированное оборудование в качестве песочницы, но и запускают полноценную гостевую ОС в каждой песочнице для использования приложениями. Тем не менее, благодаря rootfs, контейнеры обладают важной особенностью, которая широко рекламируется: согласованность.
Понятие слоёв в контейнерных образах
В чём заключается “согласованность” контейнеров? Из-за различий между облачной и локальной серверной средой процесс упаковки приложений всегда был одним из самых “болезненных” этапов при использовании PaaS. Однако с помощью контейнеров, точнее, контейнерных образов (т.е. rootfs), эта проблема была элегантно решена.Поскольку rootfs упаковывает не только приложение, но и всю файловую систему и каталоги операционной системы, он инкапсулирует все зависимости, необходимые для работы приложения. Фактически, для большинства разработчиков их понимание зависимостей приложений ограничивалось уровнем языка программирования, например Godeps.json в Golang. Однако давно упускался из виду тот факт, что для приложения сама операционная система является наиболее полной “библиотекой зависимостей”, необходимой для его работы.Благодаря способности контейнерных образов “упаковывать операционную систему”, эта фундаментальная среда зависимостей наконец становится частью песочницы приложения. Это даёт контейнерам их разрекламированную согласованность: независимо от того, где это происходит — на локальной машине, в облаке или где-либо ещё, пользователям нужно только распаковать контейнерный образ, чтобы воссоздать полное окружение выполнения, необходимое для работы приложения.Инкрементальный дизайн слоёв в контейнерных образахЭта согласованность на уровне операционной системы устраняет разрыв между локальной разработкой и удалённой средой выполнения приложений. Однако вы могли заметить ещё одну сложную проблему: нужно ли каждый раз создавать rootfs заново при разработке нового приложения или обновлении существующего? Интуитивное решение — сохранять rootfs после каждой “значимой” операции при его создании, чтобы коллеги могли использовать нужный им rootfs.Однако это решение не масштабируется. Причина в том, что как только коллеги изменяют этот rootfs, между старым и новым rootfs не остаётся связи, что приводит к крайней фрагментации. Поскольку эти изменения основаны на старом rootfs, можно ли сделать эти изменения инкрементальными? Преимущество такого подхода в том, что каждый поддерживает только инкрементальное содержимое относительно базового rootfs, а не создаёт “форк” при каждом изменении.Ответ, конечно, да. Именно поэтому Docker не пошёл по стандартному процессу создания rootfs при реализации Docker-образов, а сделал небольшое нововведение: Docker ввёл концепцию слоёв в дизайн своих образов. Каждая операция, выполняемая пользователем для создания образа, порождает слой, который является инкрементальным rootfs. Эта идея не возникла на пустом месте, а использовала возможность, называемую Объединённой файловой системой (Union File System, UnionFS), основная функция которой — объединённое монтирование (union mount) нескольких каталогов из разных мест в один каталог.
Слои в контейнерах

Слои в контейнерах
Часть 1: Слои только для чтения. Это нижние пять слоёв rootfs данного контейнера, соответствующие пяти слоям образа ubuntu:latest. Они смонтированы как только для чтения (ro+wh, т.е. readonly+whiteout). Каждый слой инкрементально содержит часть операционной системы Ubuntu.
Часть 2: Слой для чтения и записи. Это верхний слой rootfs данного контейнера (6e3be5d2ecccae7cc), смонтированный как rw, то есть чтение-запись. До записи любых файлов этот каталог пуст. Как только внутри контейнера выполняется операция записи, изменения появляются инкрементально в этом слое. Но задумывались ли вы, что произойдёт, если нужно удалить файл из слоя только для чтения? Для этого AuFS создаёт в слое чтения-записи файл whiteout, чтобы “скрыть” файл в слое только для чтения. Например, удаление файла с именем foo из слоя только для чтения фактически создаёт файл с именем .wh.foo в слое чтения-записи. Таким образом, при объединённом монтировании этих слоёв файл foo скрывается файлом .wh.foo и “исчезает”. Это функциональность, которую обозначает метод монтирования “ro+wh”, т.е. только чтение плюс whiteout.
Часть 3: Init-слой. Это внутренний слой, генерируемый проектом Docker, заканчивающийся на “-init”, расположенный между слоями только для чтения и чтения-записи. Init-слой специально используется для хранения информации, такой как /etc/hosts и /etc/resolv.conf. Необходимость в таком слое возникает потому, что эти файлы изначально принадлежат образу Ubuntu только для чтения, но часто требуют определённых значений, таких как имя хоста, которые должны быть записаны при запуске контейнера. Следовательно, требуются изменения в слое чтения-записи. Однако эти изменения обычно относятся только к текущему контейнеру и не должны включаться в слой чтения-записи при выполнении docker commit. Поэтому подход Docker заключается в монтировании этих изменённых файлов в отдельном слое. Когда пользователь выполняет docker commit, фиксируется только слой чтения-записи, исключая это содержимое.
Преимущества слоистого дизайна в контейнерных образах
Благодаря дизайну “слоистого образа”, в центре которого находятся Docker-образы, специалисты из разных компаний и команд оказываются тесно связаны. Более того, поскольку операции с контейнерными образами являются инкрементальными, содержимое, загружаемое или отправляемое каждый раз, намного меньше, чем несколько полных операционных систем; общие слои означают, что общее пространство, необходимое для всех этих контейнерных образов, меньше суммы каждого образа по отдельности. Такая гибкость совместной работы на основе контейнерных образов намного превосходит образы дисков виртуальных машин, которые могут иметь размер в несколько гигабайт.Что более важно, после публикации образа загрузка его в любой точке мира даёт точно такое же содержимое, полностью воспроизводя исходную среду, созданную создателем образа.
Влияние контейнерных образов на рабочие процессы разработки ПО
Изобретение контейнерных образов не только соединяет каждый шаг процесса “разработка — тестирование — развёртывание”, но и означает, что контейнерные образы станут основным методом распространения программного обеспечения в будущем. Этот метод распространения обладает такими преимуществами, как лёгкость, высокая согласованность и удобство совместной работы, что делает разработку и развёртывание программного обеспечения более эффективными и надёжными. Благодаря своей лёгкости, согласованности и эффективности контейнерная технология всё чаще становится незаменимым инструментом в разработке и эксплуатации программного обеспечения. По мере развития и инноваций технологий есть основания полагать, что контейнерная технология будет играть ещё более важную роль в будущем.
Novita AI, универсальная платформа для безграничного творчества, предоставляющая доступ к 100+ API. От генерации изображений и обработки языка до улучшения аудио и манипуляции видео — недорогая оплата по мере использования, она освобождает вас от забот по обслуживанию GPU, пока вы создаёте свои продукты. Попробуйте бесплатно.
