В этой части перевода серии материалов, которая посвящена Docker, мы поговорим о том, как оптимизировать размеры образов и ускорить их сборку. В прошлых материалах мы сравнивали образы Docker с пиццей, термины с пончиками, а инструкции файлов Dockerfile с бубликами. Сегодня же не будет никакой выпечки. Пришло время посидеть на диете.
→ Часть 1: основы
→ Часть 2: термины и концепции
→ Часть 3: файлы Dockerfile
→ Часть 4: уменьшение размеров образов и ускорение их сборки
Для того чтобы разобраться с тем, о чём мы будем тут говорить, вам будет полезно освежить в памяти то, о чём шла речь в третьей части этой серии материалов. А именно, там мы говорили об инструкциях файлов Dockerfile. Знание этих инструкций и тех особенностей Docker, которые мы обсудим сегодня, поможет вам оптимизировать файлы образов Docker.
Кэширование
Одной из сильных сторон Docker является кэширование. Благодаря этому механизму ускоряется сборка образов.
При сборке образа Docker проходится по инструкциям файла Dockerfile, выполняя их по порядку. В процессе анализа инструкций Docker проверяет собственный кэш на наличие в нём образов, представляющих собой то, что получается на промежуточных этапах сборки других образов. Если подобные образы удаётся найти, то система может ими воспользоваться, не тратя время на их повторное создание.
Если кэш признан недействительным, то инструкция, в ходе выполнения которой это произошло, выполняется, создавая новый слой без использования кэша. То же самое происходит и при выполнении инструкций, которые следуют за ней.
В результате, если в ходе выполнения инструкций из Dockerfile оказывается, что базовый образ имеется в кэше, то используется именно этот образ из кэша. Это называется «попаданием кэша». Если же базового образа в кэше нет, то весь процесс сборки образа будет происходить без использования кэша.
Затем следующая инструкция сопоставляется со всеми образами из кэша, в основе которых лежит тот же самый базовый образ, который уже обнаружен в кэше. Каждый кэшированный промежуточный образ проверяется на предмет того, имеется ли в нём то, что было создано такой же инструкцией. Если совпадения найти не удаётся, это называется «промахом кэша» и кэш считается недействительным. То же самое происходит до тех пор, пока не будет обработан весь файл Dockerfile.
Большинство новых инструкций просто сравниваются с тем, что уже есть в промежуточных образах. Если системе удаётся найти совпадение, то при сборке используется то, что уже есть в кэше.
Использование кэша способно ускорить сборку образов, но тут есть одна проблема. Например, если в Dockerfile обнаруживается инструкция RUN pip install -r requirements.txt
, то Docker выполняет поиск такой же инструкции в своём локальном кэше промежуточных образов. При этом содержимое старой и новой версий файла requirements.txt
не сравнивается.
Подобное может приводить к проблемам в том случае, если в requirements.txt
были добавлены сведения о новых пакетах, после чего, при сборке обновлённого образа, для того, чтобы установить новый набор пакетов, нужно снова выполнить инструкцию RUN pip install
. Совсем скоро мы поговорим о том, как бороться с этой проблемой.
В отличие от других инструкций Docker, при выполнении инструкций ADD
и COPY
от Docker требуется проверка содержимого файла или файлов для определения того, можно ли, при формировании образа, воспользоваться кэшем. А именно, контрольная сумма файлов, упомянутых в этих инструкциях, сравнивается с контрольной суммой файлов, которые имеются в промежуточных образах, которые уже есть в кэше. Если изменилось содержимое файлов или их метаданные, тогда кэш признаётся недействительным.
Вот несколько советов, касающихся эффективного использования кэша Docker:
- Кэширование можно отключить, передав ключ
--no-cache=True
командеdocker build
. - Если вы собираетесь вносить изменения в инструкции Dockerfile, тогда каждый слой, созданный инструкциями, идущими после изменённых, будет достаточно часто собираться повторно, без использования кэша. Для того чтобы воспользоваться преимуществами кэширования, помещайте инструкции, вероятность изменения которых высока, как можно ближе к концу Dockerfile.
- Объединяйте команды
RUN apt-get update
иapt-get install
в цепочки для того, чтобы исключить проблемы, связанные с неправильным использованием кэша. - Если вы используете менеджеры пакетов, наподобие
pip
, с файломrequirements.txt
, тогда придерживайтесь нижеприведённой схемы работы для того, чтобы исключить использование устаревших промежуточных образов из кэша, содержащих набор пакетов, перечисленных в старой версии файлаrequirements.txt
. Вот как это выглядит:COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt COPY . /tmp/
Если вам известны другие способы борьбы с «проблемой requirements.txt» — можете рассказать о них в комментариях.
Уменьшение размеров образов
▍Тщательный подбор базового образа
Образы Docker могут быть довольно большими. Это противоречит вполне обоснованному стремлению того, кто их создаёт, к тому, чтобы сделать их как можно более компактными, что облегчит их загрузку из удалённого репозитория и благотворно скажется на объёме свободного места на компьютере, на который они загружаются. Поговорим о том, как уменьшать их размеры.
Вместо бубликов и пончиков мы теперь будем есть зелень
Одним из способов уменьшения размеров образов является тщательный подбор базовых образов и их последующая настройка.
Так, например, базовый образ Alpine представляет собой полноценный дистрибутив Linux-подобной ОС, содержащий минимум дополнительных пакетов. Его размер — примерно 5 мегабайт. Однако сборка собственного образа на основе Alpine потребует потратить достаточно много времени на то, чтобы оснастить его всем необходимым для обеспечения работы некоего приложения.
Существуют и специализированные варианты базового образа Alpine. Например, соответствующий образ из репозитория python, в который упакован скрипт print("hello world")
весит около 78.5 Мб. Вот Dockerfile для сборки такого образа:
FROM python:3.7.2-alpine3.8
COPY . /app
ENTRYPOINT ["python", "./app/my_script.py", "my_var"]
При этом на Docker Hub сказано, что этот базовый образ имеет размер 29 Мб. Размер образа, основанного на этом базовом образе, увеличивается за счёт загрузки и установки Python.
Помимо использования базовых образов, основанных на Alpine, уменьшить размеры образов можно благодаря использованию технологии многоступенчатой сборки.
▍Многоступенчатая сборка образов
В Dockerfile, описывающем многоступенчатую сборку образа, используется несколько инструкций FROM
. Создатель такого образа может настроить выборочное копирование файлов, называемых артефактами сборки, из одной ступени сборки в другую ступень. При этом появляется возможность избавиться от всего того, что в готовом образе не понадобится. Благодаря этому методу можно уменьшить размер готового образа.
Вот как работает каждая инструкция FROM
:
- Она начинает новый шаг сборки.
- Она не зависит от того, что было создано на предыдущем шаге сборки.
- Она может использовать базовый образ, отличающийся от того, который применялся на предыдущем шаге.
Вот модифицированный пример файла Dockerfile из документации Docker, описывающего многоступенчатую сборку.
FROM golang:1.7.3 AS build
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=build /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
Обратите внимание на то, что мы дали имя первой ступени сборки, указав его после инструкции FROM
. К именованному этапу сборки мы обращаемся в инструкции COPY --from=
ниже в Dockerfile.
Применение процесса многоступенчатой сборки образов имеет смысл в некоторых случаях, когда приходится создавать множество контейнеров для продакшн-окружения. Многоступенчатая сборка позволяет максимально сократить размеры готовых образов. Но иногда такой подход приводит к усложнению поддержки образов. Поэтому вы, вероятно, не будете пользоваться многоступенчатой сборкой образов в тех случаях, в которых без неё можно обойтись. Об особенностях этой технологии можно почитать здесь и здесь.
Как видите, многоступенчатая сборка — технология интересная, но подходит она далеко не для всех случаев. Тот же способ уменьшения размера образов, который мы обсудим ниже, можно порекомендовать абсолютно всем.
▍Файл .dockerignore
О файлах .dockerignore
нужно знать абсолютно всем, кто хочет освоить Docker. Эти файлы похожи на файлы .gitignore
. Они содержат список файлов и папок, в виде имён или шаблонов, которые Docker должен игнорировать в ходе сборки образа.
Этот файл размещают там же, где находится файл Dockerfile, и всё остальное, входящее в контекст сборки образа.
При запуске команды docker build
, инициирующей сборку образа, Docker проверяет папку на наличие в ней файла .dockerignore
. Если такой файл найти удаётся, тогда этот файл разбирается, при этом при определении списка файлов, которые нужно игнорировать, используются правила функции Match()
из пакета filepath
Go и некоторые собственные правила Docker.
Так, например, если в файле .dockerignore
встретится шаблон вида *.jpg
, то при создании образа проигнорированы будут файлы с любым именем и с расширением .jpg
. Если в файле встретится строка videos
, то система проигнорирует папку videos
и всё её содержимое.
При составлении файла .dockerignore
его можно снабжать комментариями, используя символ #
.
Вот что даёт тому, кто занимается созданием образов Docker, применение файлов .dockerignore
:
- Это позволяет исключать из состава образа файлы, содержащие секретные сведения наподобие логинов и паролей.
- Это позволяет уменьшить размер образа. Чем меньше в образе файлов — тем меньше будет его размер и тем быстрее с ним можно будет работать.
- Это даёт возможность уменьшить число поводов для признания недействительным кэша при сборке похожих образов. Например, если при повторной сборке образа меняются некие служебные файлы проекта, наподобие файлов с журналами, из-за чего данные, хранящиеся в кэше, по сути, необоснованно признаются недействительными, это замедляет сборку образов.
Подробности о файле .dockerignore
можно почитать в документации к Docker.
Исследование размеров образов
Поговорим о том, как, пользуясь средствами командной строки, узнавать размеры образов и контейнеров Docker.
- Для того чтобы выяснить примерный размер выполняющегося контейнера, можно использовать команду вида
docker container ls -s
. - Команда
docker image ls
выводит размеры образов. - Узнать размеры промежуточных образов, из которых собран некий образ, можно с помощью команды
docker image history my_image:my_tag
. - Команда
docker image inspect my_image:tag
позволяет узнать подробные сведения об образе, в том числе — размер каждого его слоя. Слои немного отличаются от промежуточных образов, из которых состоит готовый образ, но, в большинстве случаев их можно рассматривать как одинаковые сущности. Вот хороший материал, который посвящён подробностям внутреннего устройства образов Docker. - Для того чтобы исследовать содержимое контейнеров можно установить пакет dive.
Теперь, когда мы обсудили возможности по уменьшению размеров образов, предлагаю вашему вниманию восемь рекомендаций, касающихся уменьшения размеров образов и ускорения процесса их сборки.
Рекомендации по уменьшению размеров образов и ускорению процесса их сборки
- Используйте всегда, когда это возможно, официальные образы в качестве базовых образов. Официальные образы регулярно обновляются, они безопаснее неофициальных образов.
- Для того чтобы собирать как можно более компактные образы, пользуйтесь базовыми образами, основанными на Alpine Linux.
- Если вы пользуетесь
apt
, комбинируйте в одной инструкцииRUN
командыapt-get update
иapt-get install
. Кроме того, объединяйте в одну инструкцию команды установки пакетов. Перечисляйте пакеты в алфавитном порядке на нескольких строках, разделяя список символами. Например, это может выглядеть так:
RUN apt-get update && apt-get install -y package-one package-two package-three && rm -rf /var/lib/apt/lists/*
Этот метод позволяет сократить число слоёв, которые должны быть добавлены в образ, и помогает поддерживать код файла в приличном виде.
- Включайте конструкцию вида
&& rm -rf /var/lib/apt/lists/*
в конец инструкцииRUN
, используемой для установки пакетов. Это позволит очистить кэшapt
и приведёт к тому, что он не будет сохраняться в слое, сформированном командойRUN
. Подробности об этом можно почитать в документации. - Разумно пользуйтесь возможностями кэширования, размещая в Dockerfile команды, вероятность изменения которых высока, ближе к концу файла.
- Пользуйтесь файлом
.dockerignore
. - Взгляните на
dive
— отличный инструмент для исследования образов Docker, который помогает в деле уменьшения их размеров. - Не устанавливайте в образы пакеты, без которых можно обойтись.
Итоги
Теперь вы знаете о том, как сделать так, чтобы образы Docker быстро собирались бы, быстро загружались бы из репозиториев и не занимали бы слишком много места на компьютере. В следующий раз мы поговорим о командах Docker.
Уважаемые читатели! Сталкивались ли вы при сборке образов Docker с проблемами, связанными с неправильным использованием механизмов кэширования?
Автор: ru_vds