Alpine собирает Docker билды под Python в 50 раз медленней, а образы в 2 раза тяжелей

Автор оригинала: Itamar Turner-Trauring
  • Перевод


Alpine Linux — часто рекомендованный как базовый образ для Docker`а. Вам говорят, что использование Alpine сделает ваши билды меньше, а процесс сборки быстрей.

Но если вы используете Alpine Linux для Python приложений, то он:

  • Делает ваши билды намного медленней
  • Делает ваши образы больше
  • Тратит ваше время
  • И в итоге может стать причиной ошибок в рантайме

Давайте рассмотрим почему же Alpine рекомендуют, но почему вам все же не стоит использовать его в месте с Python.

Почему люди рекомендуют Alpine?


Давайте предположим, что нам необходим gcc как часть нашего образа и мы хотим сравнить Alpine Linux vs Ubuntu 18.04, по скорости сборки и конечному размеру образа.

Для начала, скачаем два образа и сравним их размер:

$ docker pull --quiet ubuntu:18.04
docker.io/library/ubuntu:18.04
$ docker pull --quiet alpine
docker.io/library/alpine:latest
$ docker image ls ubuntu:18.04
REPOSITORY          TAG        IMAGE ID         SIZE
ubuntu              18.04      ccc6e87d482b     64.2MB
$ docker image ls alpine
REPOSITORY          TAG        IMAGE ID         SIZE
alpine              latest     e7d92cdc71fe     5.59MB

Как вы видите, базовый образ для Alpine намного меньше. Давайте теперь попробуем установить gcc и начнем с Ubuntu:

FROM ubuntu:18.04
RUN apt-get update && \
    apt-get install --no-install-recommends -y gcc && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

Написание идеальных Dockerfile выходит за рамки этой статьи

Замерим скорость сборки:

$ time docker build -t ubuntu-gcc -f Dockerfile.ubuntu --quiet .
sha256:b6a3ee33acb83148cd273b0098f4c7eed01a82f47eeb8f5bec775c26d4fe4aae

real    0m29.251s
user    0m0.032s
sys     0m0.026s
$ docker image ls ubuntu-gcc
REPOSITORY   TAG      IMAGE ID      CREATED         SIZE
ubuntu-gcc   latest   b6a3ee33acb8  9 seconds ago   150MB

Повторяем все то же самое для Alpine (Dockerfile):

FROM alpine
RUN apk add --update gcc

Собираем, смотрим на время и размер сборки:

$ time docker build -t alpine-gcc -f Dockerfile.alpine --quiet .
sha256:efd626923c1478ccde67db28911ef90799710e5b8125cf4ebb2b2ca200ae1ac3

real    0m15.461s
user    0m0.026s
sys     0m0.024s
$ docker image ls alpine-gcc
REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
alpine-gcc   latest   efd626923c14   7 seconds ago   105MB

Как и обещано, образы на базе Alpine собираются быстрей и сами по себе меньше: 15 секунда вместо 30 и размер образа 105MB против 150MB. Это довольно хорошо!

Но если мы переключимся на сборку Python приложения, то все не так радужно.

Python образ


Python приложения часто используют pandas и matplotlib. Поэтому, один из вариантов взять официальный образ на базе Debian, используя такой Dockerfile:

FROM python:3.8-slim
RUN pip install --no-cache-dir matplotlib pandas

Собираем его:

$ docker build -f Dockerfile.slim -t python-matpan.
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM python:3.8-slim
 ---> 036ea1506a85
Step 2/2 : RUN pip install --no-cache-dir matplotlib pandas
 ---> Running in 13739b2a0917
Collecting matplotlib
  Downloading matplotlib-3.1.2-cp38-cp38-manylinux1_x86_64.whl (13.1 MB)
Collecting pandas
  Downloading pandas-0.25.3-cp38-cp38-manylinux1_x86_64.whl (10.4 MB)
...
Successfully built b98b5dc06690
Successfully tagged python-matpan:latest

real    0m30.297s
user    0m0.043s
sys     0m0.020s

Получаем образ размером в 363MB.
Получится у нас лучше с Alpine? Давайте попробуем:

FROM python:3.8-alpine
RUN pip install --no-cache-dir matplotlib pandas

$ docker build -t python-matpan-alpine -f Dockerfile.alpine .                                 
Sending build context to Docker daemon  3.072kB                                               
Step 1/2 : FROM python:3.8-alpine                                                             
 ---> a0ee0c90a0db                                                                            
Step 2/2 : RUN pip install --no-cache-dir matplotlib pandas                                                  
 ---> Running in 6740adad3729                                                                 
Collecting matplotlib                                                                         
  Downloading matplotlib-3.1.2.tar.gz (40.9 MB)                                               
    ERROR: Command errored out with exit status 1:                                            
     command: /usr/local/bin/python -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/
tmp/pip-install-a3olrixa/matplotlib/setup.py'"'"'; __file__='"'"'/tmp/pip-install-a3olrixa/matplotlib/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base /tmp/pip-install-a3olrixa/matplotlib/pip-egg-info                              

...
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.
The command '/bin/sh -c pip install matplotlib pandas' returned a non-zero code: 1

Что происходит?

Alpine не поддерживает wheels


Если вы посмотрите на билд, который базируется на Debian, то вы увидите, что он скачивает matplotlib-3.1.2-cp38-cp38-manylinux1_x86_64.whl.

Это бинарник для wheel. Alpine же скачивает исходники `matplotlib-3.1.2.tar.gz`, так как он не поддерживает стандартный wheels.

Почему? Большинство Linux дистрибутивов используют GNU версию (glibc) стандартной библиотеки C, который по факту необходим каждой программе написанной на C, включая Python. Но Alpine использует `musl`, а так как те бинарники предназначены для `glibc`, они попросту не вариант.

Поэтому, если вы используете Alpine, вам необходимо компилировать весь код, написанный на C, в каждом пакете Python.

Ах, да, список всех таких зависимостей которые, нужно компилировать придется искать самим.
В данном случае получаем такое:

FROM python:3.8-alpine
RUN apk --update add gcc build-base freetype-dev libpng-dev openblas-dev
RUN pip install --no-cache-dir matplotlib pandas

И время билда занимает…

… 25 минут 57 секунд! А размер образа 851MB.

Образы на базе Alpine собираются намного дольше, сами по себе они большего размера и вам еще нужно искать все зависимости. Можно конечно уменьшить размер сборки используя multi-stage builds но это означает, что нужно проделать еще больше работы.

Это еще не все!

Alpine может быть причиной неожиданных багов в рантайме


  • В теории musl совместим с glibc, но на практике различия могут стать причиной многих проблем. И если они будут, то наверняка неприяные. Вот некоторые проблемы, которые могут возникнуть:
  • Alpine по умолчанию имеет меньший размер стека потока, что может привести к ошибкам в Python
  • Некоторые пользователи обнаружили, что Python приложения работают медленней из-за того как, musl выделяет память (отличается от glibc).
  • Один из пользователей обнаружил ошибку при форматировании даты

Наверняка эти ошибки уже исправили, но кто знает сколько их еще.

Не используйте образы Alpine для Python


Если не хотите возиться с большими и долгими билдами, поиском зависимостей и потенциальными ошибками — не используйте Alpine Linux в качестве базового образа. Сhoosing a good base image.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 27

    +14
    Переводите тогда уже и комментарии:

    First «the Dockerfiles in this article are not examples of best practices»

    Well, that's a big mistake. Of course if you don't follow best practices you won't get the best results. In these examples the author doesn't even follow the basic recommendations from the Docker Alpine image page. Ex, use «apk add --no-cache PACKAGE». When you're caching apt & apk, of course the image is going to be a ton larger. On the flip side he does basically exactly that to clean up ubuntus apt cache.

    The real article should have been «should you use alpine for every python/docker project?» and the answer is «No». If you're doing something complicated that requires a lot of system libs, like say machine learning or imagine manipulation — don't use Alpine. It's a pain. On the flip side if all you need is a small flask app, Alpine is a great solution.
      +3
      Увы, для production обычно нужен gunicorn/uwsgi/gevent/uvloop и доступ к ресурсам (psycopg2-binary/hiredis), а они опять таки — требуют wheels и некоторые библиотеки (libc-compat, libev).
      Потому — multi-stage с python:3 и python:3-slim будут лучшим доступным решением.

      ИМХО: Батарейки — главное преимущество Python, и главный его недостаток.
      +6
      Alpine не поддерживает wheels

      Он поддерживает wheels, это manylinux1 не поддерживается.

        +6
        Мне кажется изначально довольно странным решением — брать максимально урезанный и ограниченный образ и собирать на нем расфуфыренный библиотеками питон.
          0

          Нет, это проблески надежды, что оптимальное решение для одного случая будет оптимальным для всего. И, да, эльпайн переоценен. Хочешь минималка — бери FROM scratch и наполняй его сам

          +5

          ну это не новость, что из-за musl в Alpine может быть много проблем.


          Большой привет некоторым программистам на Си, беззаботно пихающим зависимости от glibc и т.п. в свои проекты, даже когда им вполне хватило бы POSIX.

            +8
            Заголовок написан так, как-будто виноват в чем-то Alpine, а не авторы python:3.8-alpine
              0

              Здесь правых нет. Виноваты все

              +4
              Давно уже не использую alpine для чего-то, что требует докерфайл больше пары строк.
              Просто потому что задолбаешься выяснять как поставить какие-то библиотеки для него, а лишние 50 Мб роли особо не играют.
                0

                Зато стоимость разработки вырастает, т.к. эти зависимости надо искать. А потом поддерживать в актуальном состоянии, когда нужно образ пересобирать.
                Плюс проблема с долгой сборкой или установкой numpy/pandas/scikit не надуманна

                +1

                Вместо
                RUN apk --update add gcc build-base
                Лучше использовать
                RUN apk --no-cache add gcc build-base


                И после установки зависимостей python можно удалить часть пакетов, не нужных в runtime.


                Может будет лучше)

                  +4
                  И после установки зависимостей python можно удалить часть пакетов, не нужных в runtime.
                  Для этого, кстати, у apk есть удобная опция --virtual:
                  RUN apk add --no-cache --virtual .build-deps gcc freetype-dev libpng-dev openblas-dev \
                      && ./configure \
                      && make \
                      && ... \
                      && apk del .build-deps       
                  


                    0

                    Именно. Это тот самый комментарий, который показывает, что автор изначальной статьи просто не разобрался как в alpine устанавливать питоновские либы. PS. Использую alpine с python больше 3 лет, с правильным подходом проблем ни с чем нк возникает.

                      0
                      с правильным подходом вообще всегда проблем нет :)
                        +1
                        Только при этом ты жертвуешь временем сборки, или размером образа (будет слой, который содержит весь build-deps). В случае с alpine — удобно работать только с статически линованными приложениями (привет Golang/JVM!), или приходится городить multistage.
                        Для себя проблему решили переборкой всех зависимостей и PYPI-proxy.

                        Как мне кажется — проблема не в alpine, а в отсутствии платформо-зависимых пакетов для него. ИМХО.
                          0

                          Касательно времени сборки — да, верное замечание, что если бы были готовые бинари, то мы не "попадали" бы на время сборки. Кстати, время сборки отчасти можно победить путем сборки СВОЕГО базового образа из alpine + самые часто используемые и долго собираемые зависимости. А потом уже в самом проекте докидывать из requirements.txt все остальное. Датасайентисты и инженеры так и делают. Ну, и версии пакетов зафиксированы — это тоже благо

                          0

                          Глупость говорите. Единственное с чем согласен — эльпайн требует отдельного подхода. Но это точно нельзя назвать 'правильным'.
                          Я уж не говорю, когда занимаетесь ml — попробуйте тот же torch или tensorflow водрузить на эльпайн. А ещё весело, когда у тебя проект состоит из 20 образов, разработчики все собрали из чего попало — что из эльпайна, что из убунту. В результате получается, что если все образы привести к единой общей базе, то они и качаются быстрее, и суммарно меньше места занимают, чем если они собраны из разных базовых.

                        0

                        Проще мультистейджем, ей-Богу

                        0

                        Не смотря на некоторые недочёты в использовании apk и в целом использования, спасибо за этот перевод. Я как-то не задумывался, что, возможно, некоторые тормоза и непонятного происхождения ошибки могут быть из-за musl. Думаю это отличный повод для исследования.

                          +1

                          Одна из проблем, с которой мы столкнулись: https://github.com/jrottenberg/ffmpeg/issues/104
                          Было ОЧЕНЬ больно, т.к. это не бинарная история, что нечто либо работает, либо не работает, а ошибка проявлялась только при определенном наборе аргументов ffmpeg (facepalm)

                          +1
                          Ой ну прямо так часто вы будете пересобирать все библиотеки. Докер билд кэш рулит.
                            –4
                            Зачем здесь перевод этой убогой статьи от восьмиклассника, который не умеет собирать образы на alpine на достаточном уровне, чтобы приводить такие бенчи?
                            Выше в комментариях уже отметили.
                              +3

                              Вы хабр с лором не перепутали случаем? Что не так в статье кроме некоторых формулировок? wheels есть, собранные с musl? Нет. Ну вот и всё. Восьмиклассник он или нет, роли не играет, если вам нужен какой-нибудь SciPy, вы его задолбётесь собирать со всеми этими openblas/atlas/fortran как по времени так и по всяким траблам, которые могут внезапно вылезти.


                              Проект manylinux стал прорывом в своё время, он сильно облегчил жизнь людям, использующим Python-пакеты с C-extensions. Не стоит это недооценивать.

                                –2

                                У alpine есть проблемы с библиотеками. На этом все. Всё остальное что описал автор оригинала — проблемы автора, потому что он не умеет пользоваться докером и собирать образы правильно.

                              +1
                              Была такая проблема — куча докер контейнеров на alpine с пандой. Решение было несложное — подготовить образ с установленными numpy и pandas и использовать его как базовый для всех этих пакетов. И всё, CI снова быстр.
                                +3

                                Я похожую статью писал в июле 2018 года:
                                Так ли мал Alpine 3.8 Docker для Python 3 runtime
                                https://habr.com/ru/post/415513/

                                  0
                                  Если использовать двухшаговую сборку, то и продакш-контейнеры будут не такими тяжёлыми, и сборка с кешем будет практически мгновенной.
                                  Но это если у вас не слишком много разной тяжести в духе pandas'а и т.п.

                                  Я использую вот такое для вебсервиса на sanic, в котором используются допом aiohttp, asyncpg, honeybadger, Jinja2, Pillow. Если pillow не нужен, то, например, jpeg-dev будет не нужен, контейнер будет меньше.

                                  FROM python:3.7.5-alpine3.10 as base
                                  
                                  FROM base as builder
                                  RUN mkdir /install
                                  WORKDIR /install
                                  RUN apk add --no-cache python3-dev \
                                                         build-base \
                                                         libc6-compat \
                                                         libffi-dev \
                                                         zlib-dev \
                                                         jpeg-dev \
                                                         linux-headers
                                  RUN pip3 install --upgrade pip
                                  COPY requirements.txt ./
                                  RUN pip3 install --prefix=/install -r requirements.txt
                                  
                                  FROM base
                                  RUN apk add --no-cache bash jpeg-dev && \
                                      rm -r /tmp
                                  EXPOSE 80
                                  WORKDIR my_awesome_app
                                  COPY --from=builder /install /usr/local
                                  COPY my_awesome_app ./
                                  RUN pip3 freeze
                                  CMD ["python3", "app.py"]
                                  

                                  Продакшн-контейнер получается 160MB.

                                  Собрать мой стек на ubuntu/debian за быстро не получилось, поэтому, увы, без сравнения.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое