Изучите внутреннюю работу OverlayFS, файловой системы, лежащей в основе многоуровневой архитектуры образов и контейнеров Docker.

Работать с Docker CLI очень просто - вы просто используете контейнеры и изображения build, run, inspect, pull и push, но задумывались ли вы, как на самом деле работают внутренние компоненты этого интерфейса Docker? За этим простым интерфейсом скрывается множество интересных технологий, и в этой статье мы рассмотрим одну из них - объединенную файловую систему - файловую систему, лежащую в основе всех слоев контейнера и изображения ...

Что такое Union Filesystem?

Union mount - это тип файловой системы, которая может создать иллюзию слияния содержимого нескольких каталогов в один без изменения его исходных (физических) источников. Это может быть полезно, поскольку у нас могут быть связанные наборы файлов, хранящиеся в разных местах или на разных носителях, и все же мы хотим показать их в едином объединенном представлении. Примером этого может быть группа /home каталогов пользователей с удаленных серверов NFS, все объединенные в один каталог или объединение разделенного образа ISO в один полный.

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

  • UnionFS. Начнем с исходной файловой системы union. UnionFS, похоже, больше не развивается активно, с его последней фиксацией от августа 2014 года. Вы можете прочитать немного больше об этом на его веб-сайте по адресу https://unionfs.filesystems.org/.
  • aufs - повторная реализация исходной UnionFS, которая добавила много новых функций, но была отклонена для слияния с основным ядром Linux. Aufs был драйвером по умолчанию для Docker в Ubuntu / Debian, но был заменен на OverlayFS (для ядра Linux ›4.0). Он имеет некоторые преимущества по сравнению с другими объединенными файловыми системами, описанными на странице документации Docker.
  • OverlayFS - далее OverlayFS, которая включена в ядро ​​Linux с версии 3.18 (26 октября 2014 г.). Это файловая система, используемая по умолчанию драйвером overlay2 Docker (вы можете проверить это с помощью docker system info | grep Storage). Обычно он имеет лучшую производительность, чем aufs, и имеет некоторые полезные функции, такие как совместное использование кеша страниц.
  • ZFS - ZFS - это объединенная файловая система, созданная Sun Microsystems (теперь Oracle). Он имеет некоторые интересные функции, такие как иерархическое вычисление контрольных сумм, встроенная обработка моментальных снимков и резервное копирование / репликация или собственное сжатие и дедупликация данных. Однако, поскольку он поддерживается Oracle, он не имеет лицензии на использование OSS (CDDL) и поэтому не может поставляться как часть ядра Linux. Однако вы можете использовать проект ZFS в Linux (ZoL), который описан в документации Docker как работоспособный и развивающийся…, но не готовый к производству. Если хотите попробовать, то можете найти здесь.
  • Btrfs. Другой вариант - Btrfs, который является совместным проектом нескольких компаний, включая SUSE, WD или Facebook, публикуется под лицензией GPL и является частью ядра Linux. Btrfs - это файловая система по умолчанию в Fedora 33. Он также имеет некоторые полезные функции, такие как операции на уровне блоков, дефрагментацию, снимки состояния с возможностью записи и многое другое. Если вы действительно хотите пережить трудности с переключением на драйвер хранилища, отличный от стандартного для Docker, то вам подойдет Btrfs с его функциями и производительностью.

Если вы хотите более подробно изучить эти драйверы применительно к Docker, вы можете проверить сравнение драйверов в Docker docs. Тем не менее, если вы действительно не знаете, что делаете (в этот момент вы не стали бы читать эту статью), вам следует просто придерживаться значения overlay2 по умолчанию, которое также будет использоваться в остальная часть этой статьи для демонстраций.

Но почему?

В предыдущем разделе мы упомянули причину, по которой этот тип файловой системы может быть полезен, но почему он является хорошим выбором для Docker и контейнеров в целом?

