OverlayFS в Docker
Изучаем внутреннюю работу OverlayFS — файловой системы, лежащей в основе образов и контейнеров Docker, вместе с сертифицированным специалистом по работе c Kubernetes, OpenShift, Docker и автором статьи на ITNEXT. В этой статье исследована одна из частей архитектуры Docker — файловая система для Linux. Всем поклонникам этой операционной системы на заметку.
Работать с Docker CLI довольно легко — вы просто создаете, запускаете, проверяете, извлекаете и отправляете контейнеры и образы. Но задумывались ли вы над тем, как на самом деле работают внутренние компоненты в Docker-интерфейсе?
Здесь скрывается множество интересных технологий, и в этой статье мы рассмотрим одну из них — union filesystem — файловую систему, лежащую в основе всех слоев контейнеров и образов.
Union mount — это тип файловой системы, которая создает иллюзию слияния содержимого нескольких каталогов в один без изменения исходных (физических) данных в оригинальных источниках. Это может быть полезно, когда у нас есть наборы файлов, которые хранятся в разных местах и на разных носителях, и мы хотим их объединить. Например, пользовательские директории /home с удаленных NFS-серверов — все они объединены в один каталог или в один полный ISO-образ.
Union mount или объединенная файловая система — это не тип файловой системы, а скорее концепция с возможностью различных реализаций. Некоторые из них быстрее, некоторые проще, они могут достигать совершенно разных целей и уровней исполнения. Прежде чем мы начнем разбираться во всех деталях, давайте кратко рассмотрим некоторые из наиболее популярных реализаций:
Чтобы более подробно изучить эти драйверы для Docker, ознакомьтесь с документацией. Но если не уверены в своих действиях, просто используйте дефолтный overlay2, который также будет использоваться в этой статье в качестве демо.
Выше была упомянута причина, по которой представленный тип файловой системы может быть полезен. Но почему именно он — хороший выбор для контейнеров и Docker в целом?
Многие образы, которые мы используем для контейнеров, имеют довольно большие размеры. К примеру, размер ubuntu 72 Мб или nginx — 133 Мб. Нецелесообразно выделять столько места каждый раз, когда мы хотим создать контейнер. Благодаря объединенной файловой системе, в Docker нужно создать только один слой поверх образа, а остальная его часть может использоваться всеми контейнерами. Это также дает дополнительное преимущество в виде сокращения времени запуска, поскольку нет необходимости копировать файлы образов и другие данные.
Union Filesystem также обеспечивает изоляцию, поскольку контейнеры имеют доступ только для чтения к слоям образов. Если им понадобится изменить какой-либо из общих файлов, доступных только для чтения, они используют копирование при записи — копирование контента на верхний доступный для записи уровень, где его можно безопасно изменить.
Из всего описанного выше может показаться, что Union Filesystem обладает магическими способностями, но на самом деле это не так. Давайте начнем с объяснения того, как это работает в общем (неконтейнерном) случае. Представим, что мы хотели бы объединить две директории (верхний и нижний каталоги) в одну и ту же точку монтирования и получить их объединенное представление:
. ├── upper │ ├── code.py # Content: `print("Hello Overlay!")` │ └── script.py └── lower ├── code.py # Content: `print("This is some code...")` └── config.yaml
В терминологии Union mount эти каталоги называются ветками. Каждой из этих веток назначается свой приоритет. Этот приоритет используется для определения того, какой файл будет отображаться в объединенном представлении в случае, если в нескольких ветках есть файлы с одинаковыми именами. Глядя на файлы и директории выше, становится ясно, что если мы попытаемся наложить их поверх, они буду конфликтовать (файл code.py):
~ $ mount -t overlay \ -o lowerdir=./lower,\ upperdir=./upper,\ workdir=./workdir \ overlay /mnt/merged ~ $ ls /mnt/merged code.py config.yaml script.py ~ $ cat /mnt/merged/code.py print("Hello Overlay!")
В приведенном выше примере мы использовали команду mount с типом overlay, чтобы объединить lower (только для чтения, низкий приоритет) и upper (запись, высокий приоритет) каталоги в единое представление /mnt/merged. Также был включен параметр workdir=./workdir, который служит местом для подготовки объединенного представления lowerdir и upperdir перед его перемещением в /mnt/merged.
Глядя на вывод команды cat выше, можно увидеть, что содержимое файлов в upper (верхнем) каталоге имеет приоритет в объединенном представлении.
Теперь мы знаем, как объединить два каталога и что произойдет в случае конфликта. Но что произойдет, если мы попытаемся изменить некоторые файлы из объединенного представления?
Здесь в игру вступает функция копирования при записи (CoW). Что это такое? CoW — это метод оптимизации, при котором создается общая копия ресурса при его запросах, осуществляемых одновременно. Копирование становится необходимым только тогда, когда один из вызывающих абонентов пытается записать свою «копию». Отсюда и термин «копировать при (первой попытке) записи».
В случае union mount это означает, что когда мы пытаемся изменить общий файл (или файл только для чтения), он сначала копируется в верхнюю доступную для записи ветку (upperdir), которая имеет более высокий приоритет, чем нижние ветки только для чтения (lowerdir). Когда файл находится в ветке с возможностью записи, его можно безопасно изменить. Его новый контент будет виден в объединенном представлении, потому что верхний слой имеет более высокий приоритет.
Последняя операция — это удаление файлов. При удалении в ветке с возможностью записи создается чистый файл. Файл, который мы хотим удалить, на самом деле не удаляется, а скорее скрывается в объединенном представлении.
Как же union mount связан с Docker и его контейнерами? Давайте посмотрим на многоуровневую архитектуру Docker. Песочница контейнера состоит из нескольких веток — или слоев. Слои доступны только для чтения (lowerdir) и являются частью объединенного представления, а слой контейнера — записываемой верхней частью (upperdir).
Слои образов, которые вы извлекаете из реестра, являются lowerdir (нижним) каталогом, а при запуске контейнера, upperdir (верхний) каталог приаттачивается к верхним слоям образов, чтобы обеспечить доступное для записи рабочее пространство для вашего контейнера. Звучит довольно просто, правда? Попробуем?
Чтобы продемонстрировать, как OverlayFS используется в Docker, попробуем имитировать то, как Docker монтирует слои образов и контейнера.
~ $ docker image prune -af ... Total reclaimed space: ...MB ~ $ docker pull nginx Using default tag: latest latest: Pulling from library/nginx a076a628af6f: Pull complete 0732ab25fa22: Pull complete d7f36f6fe38f: Pull complete f72584a26f32: Pull complete 7125e4df9063: Pull complete Digest: sha256:10b8cc432d56da8b61b070f4c7d2543a9ed17c2b23010b43af434fd40e2ca4aa Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest
Итак, у нас есть nginx, теперь давайте проверим его слои. Мы можем проверить слои образов, запустив docker inspect и просмотрев поля GraphDriver, или отправиться в каталог /var/lib/docker/overlay2, где хранятся все слои образов. Давайте воспользуемся двумя способами и посмотрим, что внутри:
~ $ cd /var/lib/docker/overlay2 ~ $ ls -l total 0 drwx------. 4 root root 55 Feb 6 19:19 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd drwx------. 3 root root 47 Feb 6 19:19 410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46 drwx------. 4 root root 72 Feb 6 19:19 685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e brw-------. 1 root root 253, 0 Jan 31 18:15 backingFsBlockDev drwx------. 4 root root 72 Feb 6 19:19 d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e drwx------. 4 root root 72 Feb 6 19:19 fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505 drwx------. 2 root root 176 Feb 6 19:19 l ~ $ tree 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/ 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/ ├── diff │ └── docker-entrypoint.d │ └── 20-envsubst-on-templates.sh ├── link ├── lower └── work ~ $ docker inspect nginx | jq .[0].GraphDriver.Data { "LowerDir": "/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:/var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:/var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:/var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff", "MergedDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/merged", "UpperDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff", "WorkDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/work" }
Очень похоже на то, что мы видели с командой mount, правда? Более конкретно:
Теперь давайте запустим контейнер и проверим слои:
~ $ docker run -d --name container nginx ~ $ docker inspect container | jq .[0].GraphDriver.Data { "LowerDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff:/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff:/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:/var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:/var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:/var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff", "MergedDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/merged", "UpperDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff", "WorkDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work" } ~ $ tree -l 3 /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff # The UpperDir /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff ├── etc │ └── nginx │ └── conf.d │ └── default.conf ├── run │ └── nginx.pid └── var └── cache └── nginx ├── client_temp ├── fastcgi_temp ├── proxy_temp ├── scgi_temp └── uwsgi_temp
Приведенный выше пример показывает, что те же каталоги, которые были выведены в docker inspect nginx ранее, как MergedDir, UpperDir и WorkDir (с ID 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd) теперь являются частью LowerDir-контейнера. Здесь LowerDir состоит из всех слоев образов nginx, наложенных друг на друга. Поверх них находится доступный для записи слой в UpperDir, который содержит /etc, /run и /var. В MergedDir находится вся файловая система, доступная для контейнера, включая все содержимое из UpperDir и LowerDir.
Чтобы имитировать поведение Docker, мы можем использовать эти же каталоги для ручного создания нашего собственного объединенного представления:
~ $ mount -t overlay -o \ lowerdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff:/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff:/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:/var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:/var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:/var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff,\ upperdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff,\ workdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work \ overlay /mnt/merged ~ $ ls /mnt/merged bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var boot docker-entrypoint.d etc lib media opt root sbin sys usr ~ $ umount overlay
Здесь мы просто взяли значения из предыдущего фрагмента и добавили их в соответствующие аргументы в команде mount с той лишь разницей, что для объединенного представления были использованы /mnt/merged вместо /var/lib/docker/overlay2/…/merged.
OverlayFS в Docker в итоге сводится единой mount команде на многих наложенных друг на друга слоях. Ниже приведена часть кода Docker, отвечающая за: подстановку значений lowerdir = …, upperdir = …, workdir = … и отслеживание unix.Mount.
// https://github.com/moby/moby/blob/1ef1cc8388165b2b848f9b3f53ec91c87de09f63/daemon/graphdriver/overlay2/overlay.go#L580 opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", strings.Join(absLowers, ":"), path.Join(dir, "diff"), path.Join(dir, "work")) mountData := label.FormatMountLabel(opts, mountLabel) mount := unix.Mount mountTarget := mergedDir rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) // ...
Интерфейс Docker поначалу кажется черным ящиком с множеством непонятных технологий внутри. Эти технологии довольно интересны и полезны. И хотя, чтобы эффективно использовать Docker, вам совсем не нужно уметь в них разбираться, все же стоит немного углубиться в эту тему и понять их предназначение.
Более глубокое понимание инструмента помогает принимать правильные решения, касающиеся в данном случае оптимизации производительности и последствий для безопасности. Вы откроете для себя некоторые крутые технологии, которые в будущем могут иметь для вас много вариантов использования.
Текст для Highload перевела Ольга Змерзлая.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…