Авторизоваться
Аким Солянкин 29.05.2021 Опубликована

Лучшие практики для R с Docker

Использование 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 предпочтителен из-за его небольших размеров изображений.

Использование BildKit

Я использовал 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. Различия в размерах были аналогичными.

Размеры изображения могут быть обманчивыми. Это может не иметь большого значения, если изображения большие, если, например, у нас есть несколько изображений, разделяющих некоторые слои (то есть из родительского изображения). Объем ЦП и ОЗУ контейнеров также может не зависеть от размеров образа. Но это может повлиять на производительность «холодного старта», когда изображения загружаются на пустой сервер.

Образ на базе Alpine Linux

Файлы 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.

1. Минимизируйте зависимости

Избегайте установки пакетов, которые «приятно иметь», и не начинайте с родительских образов общего назначения, предназначенных для интерактивного использования. Образы для приложений Shiny и других веб-сервисов выигрывают от сохранения максимально компактных изображений за счет добавления абсолютно необходимых пакетов R и системных требований. Многоступенчатые сборки могут быть полезны, если они включают только необходимые артефакты.

2. Используйте кеширование

При создании образа Docker выполняет каждую инструкцию в порядке, указанном в Dockerfile. Docker ищет в своем кеше существующий образ, который он может использовать повторно, вместо того, чтобы создавать новый (дублированный) образ.Только инструкции RUN, COPY, ADD  создают слои:

  • для инструкций RUN используется только командная строка из файла Dockerfile, чтобы найти соответствие из существующего образа;;
  • для инструкций ADD и COPY проверяется содержимое файла(файлов) в изображении и вычисляется контрольная сумма для каждого файла;
  • время последнего изменения и последнего доступа к файлу(файлам) не учитывается в этих контрольных суммах для инструкций ADD и COPY.

3. Слои заказа

Кеширование может быть полезно при установке зависимостей пакета 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 от менее изменяемых к наиболее часто изменяемым

4. Сменить пользователя.

Запуск контейнера с привилегиями 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

5. Используйте линтер.

Согласно этому документу, передовые методы написания файлов Docker используются все чаще и чаще после добычи более 10 миллионов файлов Docker на Docker Hub и GitHub. Однако есть еще возможности для улучшения. Именно здесь линтеры становятся полезными инструментами для статического анализа кода. Hadolint перечисляет множество правил для файлов Dockerfiles и доступен как расширение VS Code.

Резюме

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

Коментарии
Авторизоваться что-бы оставить комментарий
Присоединяйся в тусовку
Наш сайт использует файлы cookie для вашего максимального удобства. Пользуясь сайтом, вы даете свое согласие с условиями пользования cookie