Многие изображения, которые мы используем для раскрутки наших контейнеров, довольно громоздки, будь то ubuntu размером 72 МБ или nginx размером 133 МБ. Было бы довольно дорого выделять столько места каждый раз, когда мы хотели бы создать контейнер из этих изображений. Благодаря объединенной файловой системе Docker нужно только создать тонкий слой поверх образа, а остальная его часть может использоваться всеми контейнерами. Это также дает дополнительное преимущество в виде сокращения времени запуска, поскольку нет необходимости копировать файлы изображений и данные.

Файловая система Union также обеспечивает изоляцию, поскольку контейнеры имеют доступ только для чтения к общим слоям изображений. Если им когда-либо понадобится изменить какой-либо из общих файлов, доступных только для чтения, они используют стратегию копирование при записи (обсуждается немного позже), чтобы скопировать контент на свой верхний доступный для записи уровень, где он может быть безопасно изменен.

Как это работает?

Пришло время задать важный вопрос - как это на самом деле работает? Из всего описанного выше может показаться, что вся файловая система union - это своего рода черная магия, но на самом деле это не так. Давайте начнем с объяснения того, как это работает в общем (неконтейнерном) случае - представим, что мы хотели бы объединить два монтируемых каталога (upper и lower) в одну точку монтирования и получить их объединенное представление:

В терминологии union mount эти каталоги называются ветвями. Каждой из этих веток назначается приоритет. Этот приоритет используется для определения того, какой файл будет отображаться в объединенном представлении, если в нескольких исходных ветвях есть файлы с одинаковыми именами. Глядя на файлы и каталоги выше, становится ясно, что если мы попытаемся наложить их поверх, мы создадим конфликт такого рода (code.py файл). Итак, давайте попробуем и посмотрим, что появится:

В приведенном выше примере мы использовали команду mount с типом overlay для объединения каталога lower (только для чтения; более низкий приоритет) и каталога upper (чтение-запись; более высокий приоритет) в объединенное представление в /mnt/merged. Мы также включили параметр workdir=./workdir, который служит местом для подготовки объединенного представления lowerdir и upperdir перед его перемещением в /mnt/merged в атомарном действии.

Также глядя на вывод команды cat выше, мы видим, что действительно содержимое файлов в каталоге upper имело приоритет в объединенном представлении.

Итак, теперь мы знаем, как объединить 2 каталога и что произойдет в случае конфликта, но что произойдет, если мы попытаемся изменить некоторые файлы из объединенного представления? Вот тут-то и вступает в игру копирование при записи (CoW). Итак, что это такое? CoW - это метод оптимизации, при котором, если два вызывающих абонента запрашивают один и тот же ресурс, вы можете дать им указатель на один и тот же ресурс, не копируя его. Копирование становится необходимым только тогда, когда один из вызывающих пытается записать в их «копию» - отсюда и термин копировать при (первой попытке) записи.

В случае объединенного монтирования это означает, что когда мы пытаемся изменить общий файл (или файл только для чтения), он сначала копируется в верхнюю ветвь с возможностью записи (upperdir), которая имеет более высокий приоритет, чем нижние ветки только для чтения (lowerdir). Затем - когда он находится в ветке с возможностью записи - его можно безопасно изменить, и его новый контент будет виден в объединенном представлении, потому что верхний уровень имеет более высокий приоритет.

Последняя операция, которую мы могли бы захотеть выполнить, - это удаление файлов. Для выполнения «удаления» в доступной для записи ветке создается файл whiteout, чтобы очистить файл, который мы хотим удалить. Это означает, что файл на самом деле не удаляется, а скорее скрывается в объединенном представлении.

Мы много говорили о том, как работает union mount в целом, но как все это связано с Docker и его контейнерами? Чтобы соединить все это вместе, давайте посмотрим на многоуровневую архитектуру Docker. Песочница контейнера состоит из нескольких ветвей изображения или, как мы все знаем, слоев. Эти слои являются частью объединенного представления, доступной только для чтения (lowerdir), а слой контейнера - это тонкая верхняя часть с возможностью записи (upperdir).

