Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Слои, кеширование и оптимизации Docker: Основы

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

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

Принцип ее работы следующий. OverlayFS работает не с единой файловой структурой, а c частями, которые называются слоями. Каждый слой, это набор файлов и директорий, получающийся в результате выполнения команды RUN и ей подобных внутри Dockerfile. Затем слои виртуально сливаются в одну структуру, создавая внешнее впечатление что все эти директории и файлы находятся в одном месте.

OverlayFS

Проще всего понять эту концепцию на примере. Возьмем такой Dockerfile:

FROM ubuntu

RUN touch file1
RUN touch file2
RUN mkdir dir1
RUN touch dir1/inner_file

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

В примере выше есть только добавление файлов и директорий. В таком случае каждый следующий слой соответственно содержит то что было добавлено именно в нем. Но что будет если файл меняется или удаляется? Ровно то же самое, предыдущие слои не могут поменяться, а это значит что измененный файл будет продублирован в том слое где его поменяли, а удаленный просто помечается как удаленный, так как его физически нельзя удалить, он уже был добавлен в предыдущих слоях. Из-за этого размер образа не будет уменьшен после удаления файла, который был добавлен в другом слое:

FROM ubuntu

# Копируем и устанавливаем пакет Debian
COPY package.deb
RUN dpkg -i package.deb
RUN rm package.deb

В данном случае удаление просто скрывает этот файл, так как более поздний слой "накладывается" на предыдущий. Но физически, файл находится внутри и занимает место.

Все это относится не только к текущему Dockerfile, но и ко всей иерархии образов вплоть до базового образа scratch. В этом смысле принцип формирования Docker-образов очень похож на то, как работает Git. Образы в этом смысле выступают просто ветками, которые никогда не сливаются.

Для того чтобы эта система работала прозрачно, каждый следующий слой видит предыдущие как объединенную файловую систему без разбиения на слои. Поэтому с точки зрения работы с Docker создается ощущение, как будто нет никакого OverlayFS. Это легко проверить если запустить любой контейнер и поменять файлы внутри. У Docker есть команда docker diff, которая показывает изменения сделанные в рамках работы с контейнером:

# Изменение ФС внутри контейнера это тоже слой
# Оригинальные файлы не меняются

# В одной вкладке терминала

docker run -it ubuntu bash
root@52c848c84eed:/# touch file
root@52c848c84eed:/# rm /etc/e2scrub.conf
root@52c848c84eed:/# mkdir lala

# В другой вкладке терминала

docker diff 52c848c84eed
A /file
C /etc
D /etc/e2scrub.conf
A /lala

Очень похоже на git status. Только в отличии от Git, внутри хранятся не изменения а файлы целиком.

Что все это дает? Довольно много. Фактически полностью пропадает необходимость копировать файловую структуру при старте контейнера. Любые изменения сделанные внутри, не меняют исходную структуру файлов, они создают новый слой, который уничтожается при удалении контейнера. Как плюс, значительно сокращается место занимаемое запущенными контейнерами.

Кеширование слоев

Так как каждый слой не изменяет предыдущий, а формирует набор изменений, то Docker идет еще дальше и вычисляет хеш этих изменений, который становится идентификатором слоя. Такая схема позволяет сравнивать слои по хешам и не дублировать их содержимое. То есть, если например мы сделали два образа наследующихся от ubuntu у себя на машине, то образ ubuntu не будет существовать в двух вариантах. То же самое касается и команд внутри Dockerfile. Если два образа наследующихся от одного базового образа имеют общие команды в начале, то они получат преимущество по объему хранения, так как эти слои не будут дублироваться:

# Первый образ
FROM ubuntu

RUN touch file1
RUN touch file2
RUN mkdir dir1
RUN touch dir1/inner_file

# Второй образ
FROM ubuntu

RUN touch file1
RUN touch file3 # тут разница
RUN mkdir dir1
RUN touch dir1/inner_file

Выше мы видим что первые строчки в образах одинаковые. Поэтому они будут переиспользоваться. Это не только сокращает место, но и ускоряет сборку. Собрав один из этих образов, мы увидим, что второй начнет собираться с той команды, где начинается различие: RUN touch file3.

Иногда это создает проблемы. Предположим что у нас есть такой Dockerfile:

FROM ubuntu

RUN apt-get install -yy make

Если мы хотя бы раз собирали или скачали образ, то повторный запуск всегда будет переиспользовать существующий слой. Docker никогда не завязывается на внешние системы и не может проверить, что make мог обновиться. И это правильно, так как задача Docker обеспечивать повторяемость (идемпотентность). Чтобы заставить его игнорировать существующие слои и выполнить все команды заново, нужно добавить флаг сборки --no-cache:

docker build --no-cache .

Иногда, для лучшего контроля делают по-другому, например, добавляют дополнительную строку в Dockerfile, которая приводит к аналогичному результату:

FROM ubuntu

# Имя переменной может быть абсолютно любым
# Если надо сбросить кеш, то меняется номер
ENV VERSION 1
RUN apt-get install -yy make

Удобство такого подхода в том, что изменение фиксируется в Git.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»