Использование 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.