Рубріки: Софт

Полное погружение в Docker: файловая система OverlayFS

Павло Бєлавін

Изучаем внутреннюю работу OverlayFS — файловой системы, лежащей в основе образов и контейнеров Docker, вместе с сертифицированным специалистом по работе c Kubernetes, OpenShift, Docker и автором статьи на ITNEXT. В этой статье исследована одна из частей архитектуры Docker — файловая система для Linux. Всем поклонникам этой операционной системы на заметку.

Работать с Docker CLI довольно легко — вы просто создаете, запускаете, проверяете, извлекаете и отправляете контейнеры и образы. Но задумывались ли вы над тем, как на самом деле работают внутренние компоненты в Docker-интерфейсе?

Здесь скрывается множество интересных технологий, и в этой статье мы рассмотрим одну из них — union filesystem — файловую систему, лежащую в основе всех слоев контейнеров и образов.

Что такое Union Filesystem?

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

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

  • UnionFS — начнем с исходной объединенной файловой системы. Похоже, что разработчики UnionFS с августа 2014 года перестали ее развивать. Больше об этом можно узнать здесь.
  • aufs — альтернативная версия UnionFS, которая располагала множеством новых функций, но была отклонена для слияния с основным ядром Linux. Aufs был дефолтным драйвером для Docker в Ubuntu/Debian, но его заменили на OverlayFS (для ядра Linux >4.0). Он имеет некоторые преимущества по сравнению с другими объединенными файловыми системами. Все они описаны в Docker docs page.
  • OverlayFS — включена в ядро Linux с версии 3.18 (26 октября 2014 года). Это файловая система, использующая дефолтный драйвер Docker overlay2 (верифицировать можно с помощью docker system info | grep Storage). OverlayFS имеет лучшую производительность, чем aufs, и располагает некоторыми приятными функциями, такими как совместное использование кеша страниц.
  • ZFS — это объединенная файловая система, созданная Sun Microsystems (теперь Oracle). Имеет некоторые интересные функции, такие как иерархическое вычисление контрольных сумм, встроенная обработка снимков файловой системы и резервное копирование/репликация или сжатие и дедупликация данных. Но при поддержке Oracle ZFS не поддерживает лицензию CDDL и поэтому не может поставляться как часть ядра Linux. Однако можно использовать проект ZFS в Linux (ZoL), который описан в документации Docker, как годный и развивающийся, но не готовый для продакшена. Если хотите попробовать, необходимую информацию можно найти здесь.
  • Btrfs — совместный проект нескольких компаний, включая SUSE, WD или Facebook, опубликованный под лицензией GPL и являющийся частью ядра Linux. Btrfs — это дефолтная файловая система в Fedora 33. Она имеет некоторые полезные функции, такие, как операции в блоках, дефрагментацию, снимки файловой системы с возможностью записи и многое другое. Если вы действительно хотите пройти через все трудности и переключиться на нестандартный драйвер хранилища для Docker, то Btrfs с его функциями и производительностью — лучший вариант.

Чтобы более подробно изучить эти драйверы для Docker, ознакомьтесь с документацией. Но если не уверены в своих действиях, просто используйте дефолтный overlay2, который также будет использоваться в этой статье в качестве демо.

Почему Union Filesystem?

Выше была упомянута причина, по которой представленный тип файловой системы может быть полезен. Но почему именно он — хороший выбор для контейнеров и 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

Чтобы продемонстрировать, как 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, правда? Более конкретно:

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

Теперь давайте запустим контейнер и проверим слои:

~ $ 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 перевела Ольга Змерзлая.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023