В остальном, кроме этой архитектурной терминологии, это действительно то же самое - слои изображений, которые вы извлекаете из реестра, - это lowerdir, а когда вы запускаете контейнер, upperdir прикрепляется к верхним слоям изображений, чтобы обеспечить доступное для записи рабочее пространство для вашего контейнера. Звучит довольно просто, правда? Итак, попробуем!

Пробовать это

Чтобы продемонстрировать, как Docker использует OverlayFS, мы попробуем эмулировать, как Docker монтирует слои контейнера и изображения. Прежде чем мы это сделаем, нам сначала нужно очистить нашу рабочую область и получить изображение, с которым можно поиграть:

У нас есть изображение (nginx), с которым можно поиграть, так что теперь давайте проверим его слои. Мы можем проверить слои изображения, запустив docker inspect на изображении и проверив поля GraphDriver, или пройдя через каталог /var/lib/docker/overlay2, в котором хранятся все слои изображения. Итак, давайте сделаем и то, и другое и посмотрим, что внутри:

Глядя на вывод выше, он очень похож на то, что мы видели с командой mount, верно? Более конкретно:

  • LowerDir: это каталог со слоями изображений, доступными только для чтения, разделенными двоеточиями
  • MergedDir: объединенный вид всех слоев изображения и контейнера
  • UpperDir: Уровень чтения-записи, на котором записываются изменения
  • WorkDir: Рабочий каталог, используемый Linux OverlayFS для подготовки объединенного представления

Затем давайте сделаем еще один шаг и запустим контейнер и проверим его слои:

Приведенный выше вывод показывает, что те же каталоги, которые были перечислены в выводе docker inspect nginx ранее как MergedDir, UpperDir и WorkDir (с идентификатором 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd), теперь являются частью контейнера LowerDir. LowerDir здесь состоит из всех nginx слоев изображений, уложенных друг на друга. Поверх них находится доступный для записи слой в UpperDir, который содержит /etc, /run и /var. Также, если мы перечислим MergedDir выше, вы увидите всю файловую систему, доступную для контейнера, включая все содержимое из UpperDir и LowerDir.

Наконец, чтобы имитировать поведение Docker, мы можем использовать те же самые каталоги для ручного создания нашего собственного объединенного представления:

Здесь мы просто взяли значения из предыдущего фрагмента и передали их соответствующим аргументам в команде mount, с той лишь разницей, что мы использовали /mnt/merged для объединенного представления вместо /var/lib/docker/overlay2/.../merged.

И это действительно то, к чему сводится вся OverlayFS в Docker - единственная команда mount для множества составных слоев. Ниже приведена часть кода Docker, отвечающая за это - подстановка значений lowerdir=...,upperdir=...,workdir=..., за которыми следует unix.Mount.

Заключение

Если смотреть на интерфейс Docker снаружи, он может показаться черным ящиком с множеством непонятных технологий внутри. Эти технологии, хотя и неясные, довольно интересны и полезны, и, хотя вам не нужно разбираться в них для эффективного использования Docker, все же, на мой взгляд, стоит потрудиться, чтобы узнать о них и понять их. Более глубокое понимание инструмента облегчает принятие правильных решений - в данном случае - в отношении оптимизации производительности или последствий для безопасности. В качестве бонуса это также поможет вам открыть для себя некоторые интересные технологии, которые могут иметь для вас еще много вариантов использования в будущем.

В этой статье мы исследовали только часть архитектуры Docker - файловую систему - и есть другие части, в которые стоит погрузиться, например cgroups или пространства имен Linux. Итак, если вам понравилась эта статья, следите за обновлениями, в которых мы также углубимся в эти технологии. 😉

Эта статья изначально была размещена на martinheinz.dev