Использование Docker с R во многом изменило ситуацию за последние 5 лет. Общим в этом разнообразии вариантов использования является то, что образы Docker почти всегда начинаются с родительского образа. Какой родительский образ вы используете? Как вы добавляете к нему новые слои? Эти вопросы определят, насколько быстро вы сможете выполнять итерацию во время разработки, и размер окончательного изображения, которое вы отправите в производство. В этом посте я сравню использование разных родительских изображений и опишу лучшие практики. Я сосредотачиваюсь на блестящих приложениях, но большинство из этих идей применимы в целом к любому докеризованному приложению R, например изображениям для вычислительных заданий или интерфейсов.
Родительский образ-это образ, который вы определяете в директиве FROM
файла Dockerfile
. Базовое изображение имеет FROM scratch
в качестве первой строки. Базовые изображения R начинаются с родительских изображений. Например, образ R Ubuntu начинается с FROM ubuntu:focal
.
Вот четыре часто используемых родительских образа для R:
docker pull rhub/r-minimal:4.0.5
docker pull rocker/r-base:4.0.4
docker pull rocker/r-ubuntu:20.04
docker pull rstudio/r-base:4.0.4-focal
Размеры изображений довольно сильно различаются: Alpine Linux base rhub/r-minimal
- самый маленький, а rstudio/r-base
на базе Ubuntu - в 25 раз больше размера самого маленького изображения:
$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'
REPOSITORY TAG SIZE
rhub/r-minimal 4.0.5 35.3MB
rocker/r-base 4.0.4 761MB
rocker/r-ubuntu 20.04 673MB
rstudio/r-base 4.0.4-focal 894MB
Образ докера на основе Debian Linux rocker/r-base
из проекта Rocker считается самым передовым, когда речь заходит о системных зависимостях, т. е. последние версии разработки обычно доступны раньше, чем в других дистрибутивах Linux.
Два образа на основе Ubuntu Linux, рокер / r-ubuntuа
и rstudio / r-base
из проекта Rocker и RStudio предназначены для долгосрочной поддержки версий Ubuntu и используют двоичные файлы RSPM CRAN.
Образ rub/r-minimal
Docker на базе Alpine Linux из проекта hub предпочтителен из-за его небольших размеров изображений.
Я использовал Docker BuildKit
Версии Docker 18.09 или выше поставляются с новым бэкэндом конструктора с возможностью выбора под названием BuildKit. BuildKit распечатывает красивую сводку по каждому слою, включая время для слоев и общую сборку. Это общая команда сборки, которую я использовал для сравнения четырех родительских изображений:
DOCKER_BUILDKIT=1 docker build --no-cache -f $FILE -t $IMAGE .
Серверная часть набора сборки включается путем включения переменной среды DOCKER_BUILD KIT=1
. Я использую опцию --no-cache
, чтобы избежать использования кэшированных слоев, таким образом, имея справедливую оценку времени сборки (обычно вы строите только 1, а не 4). Флаг -f $FILE
позволяет строить из разных файлов, хранящихся в одной папке..
Весь используемый здесь код можно найти в этом репозитории GitHub, загляните в папку 99-images
:
Это сценарий, который я использовал для создания четырех изображений с помощью BuildKit:
# rhub/r-minimal
export IMAGE="analythium/covidapp-shiny:minimal"
export FILE="Dockerfile.minimal"
DOCKER_BUILDKIT=1 docker build --no-cache -f $FILE -t $IMAGE .
# rocker/r-base
export IMAGE="analythium/covidapp-shiny:base"
export FILE="Dockerfile.base"
DOCKER_BUILDKIT=1 docker build --no-cache -f $FILE -t $IMAGE .
# rocker/r-ubuntu
export IMAGE="analythium/covidapp-shiny:ubuntu"
export FILE="Dockerfile.ubuntu"
DOCKER_BUILDKIT=1 docker build --no-cache -f $FILE -t $IMAGE .
# rstudio/r-base
export IMAGE="analythium/covidapp-shiny:focal"
export FILE="Dockerfile.focal"
DOCKER_BUILDKIT=1 docker build --no-cache -f $FILE -t $IMAGE .
Я изменил репозиторий CRAN для образов Debian и Ubuntu Rocker, чтобы увидеть разницу во времени между установкой пакетов в двоичном виде или из исходного кода. Общее время сборки (на MacBook Pro 6 лет) было следующим:
rhub/r-minimal
: 27 минут со сборкой пакетов из исходниковrocker/r-base
: 12 минут при сборке из исходного кода, 2,9 минуты при установке бинарных пакетовrocker/r-ubuntu
: 12 минут при сборке из исходного кода, 3,2 минуты при установке бинарных пакетовrstudio/r-base
: 3,1 минуты с установкой бинарных пакетовОжидается разница между установкой бинарных пакетов и пакетов с исходным кодом. Что интересно, так это 12 минут против 27 между образами Debian / Ubuntu и минимальным образом Alpine. Стоит ли ждать?
Я получил размеры изображений из docker images
и сделал небольшой фрейм данных в R, чтобы вычислить разницу в размерах между конечным и родительским изображениями:
x = data.frame(TAG=c("minimal", "base", "ubuntu", "focal"),
PARENT_SIZE=c(35, 761, 673, 894) / 1000, # base image
FINAL_SIZE=c(222 / 1000, 1.05, 1.22, 1.38)) # final image
x$DIFF = x$FINAL_SIZE - x$PARENT_SIZE
# TAG PARENT_SIZE FINAL_SIZE DIFF
# 1 minimal 0.035 0.222 0.187
# 2 base 0.761 1.050 0.289
# 3 ubuntu 0.673 1.220 0.547
# 4 focal 0.894 1.380 0.486
Сами размеры изображений немного различались, образ RStudio Ubuntu был в 6,2 раза больше, чем минимальный образ R. Различия в размерах были аналогичными.
Размеры изображения могут быть обманчивыми. Это может не иметь большого значения, если изображения большие, если, например, у нас есть несколько изображений, разделяющих некоторые слои (то есть из родительского изображения). Объем ЦП и ОЗУ контейнеров также может не зависеть от размеров образа. Но это может повлиять на производительность «холодного старта», когда изображения загружаются на пустой сервер.
Файлы Dockerfiles и сборка образов Ubuntu и Debian были очень похожи. Время сборки и размеры изображений также были сопоставимы. Создание минимального образа на базе Alpine Linux заняло больше всего времени, но в результате получился самый маленький размер образа. Dockerfile для этой установки также сильно отличается от установки Debian / Ubuntu:
FROM rhub/r-minimal:4.0.5
RUN apk update
RUN apk add --no-cache --update-cache \
--repository http://nl.alpinelinux.org/alpine/v3.11/main \
autoconf=2.69-r2 \
automake=1.16.1-r0 \
bash tzdata
RUN echo "America/Edmonton" > /etc/timezone
RUN installr -d \
-t "R-dev file linux-headers libxml2-dev gnutls-dev openssl-dev libx11-dev cairo-dev libxt-dev" \
-a "libxml2 cairo libx11 font-xfree86-type1" \
remotes shiny forecast jsonlite ggplot2 htmltools plotly Cairo
RUN rm -rf /var/cache/apk/*
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /home/app
COPY app .
RUN chown app:app -R /home/app
USER app
EXPOSE 3838
CMD ["R", "-e", "options(tz='America/Edmonton');shiny::runApp('/home/app', port = 3838, host = '0.0.0.0')"]
Базовый образ настолько прост, что для работы ggplot2 необходимо установить часовые пояса, шрифты и устройство Cairo (см. Ограничения здесь ). Вместо apt
у вас есть apk
, и, возможно, вам придется немного потрудиться, чтобы найти все зависимости, зависящие от Alpine.
Одним из интересных аспектов этого образа является то, что вместо небольших утилит, знакомых по образам Rocker, у нас есть очень похожий сценарий installr
, который устанавливает пакеты R и системные требования:
-d
устанавливает, а затем удаляет компиляторы ( gcc
, musl-dev
, g++
), так как они обычно не нужны в конечном образе;-t
удаляются после установки пакетов R;-а
- это зависимости времени выполнения, которые необходимы для правильной работы пакетов и не удаляются из образа.Остальная часть файла Dockerfile
очень похожа на другие дистрибутивы: и пользователь Linux, копирующий файлы, открывающий порт, определяет команду entrypoint. Но как вы определяете, какие системные пакеты вам нужны?
Прежде всего, в каждом пакете указаны системные требования. Обычно это зависимости времени выполнения, которые необходимы пакету для правильной работы. Так что сначала проверьте это.
Есть как минимум две базы данных, в которых перечислены требования к пакету: одна поддерживается RStudio (поддерживает RSPM), другая - R-hub. Оба эти списка содержат системные пакеты для различных дистрибутивов Linux, macOS и Windows. Но даже с этими базами данных иногда бывает трудно отличить зависимости между сборкой и временем выполнения. Системные библиотеки времени сборки всегда именуются с постфиксом -dev
или -devel
. Прочтите виньетку пакета maketools R от Джероена Оомса, чтобы получить хорошее объяснение и предлагаемый рабочий процесс для определения зависимостей пакетов во время выполнения.
Основываясь на этих результатах и списке передовых практик Dockerfile, вот несколько предложений по улучшению взаимодействия с разработчиками и качества финальных образов Docker.
Избегайте установки пакетов, которые «приятно иметь», и не начинайте с родительских образов общего назначения, предназначенных для интерактивного использования. Образы для приложений Shiny и других веб-сервисов выигрывают от сохранения максимально компактных изображений за счет добавления абсолютно необходимых пакетов R и системных требований. Многоступенчатые сборки могут быть полезны, если они включают только необходимые артефакты.
При создании образа Docker выполняет каждую инструкцию в порядке, указанном в Dockerfile
. Docker ищет в своем кеше существующий образ, который он может использовать повторно, вместо того, чтобы создавать новый (дублированный) образ.Только инструкции RUN
, COPY
, ADD
создают слои:
RUN
используется только командная строка из файла Dockerfile
, чтобы найти соответствие из существующего образа;;ADD
и COPY
проверяется содержимое файла(файлов) в изображении и вычисляется контрольная сумма для каждого файла;ADD
и COPY
.Кеширование может быть полезно при установке зависимостей пакета R. Вот упрощенный фрагмент из этого Dockerfile
:
## install dependencies
COPY ./renv.lock .
RUN Rscript -e "renv::restore()"
## copy the app
COPY app .
Что произойдет, если мы поменяем два блока?
## copy the app
COPY app .
## install dependencies
COPY ./renv.lock .
RUN Rscript -e "renv::restore()"
Вам придется ждать, пока сборка переустановит все пакеты R всякий раз, когда файлы приложений изменятся. Это связано с тем, что после того, как кэш становится недействительным, все последующие команды Dockerfile
генерируют новые изображения вместо использования кэша.
Лучше всего упорядочить инструкции в файле Dockerfile
с менее часто изменяемых на более часто изменяемые. Это гарантирует, что кэш сборки будет повторно использоваться.
Упорядочивайте инструкции Dockerfile от менее изменяемых к наиболее часто изменяемым
Запуск контейнера с привилегиями root позволяет неограниченное использование, чего следует избегать в производственной среде. Хотя в Интернете можно найти множество примеров, когда контейнер запускается от имени пользователя root, обычно это считается плохой практикой. Используйте что-то вроде этого:
RUN addgroup --system app \
&& adduser --system --ingroup app app
WORKDIR /home/app
COPY app .
RUN chown app:app -R /home/app
USER app
Согласно этому документу, передовые методы написания файлов Docker используются все чаще и чаще после добычи более 10 миллионов файлов Docker на Docker Hub и GitHub. Однако есть еще возможности для улучшения. Именно здесь линтеры становятся полезными инструментами для статического анализа кода. Hadolint перечисляет множество правил для файлов Dockerfiles и доступен как расширение VS Code.
Этот пост начался с простой предпосылки: сравните четыре наиболее часто используемых родительских изображения для R и сделайте некоторые выводы. Это стало действительно длинным постом, но я считаю, что он дает хорошее обоснование для следующих нескольких основных предложений, которые могут значительно улучшить опыт разработчика и качество финальных образов Docker.