Оптимизация сборки с помощью управления кэшем

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

Наиболее важной функцией для повышения скорости сборки является кэш сборки Docker.

Как работает кэш сборки?

Понимание кэша сборок Docker поможет вам писать более качественные Docker-файлы, что приведёт к ускорению сборок.

Посмотрите на следующий пример, в котором показан простой Dockerfile для программы, написанной на языке C.

# syntax=docker/dockerfile:1
FROM ubuntu:latest

RUN apt-get update && apt-get install -y build-essentials
COPY main.c Makefile /src/
WORKDIR /src/
RUN make build

Каждая оператор в этом Dockerfile переводится (примерно) как слой в конечном образе. Слои образа можно представить в виде стопки, где каждый слой добавляет больше содержимого поверх предыдущих слоев:

Image layer diagram showing the above commands chained together one after the other

При каждом изменении слоя данный слой нужно будет создавать заново. Например, предположим, что вы внесли изменения в свою программу в файле main.c. После этого изменения команда COPY должна быть запущена снова, чтобы данные изменения появились в образе. Другими словами, Docker аннулирует кэш для этого слоя.

Image layer diagram, but now with the link between COPY and WORKDIR marked as invalid

Если слой изменяется, это затрагивает и все последующие слои. Когда слой с командой COPY становится недействительным, все последующие слои также должны быть запущены заново:

Image layer diagram, but now with all links after COPY marked as invalid

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

Примечание

Предположим, что в вашем Dockerfile есть шаг RUN apt-get update && apt-get upgrade -y для обновления всех пакетов программного обеспечения в вашем образе на базе Debian до последней версии.

Это не означает, что создаваемые вами образы всегда актуальны. Если пересобрать образ на том же хосте неделю спустя, вы получает те же пакеты, что и раньше. Единственный способ принудительно пересобрать образ — убедиться, что слой перед ним изменился, или очистить кэш сборки с помощью docker builder prune .

Как я могу эффективно использовать кэш?

Теперь, когда вы понимаете, как работает кэш, вы можете начать использовать его в своих интересах. Хотя кэш будет автоматически работать на любом docker build, который вы запускаете, вы можете часто рефакторить свой Dockerfile, чтобы добиться ещё большей производительности. Данные оптимизации могут сэкономить драгоценные секунды (или даже минуты) на ваших сборках.

Закажите слои

Размещение команд в вашем Dockerfile в логическом порядке — отличное место для начала. Поскольку изменение вызывает перестройку последующих шагов, старайтесь, чтобы дорогостоящие шаги появлялись в начале Dockerfile. Шаги, которые часто меняются, должны появляться в конце Dockerfile, чтобы не вызывать пересборку слоев, которые не изменились.

Рассмотрим следующий пример. Фрагмент Dockerfile, запускающий сборку JavaScript из исходных файлов в текущем каталоге:

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY . .          # Copy over all files in the current directory
RUN npm install   # Install dependencies
RUN npm build     # Run build

Данный Dockerfile довольно неэффективен. Обновление любого файла приводит к переустановке всех зависимостей каждый раз, когда вы собираете образ Docker, даже если зависимости не изменились с прошлого раза!

Вместо этого команду COPY можно разделить на две части. Сначала копирует файлы управления пакетами (в данном случае package.json и yarn.lock ). Затем устанавливает зависимости. Наконец, копирует исходный код проекта, который подвержен частым изменениям.

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY package.json yarn.lock .    # Copy package management files
RUN npm install                  # Install dependencies
COPY . .                         # Copy over project files
RUN npm build                    # Run build

Благодаря установке зависимостей в более ранние слои Dockerfile, нет необходимости перестраивать данные слои при изменении файла проекта.

Уменьшайте количество слоев

Одна из лучших вещей, которые вы можете сделать для ускорения сборки образа, — это просто поместить меньше деталей в вашу сборку. Меньшее количество деталей означает, что кэш остаётся меньшим, но также и то, что должно быть меньше вещей, которые могут устареть и потребовать восстановления.

Чтобы начать, вот несколько советов и рекомендаций:

Не включайте ненужные файлы

Будьте внимательны к тому, какие файлы вы добавляете к образу.

