Создавать, запускать, просматривать, перемещать контейнеры и образы с помощью интерфейса командной строки Docker (Docker CLI) проще простого, но задумывались ли вы когда-нибудь, как на самом деле работают внутренние компоненты, обеспечивающие работу интерфейса Docker? За этим простым интерфейсом скрывается множество продвинутых технологий, и специально к старту нового потока курса по DevOps в этой статье мы рассмотрим одну из них — объединённую файловую систему, используемую во всех слоях контейнеров и образов. Маститым знатокам контейнеризации и оркестрации данный материал навряд ли откроет что-то новое, зато будет полезен тем, кто делает первые шаги в DevOps. Каскадно-объединённое монтирование — это тип файловой системы, в которой создается иллюзия объединения содержимого нескольких каталогов в один без изменения исходных (физических) источников. Такой подход может оказаться полезным, если имеются связанные наборы файлов, хранящиеся в разных местах или на разных носителях, но отображать их надо как единое и совокупное целое. Например, набор пользовательских/корневых каталогов, расположенных на удалённых NFS-серверах, можно свести в один каталог или можно объединить разбитый на части ISO-образ в один целый образ. Однако технологию объединённого монтирования (или объединённой файловой системы), по сути, нельзя считать отдельным типом файловой системы. Это, скорее, особая концепция с множеством реализаций. Некоторые реализации работают быстрее, некоторые медленнее, некоторые проще в использовании, некоторые сложнее — проще говоря, у разных реализаций разные цели и разные уровни зрелости. Поэтому, прежде чем углубиться в детали, ознакомимся с некоторыми наиболее популярными реализациями объединённой файловой системы: Начнём с исходной объединённой файловой системы, а именно UnionFS. На данный момент поддержка файловой система UnionFS прекращена, последнее изменение кода было зафиксировано в августе 2014 года. Более подробная информация об этой файловой системе приведена здесь: unionfs.filesystems.org. aufs — альтернативная версия исходной файловой системы UnionFS с добавлением множества новых функций. Данную файловую систему нельзя использовать в составе ванильного ядра Linux. Aufs использовалась в качестве файловой системы по умолчанию для Docker на Ubuntu/Debian, однако со временем она была заменена на OverlayFS (для ядра Linux >4.0). По сравнению с другими объединёнными файловыми системами эта система имеет ряд преимуществ, описанных в Docker Docs. Следующая система — OverlayFS — была включена в ядро Linux Kernel, начиная с версии 3.18 (26 октября 2014 года). Данная файловая система используется по умолчанию драйвером overlay2 Docker (это можно проверить, запустив команду docker system info | grep Storage). Данная файловая система в целом обеспечивает лучшую, чем aufs, производительность и имеет ряд интересных функциональных особенностей, например функцию разделения страничного кэша. ZFS — объединённая файловая система, разработанная Sun Microsystems (в настоящее время эта компания называется Oracle). В этой системе реализован ряд полезных функций, таких как функция иерархического контрольного суммирования, функция обработки снимков, функция резервного копирования/репликации или архивирования и дедупликации (исключения избыточности) внутренних данных. Однако, поскольку автором этой файловой системы является Oracle, её выпуск осуществлялся под общей лицензией на разработку и распространение (CDDL), не распространяемой на программное обеспечение с открытым исходным кодом, поэтому данная файловая система не может поставляться как часть ядра Linux. Тем не менее можно воспользоваться проектом ZFS on Linux (ZoL), который в документации Docker описывается как работоспособный и хорошо проработанный..., но, увы, непригодный к промышленной эксплуатации. Если вам захочется поработать с этой файловой системой, её можно найти здесь. Btrfs — ещё один вариант файловой системы, представляющий собой совместный проект множества компаний, в том числе SUSE, WD и Facebook. Данная файловая система выпущена под лицензией GPL и является частью ядра Linux. Btrfs — файловая система по умолчанию дистрибутива Fedora 33. В ней также реализованы некоторые полезные функции, такие как операции на уровне блоков, дефрагментация, доступные для записи снимки и множество других. Если вас не пугают трудности, связанные с переходом на специализированный драйвер устройств памяти для Docker, файловая система Btrfs с её функциональными и производительными возможностями может стать лучшим вариантом. Если вы хотите более глубоко изучить характеристики драйверов, используемых в Docker, в Docker Docs можно ознакомиться с таблицей сравнения разных драйверов. Если вы затрудняетесь с выбором файловой системы (не сомневаюсь, есть и такие программисты, которые знают все тонкости файловых систем, но эта статья предназначена не для них), возьмите за основу файловую систему по умолчанию overlay2 — именно её я буду использовать в примерах в оставшейся части данной статьи. В предыдущем разделе мы рассказали о некоторых полезных возможностях файловой системы такого типа, но почему именно она является оптимальным выбором для работы Docker и контейнеров в целом? Многие образы, используемые для запуска контейнеров, занимают довольно большой объём, например, ubuntu занимает 72 Мб, а nginx — 133 Мб. Было бы довольно разорительно выделять столько места всякий раз, когда потребуется из этих образов создать контейнер. При использовании объединённой файловой системы Docker создаёт поверх образа тонкий слой, а остальная часть образа может быть распределена между всеми контейнерами. Мы также получаем дополнительное преимущество за счёт сокращения времени запуска, так как отпадает необходимость в копировании файлов образа и данных. Объединённая файловая система также обеспечивает изоляцию, поскольку контейнеры имеют доступ к общим слоям образа только для чтения. Если контейнерам когда-нибудь понадобится внести изменения в любой файл, доступный только для чтения, они используют стратегию копирования при записи copy-on-write (её мы обсудим чуть позже), позволяющую копировать содержимое на верхний слой, доступный для записи, где такое содержимое может быть безопасно изменено. Теперь вы вправе задать мне важный вопрос: как же это всё работает на практике? Из сказанного выше может создаться впечатление, что объединённая файловая система работает с применением некой чёрной магии, но на самом деле это не так. Сейчас я попытаюсь объяснить, как это работает в общем (неконтейнерном) случае. Предположим, нам нужно объединить два каталога (верхний и нижний) в одной точке монтирования и чтобы такие каталоги были представлены унифицированно: В терминологии объединённого монтирования такие каталоги называются ветвями. Каждой из таких ветвей присваивается свой приоритет. Приоритет используется для того, чтобы решить, какой именно файл будет отображаться в объединённом представлении, если в нескольких исходных ветках присутствуют файлы с одним и тем же именем. Если проанализировать представленные выше файлы и каталоги, станет понятно, что такой конфликт может возникнуть, если мы попытаемся использовать их в режиме наложения (файл code.py). Давайте попробуем и посмотрим, что у нас получится: В приведённом выше примере мы использовали команду mount с type overlay, чтобы объединить нижний каталог (только для чтения; более низкий приоритет) и верхний каталог (чтение-запись; более высокий приоритет) в объединённое представление в каталоге /mnt/merged. Мы также включили опцию workdir=./workdir. Этот каталог служит местом для подготовки объединённого представления нижнего каталога (lowerdir) и верхнего каталога (upperdir) перед их перемещением в каталог /mnt/merged. Если посмотреть на выходные данные команды cat, можно заметить, что в объединённом представлении приоритет получили файлы верхнего каталога. Теперь мы знаем, как объединить два каталога и что произойдёт при возникновении конфликта. Но что произойдёт, если попытаться изменить определенные файлы в объединённом представлении? Здесь в игру вступает функция копирования при записи (CoW). Что именно делает эта функция? CoW — это способ оптимизации, при котором, если две вызывающих программы обращаются к одному и тому же ресурсу, можно дать им указатель на один и тот же ресурс, не копируя его. Копирование необходимо только тогда, когда одна из вызывающих программ пытается осуществить запись собственной "копии" — отсюда в названии способа появилось слово "копия", то есть копирование осуществляется при (первой попытке) записи. В случае объединённого монтирования это означает, что, если мы пытаемся изменить совместно используемый файл (или файл только для чтения), он вначале копируется в верхнюю ветвь, доступную для записи (upperdir), имеющую более высокий приоритет, чем нижние ветви (lowerdir), доступные только для чтения. Когда файл попадает в ветвь, доступную для записи, его можно безопасно изменить, и его новое содержимое отобразится в объединённом представлении, так как верхний слой имеет более высокий приоритет. Последняя операция, которую мы, возможно, захотим выполнить, — это удаление файлов. Чтобы "удалить" файл, в ветви, доступной для записи, создается файл whiteout для очистки "удаляемого" файла. На самом деле файл не будет удалён физически. Вернее сказать, что он будет скрыт в объединённом представлении. Мы много говорили о принципах объединённого монтирования, но как все эти принципы работают на платформе Docker и её контейнерах? Рассмотрим многоуровневую архитектуру Docker. Песочница контейнера состоит из нескольких ветвей образа, или, как мы их называем, слоёв. Такими слоями являются часть объединённого представления, доступная только для чтения (lowerdir), и слой контейнера — тонкая верхняя часть, доступная для записи (upperdir). Не считая терминологических различий, речь фактически идёт об одном и том же — слои образа, извлекаемые из реестра, представляют собой lowerdir, и, если запускается контейнер, upperdir прикрепляется поверх слоев образа, обеспечивая рабочую область, доступную для записи в контейнер. Звучит довольно просто, не так ли? Давайте проверим, как всё работает! Чтобы показать, как Docker использует файловую систему OverlayFS, попробуем смоделировать процесс монтирования Docker контейнеров и слоев образа. Прежде чем мы приступим, нужно вначале очистить рабочее пространство и получить образ, с которым можно работать: Итак, у нас имеется образ (nginx), с которым можно работать, далее нужно проверить его слои. Проверить слои образа можно, либо запустив проверку образа в Docker и изучив поля GraphDriver, либо перейдя в каталог /var/lib/docker/overlay2, в котором хранятся все слои образа. Выполним обе эти операции и посмотрим, что получится: Если внимательно посмотреть на полученные результаты, можно заметить, что они весьма похожи на те, что мы уже наблюдали после применения команды mount, не находите? LowerDir: это каталог, в котором слои образа, доступные только для чтения, разделены двоеточиями. MergedDir: объединённое представление всех слоев образа и контейнера. UpperDir: слой для чтения и записи, на котором записываются изменения. WorkDir: рабочий каталог, используемый Linux OverlayFS для подготовки объединённого представления. Сделаем ещё один шаг — запустим контейнер и изучим его слои: Из представленных выше выходных данных следует, что те же каталоги, которые были перечислены в выводе команды docker inspect nginx ранее как MergedDir, UpperDir и WorkDir (с id 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd), теперь являются частью LowerDir контейнера. В нашем случае LowerDir составляется из всех слоев образа nginx, размещённых друг на друге. Поверх них размещается слой в UpperDir, доступный для записи, содержащий каталоги /etc, /run и /var. Также, раз уж мы выше упомянули MergedDir, можно видеть всю доступную для контейнера файловую систему, в том числе всё содержимое каталогов UpperDir и LowerDir. И, наконец, чтобы эмулировать поведение Docker, мы можем использовать эти же каталоги для ручного создания собственного объединённого представления: В нашем случае мы просто взяли значения из предыдущего фрагмента кода и передали их в качестве соответствующих аргументов в команду mount. Разница лишь в том, что для объединённого представления вместо /var/lib/docker/overlay2/.../merged мы использовали /mnt/merged. Именно к этому сводится действие файловой системы OverlayFS в Docker — на множестве уложенных друг на друга слоев может использоваться одна команда монтирования. Ниже приводится отвечающая за это часть кода Docker — заменяются значения lowerdir=...,upperdir=...,workdir=..., после чего следует команда unix.Mount. Интерфейс Docker может показаться чёрным ящиком с большим количеством скрытых внутри непонятных технологий. Эти технологии — пусть непонятные — довольно интересны и полезны. Я вовсе не хочу сказать, что для эффективного использования Docker нужно досконально знать все их тонкости, но, на мой взгляд, если вы потратите немного времени и поймёте, как они работают, это пойдёт вам только на пользу. Ясное понимание принципов работы инструмента облегчает принятие правильных решений — в нашем случае речь идёт о повышении производительности и возможных аспектах, связанных с безопасностью. Кроме того, вы сможете ознакомиться с некоторыми продвинутыми технологиями, и кто знает, в каких областях знаний они могут вам пригодится в будущем! В этой статье мы рассмотрели только часть архитектуры Docker — файловую систему. Есть и другие части, с которыми стоит ознакомиться более внимательно, например контрольные группы (cgroups) или пространства имен Linux. Если вы их освоите — можно уже задуматься о переходе в востребованный DevOps. А с остальными знаниями, необходимыми для данной профессии мы поможем на курсе по профессии DevOps-инженер. Узнайте, как прокачаться в других специальностях или освоить их с нуля: Профессия Data Scientist Профессия Data Analyst Курс по Data Engineering ПРОФЕССИИ Профессия Fullstack-разработчик на Python Профессия Java-разработчик Профессия QA-инженер на JAVA Профессия Frontend-разработчик Профессия Этичный хакер Профессия C++ разработчик Профессия Разработчик игр на Unity Профессия Веб-разработчик Профессия iOS-разработчик с нуля Профессия Android-разработчик с нуля КУРСЫ Курс по Machine Learning Курс "Machine Learning и Deep Learning" Курс "Математика для Data Science" Курс "Математика и Machine Learning для Data Science"  Курс "Python для веб-разработки" Курс "Алгоритмы и структуры данных" Курс по аналитике данных Курс по DevOps