Сегодняшняя статья будет рассмотрена в срезе использования runtime системы в докере для Python 3.6.X версий, с различным составом пакетов pip. А так же мы соберём самый новый Python 3.7 в Alpine.
В конце статьи будет представлен размер образа image, занимаемый на диске, в зависимости от состава пакетов pip и произведено сравнение между дистрибутивами Alpine 3.8, Debian 9, Fedora 28.
Итак, приступим: для тестирования дистрибутивы выбраны. Будем собирать следующие docker images:
- Система, ее обновление. И Python3 с обновлённым pip (10 версии)
- п.1 + tornado cython
- п.2 + numpy-scipy
- п.3 + pillow bokeh pandas websocket-client
В результате даных заливок, мы получим различные версии: Python без пакетов, Python с web сервером, Python с пакетами для обработки многопоточных математических вычислений, Python с «графическим» стеком и работы с данными.
Итак, результирующие файлы для Debian и Fedora будут выглядеть у нас так:
Debian
FROM debian
RUN apt-get update -y && apt-get install python3-pip -y && pip3 install pip --upgrade && apt-get clean
RUN pip3 install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow
Fedora
FROM fedora
RUN dnf update -y && dnf install libstdc++ -y && dnf clean all && pip3 install --upgrade pip && python3 --version
RUN pip3 install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow
А вот с Alpine 3.8 пока заминка. Официально на момент написания статьи он ещё на вышел, а посмотреть, то хочется:-). Поэтому нам понадобиться их образ системы:
dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64
И мы соберём свой Alpine from Sratch:
github.com/gliderlabs/docker-alpine/tree/master/versions/library-3.8/x86_64
Выкачиваем файловую систему Alpine 3.8:
curl dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-minirootfs-3.8.0_rc10-x86_64.tar.gz >alpine3.8.tar.gz
Создаём свой докер файл:
FROM scratch
ADD alpine3.8.tar.gz /
ENV RELEASE="v3.8"
ENV MIRROR="http://dl-cdn.alpinelinux.org/alpine"
ENV PACKAGES="alpine-baselayout,busybox,alpine-keys,apk-tools,libc-utils"
ENV TAGS=(alpine:3.8)
Затем копипастим и добавляем в этот файл борку Python 3.6 со страницы github.com/docker-library/python/blob/master/3.6/alpine3.7/Dockerfile
не забыв удалить или закомментировать строку FROM alpine:3.7
И пробуем создать образ с Alpine 3.8 и Python на борту:
$docker build -t alpine3.8 .
#результат работы (последние строки)
(1/2) Purging .fetch-deps (0)
(2/2) Purging libressl (2.7.4-r0)
Executing busybox-1.28.4-r0.trigger
OK: 33 MiB in 45 packages
+ python get-pip.py --disable-pip-version-check --no-cache-dir 'pip==10.0.1'
Collecting pip==10.0.1
Downloading https://files.pythonhosted.org/packages/0f/74/ecd13431bcc456ed390b44c8a6e917c1820365cbebcb6a8974d1cd045ab4/pip-10.0.1-py2.py3-none-any.whl (1.3MB)
Collecting setuptools
Downloading https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl (567kB)
Collecting wheel
Downloading https://files.pythonhosted.org/packages/81/30/e935244ca6165187ae8be876b6316ae201b71485538ffac1d718843025a9/wheel-0.31.1-py2.py3-none-any.whl (41kB)
Installing collected packages: pip, setuptools, wheel
Successfully installed pip-10.0.1 setuptools-39.2.0 wheel-0.31.1
+ pip --version
pip 10.0.1 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)
+ find /usr/local -depth '(' '(' -type d -a '(' -name test -o -name tests ')' ')' -o '(' -type f -a '(' -name '*.pyc' -o -name '*.pyo' ')' ')' ')' -exec rm -rf '{}' +
+ rm -f get-pip.py
---> f7439dca5f31
Removing intermediate container e1fcd1c74873
Step 17/17 : CMD python3
---> Running in a4dc6dfa5184
---> 6924b206c6b9
Removing intermediate container a4dc6dfa5184
Successfully built 6924b206c6b9
Результаты первого шага установка только Python (docker images --all):
- Debian 9 / 513 MB
- Fedora 28 / 387 MB
- Alpine 3.8 / 82.2 MB
Шаг 2. Установка cython и tornado
Начинаем добавлять пакеты pip. Первым установим cython и tornado. Для Debian и Fedora пакеты ставятся без ошибок, а вот Alpine падает с ошибкой:unable to execute 'gcc': No such file or directory
error: command 'gcc' failed with exit status 1
Придется гуглить и потом уже добавлять библиотеки сборки в Alpine, чтобы pip успешно собрал их из исходного текста. Затем запускать сборку докера снова, затем опять искать зависимости, читать форумы stackoverflow и issues в github и ждать и ждать и ждать.
Поскольку в следующих шагах мы начнём добавлять математические и графические библиотеки в наш образ runtime Python, и чтобы слишком не увеличивать текст данной статьи, я приведу финальные зависимости для Alpine linux:
apk add --no-cache --virtual .build-deps gfortran build-base openblas-dev bzip2-dev coreutils dpkg-dev dpkg expat-dev gcc gdbm-dev libc-dev libffi-dev libnsl-dev libressl libressl-dev libtirpc-dev linux-headers make ncurses-dev pax-utils readline-dev sqlite-dev tcl-dev tk tk-dev xz-dev zlib-dev libxml2-dev libxslt-dev musl-dev libgcc curl jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev && ln -s /usr/include/locale.h /usr/include/xlocale.h
- Debian 9 / 534 MB
- Fedora 28 / 407 MB
- Alpine 3.8 / 144 MB
Шаг 3. Добавляем математику numpy scipy
- Debian 9 / 763 MB
- Fedora 28 / 626 MB
- Alpine 3.8 / 404 MB MB
Шаг 4. Добавляем графический стек websocket-client pytest pandas bokeh pillow
FROM scratch
ADD alpine3.8.tar.gz /
CMD ["/bin/sh"]
ENV RELEASE="v3.8"
ENV MIRROR="http://dl-cdn.alpinelinux.org/alpine"
ENV PACKAGES="alpine-baselayout,busybox,alpine-keys,apk-tools,libc-utils"
#ENV BUILD_OPTIONS=(-b -s -t UTC -r $RELEASE -m $MIRROR -p $PACKAGES)
ENV TAGS=(alpine:3.8)
#
# NOTE: THIS DOCKERFILE IS GENERATED VIA "update.sh"
#
# PLEASE DO NOT EDIT IT DIRECTLY.
#
#https://github.com/docker-library/python/blob/master/3.6/alpine3.7/Dockerfile
# ensure local python is preferred over distribution python
ENV PATH /usr/local/bin:$PATH
# http://bugs.python.org/issue19846
# > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
ENV LANG C.UTF-8
# install ca-certificates so that HTTPS works consistently
# the other runtime dependencies for Python are installed later
RUN apk add --no-cache ca-certificates
ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
ENV PYTHON_VERSION 3.6.6
RUN set -ex && apk add --no-cache --virtual .fetch-deps gnupg libressl tar xz && wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" && wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" && export GNUPGHOME="$(mktemp -d)" && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" && gpg --batch --verify python.tar.xz.asc python.tar.xz && rm -rf "$GNUPGHOME" python.tar.xz.asc && mkdir -p /usr/src/python && tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz && rm python.tar.xz && apk add --no-cache --virtual .build-deps bzip2-dev coreutils dpkg-dev dpkg expat-dev gcc gdbm-dev libc-dev libffi-dev libnsl-dev libressl libressl-dev libtirpc-dev linux-headers make ncurses-dev pax-utils readline-dev sqlite-dev tcl-dev tk tk-dev xz-dev zlib-dev # add build deps before removing fetch deps in case there's overlap
&& apk del .fetch-deps && cd /usr/src/python && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" && ./configure --build="$gnuArch" --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip && make -j "$(nproc)" # set thread stack size to 1MB so we don't segfault before we hit sys.getrecursionlimit()
# https://github.com/alpinelinux/aports/commit/2026e1259422d4e0cf92391ca2d3844356c649d0
EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000" && make install && runDeps="$( scanelf --needed --nobanner --format '%n#p' --recursive /usr/local | tr ',' '\n' | sort -u | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' )" && apk add --virtual .python-rundeps $runDeps && apk del .build-deps && find /usr/local -depth \( \( -type d -a \( -name test -o -name tests \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \) -exec rm -rf '{}' + && rm -rf /usr/src/python
# make some useful symlinks that are expected to exist
RUN cd /usr/local/bin && ln -s idle3 idle && ln -s pydoc3 pydoc && ln -s python3 python && ln -s python3-config python-config
# if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 10.0.1
RUN set -ex; apk add --no-cache --virtual .fetch-deps libressl; wget -O get-pip.py 'https://bootstrap.pypa.io/get-pip.py'; apk del .fetch-deps; python get-pip.py --disable-pip-version-check --no-cache-dir "pip==$PYTHON_PIP_VERSION" ; pip --version; find /usr/local -depth \( \( -type d -a \( -name test -o -name tests \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \) -exec rm -rf '{}' +; rm -f get-pip.py
apk add --no-cache --virtual .build-deps gfortran build-base openblas-dev bzip2-dev coreutils dpkg-dev dpkg expat-dev gcc gdbm-dev libc-dev libffi-dev libnsl-dev libressl libressl-dev libtirpc-dev linux-headers make ncurses-dev pax-utils readline-dev sqlite-dev tcl-dev tk tk-dev xz-dev zlib-dev libxml2-dev libxslt-dev musl-dev libgcc curl jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev && ln -s /usr/include/locale.h /usr/include/xlocale.h && pip install cython tornado websocket-client pytest numpy pandas scipy bokeh pillow && apk del .build-deps && apk add --no-cache libstdc++ openblas zlib jpeg openjpeg tiff tk tcl musl libxml2 libxslt xz zlib libstdc++ openblas && pip list
CMD ["python3"]
- Debian 9 / 905 MB
- Fedora 28 / 760 MB
- Alpine 3.8 / 650 MB
В качестве бонуса, попробуем в Alpine 3.8 скомпилировать ещё не вышедший для докера Python 3.7.
Новая версия Python 3.7 представлена 27 июня 2018 года
Код компиляции возьмём из:
github.com/docker-library/python/tree/bbbc37fff3411a34deef30dd9b34dc938fe7b134/3.7-rc/alpine3.7
Package Version
---------------- -------
atomicwrites 1.1.5
attrs 18.1.0
bokeh 0.13.0
Cython 0.28.3
Jinja2 2.10
MarkupSafe 1.0
more-itertools 4.2.0
numpy 1.14.5
packaging 17.1
pandas 0.23.1
Pillow 5.1.0
pip 10.0.1
pluggy 0.6.0
py 1.5.4
pyparsing 2.2.0
pytest 3.6.2
python-dateutil 2.7.3
pytz 2018.4
PyYAML 4.1
scipy 1.1.0
setuptools 39.2.0
six 1.11.0
tornado 5.0.2
websocket-client 0.48.0
wheel 0.31.1
---> 3d18b8c27cd9
Removing intermediate container f546e004b79f
Step 18/18 : CMD python3
---> Running in 23c5aea50a0d
---> d5385e425064
Removing intermediate container 23c5aea50a0d
Successfully built d5385e425064
real 41m17,619s
Размер Alpine 3.8 с Python 3.7 с текущим списком пакетов pip 656 MB
Итоги
Python
- Debian 9 / больше в 6.24х / +430 Mb
- Fedora 28 / больше в 4,7х / +304 Mb
Python tornado cython
- Debian 9 / больше в 3,71х / +390 Mb
- Fedora 28 / больше в 2,82x / +263 Mb
Python tornado cython numpy scipy
- Debian 9 / больше в 1,88 раз / +359 Mb
- Fedora 28 / больше в 1.54 раз / +222 Mb
Python tornado cython numpy scipy websocket-client pytest pandas bokeh pillow
- Debian 9 / больше в 1,39 раз / +255 Mb
- Fedora 28 / больше в 1.16 раз / +110 Mb
При использовании пустого runtime Python, дистрибутив Alpine linux лидер по минимальному размеру. При увеличени количества библиотек pip до tornado+cython+numpy+scipy Alpine все ещё дает заметную экономию в размере на жёстком диске. Одако как только в пакетах появляются графические утилиты для работы с данными для Python, разница практически исчезает.
При большом количестве графических пакетов, оптимальнее выбрать дистрибутив Fedora, чем заниматься компиляцией пакетов в Alpine (компиляция может длиться 1-2 часа), и в результате получить экономию в один или два десятка процентов места на жёстком диске.
UPDATE1: Тестирование проводилось на Fedora Atomic Host: release 28 (Twenty Eight), Version: 2018.5
UPDATE2: Проверка занимаемого места на диске, была проведена по данному билду hub.docker.com/r/flytrue/python-runtime-docker/tags
$docker pull flytrue/python-runtime-docker:alpine-full
$docker save flytrue/python-runtime-docker:alpine-full -o alpine-full.tar
$ls -lh alpine-full.tar
-rw-------. 1 fedora fedora 631M июн 29 08:38 alpine-full.tar
$ docker images --all|grep alpine
docker.io/flytrue/python-runtime-docker alpine-full f37154658671 19 hours ago 650 MB
Комментарии (9)
kkirsanov2
28.06.2018 15:53А почему вы не удаляете из сборки *-dev пактеы, которые ненужны после компиляции?
Сам делаю вот так: разбиваю пакеты на две группы. перечисленные в dev удаляю после установки pip -r
RUN apk update && \cat /opt/app/requirements/apk.txt | xargs apk add --no-cache && \cat /opt/app/requirements/apk_dev.txt | xargs apk add --no-cache && \python3 -m pip install -r /opt/app/requirements/python-requirements.txt && \cat /opt/app/requirements/apk_dev.txt | xargs apk del
Удаление тяжелых тяжелых пакетов типа mariadb-dev сильно экономит место и заметно ускоряет деплой.
Renatk Автор
28.06.2018 16:07Именно так, как вы написали и происходит: установка сборочных инструментариев, сборка, удаление сборочных инструментариев.
apk add --no-cache --virtual .build-deps .... apk del .build-deps
Jon7
28.06.2018 18:52А Вы уверены что "apk del .build-deps" удаляет и фактически освобождает место? У меня возникли сомнения потому, что докер использует overlayfs2 за которым стоит union mount. https://docs.docker.com/glossary/?term=overlay%20storage%20driver
То есть нужно точно знать что все действия развертыванию, компиляции с её временными файлами, деплою и удалению инструментария происходят в одном и том же обязательно последнем слое фс.
В противном случае никакого фактического освобождения места может не оказаться.
darkslave
28.06.2018 20:09У Debian есть slim сборка, в которой вырезаны лишние зависимости и утилиты. Аналогичные сборки есть у дистрибутивов на основе debian.
К примеру образ на alpine python:3.7-rc-alpine весит 94,5Мб, а образ на debian 9 slim python:3.7-rc-slim всего 143Мб, что не многим больше.
Ipeacocks
28.06.2018 20:12Ну в принципе вы не открыли Америку. Если много зависимостей, то Альпайн — это ещё та заноза!
Stas911
29.06.2018 16:43Список зависимостей внушает. Страшно представить сколько пришлось все это собирать.
На тему размера — приятно, конечно, иметь образ поменьше, но во всем нужен баланс: если посчитать стоимость времени инженера и стоимость стораджа — может оказаться, что и фиг с ним, с местом.Renatk Автор
29.06.2018 16:56У меня Alpine в облаке собирался приблизительно за один час.
Бесплатный hub.docker.com собирает около 2х часов
zloddey
А если применить multi-stage build? Один образ Alpine для компиляции зависимостей, другой для рантайма. Конечно, это ещё больше возни, чем предложенный вариант, но экономия места всё же будет.
Renatk Автор
Согласен, ничто не мешает использовать данный способ. Но в данных примерах (официальные сборки Alpine 3.7 для Python 3.6) вычищают все установленные и временные зависимости. Поэтому цель была сравнить пустой Python и дополненный пакетами.