Выполнение команды типа COPY . /src приведёт к COPY всему вашему созданию контекста в образ. Если в текущем каталоге есть журналы, артефакты менеджера пакетов или даже предыдущие результаты сборки, они также будут скопированы. Это может сделать ваш образ больше, чем нужно, тем более что данные файлы обычно не нужны.

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

COPY ./src ./Makefile /src

В отличие от этого:

COPY . /src

Вы также можете создать файл .dockerignore и с его помощью указывает, какие файлы и каталоги следует исключить из контекста сборки.

Используйте менеджер пакетов с умом

Большинство сборок образов Docker подразумевают использование менеджера пакетов для установки программного обеспечения в образ. Debian имеет apt, Alpine имеет apk, Python имеет pip, NodeJS имеет npm, и так далее.

При установке пакетов будьте внимательны. Убедиться, что устанавливаете только те пакеты, которые вам нужны. Если вы не собираетесь их использовать, не устанавливайте их. Помните, что это может быть разный список для локальной среды разработки и производственной среды. Вы можете использовать многоэтапные сборки для эффективного разделения данных пакетов.

Используйте специальный

Команда RUN поддерживает специализированный кэш, который вы можете использовать, когда вам нужен более тонкий кэш между запусками. Например, при установке пакетов вам не всегда нужно каждый раз получать все пакеты из Интернета. Вам нужны только те, которые изменились.

Чтобы решить эту проблему, вы можете использовать RUN --mount type=cache . Например, для образа на базе Debian вы можете использовать следующее:

RUN \
    --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y git

Использование явного кэша с флагом --mount сохраняет содержимое каталога target между сборками. Когда данный слой нужно пересобрать, то будет использоваться кэш apt в /var/cache/apt .

Минимизировать количество слоев

Сохранение небольшого количества слоев — это хороший первый шаг, и логичным следующим шагом будет уменьшение количества слоев, которые у вас есть. Меньшее количество слоев означает, что вам придется меньше перестраивать, если что-то в вашем Dockerfile изменится, поэтому сборка будет завершена быстрее.

В следующих разделах рассмотрены некоторые советы, которые можно использовать, чтобы свести количество слоев к минимуму.

Используйте подходящее базовое образ

Docker предоставляет более 170 готовых официальные образов почти для каждого распространенного сценария разработки. Например, если вы создаёте веб-сервер Java, используйте специальный образ, такой как openjdk. Даже если нет официального образа для того, что вам может понадобиться, Docker предоставляет образы из проверенные издатели и партнеры с открытым исходным кодом, которые могут помочь вам в пути. Сообщество Docker часто также создаёт сторонние образы для использования.

Использование официальных образов экономит ваше время и обеспечивает актуальность и безопасность по умолчанию.

Используйте многоступенчатые сборки

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

При многоэтапной сборке используются две или более команды FROM. Следующий пример иллюстрирует создание простого веб-сервера, который обслуживает HTML из вашего каталога docs в Git:

# syntax=docker/dockerfile:1

# stage 1
FROM alpine as git
RUN apk add git

# stage 2
FROM git as fetch
WORKDIR /repo
RUN git clone https://github.com/your/repository.git .

# stage 3
FROM nginx as site
COPY --from=fetch /repo/docs/ /usr/share/nginx/html

Эта сборка имеет 3 стадии: git , fetch и site . В этом примере git является базой для этапа fetch. Он использует флаг COPY --from для копирования данных из каталога docs/ в каталог сервера Nginx.

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

По возможности объединяйте команды вместе.

Большинство команд Dockerfile, и в частности команда RUN, часто могут быть объединены. Например, вместо того, чтобы использовать RUN следующим образом:

RUN echo "the first command"
RUN echo "the second command"

Можно запускать обе данных команды внутри одного RUN , что означает, что они будут использовать один и тот же кэш! Этого можно добиться, используя оператор оболочки && для запуска одной команды за другой:

RUN echo "the first command" && echo "the second command"
# or to split to multiple lines
RUN echo "the first command" && \
    echo "the second command"

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

RUN <<EOF
set -e
echo "the first command"
echo "the second command"
EOF

(Обратите внимание на команду set -e для выхода сразу после неудачи любой команды, вместо продолжения).

Другие ресурсы

Подробнее об использовании кэша для эффективной сборки см: