用Alpine会让Python Docker的构建慢50倍

当你为Docker镜像选择基础镜像时,Alpine Linux可能被推荐。有人告诉你,用Alpine将使你的镜像更小,并能加快你的builds。如果你正在用Go,这无疑是个合理的建议。

但如果你使用Python,Alpine Linux会经常:

  1. 让你的构建更慢
  2. 让你的镜像更大
  3. 浪费你的时间
  4. 偶尔引入一些令人费解的运行时Bug

让我们看看为什么人们推荐使用Alpine,以及为什么不应该在Python应用程序中使用它。

为什么人们推荐使用Alpine

假设我们需要安装gcc作为镜像构建的一部分,并且我们想看看Alpine Linux在构建时间和镜像大小方面与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并不是最佳实践的示例,因为增加的复杂性会掩盖本文的主要观点。因此,如果你打算用Docker在生产环境中运行你的Python应用程序,这里有两种方法可以应用最佳实践:

如果你想DIY:一个详细的清单、例子和参考资料

如果你想要尽快拥有一个基本够用的设置:一个模板和为你实现的最佳实践

然后,我们可以构建并记录时间:

$ 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镜像

我们希望打包一个使用了panda和matplotlib的Python应用程序。因此,一种选择是使用基于Debian的官方Python镜像(我提前拉取的),和以下这个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

发生了什么?

标准的PyPI wheel在Alpine上无效

如果你仔细查看上面基于Debian的构建,就会看到它正在下载matplotlib-3.1.2-cp38-cp38-manylinux1_x86_64.whl。这是一个预编译的二进制wheel。与此相反,Alpine会下载源代码(matplotlib-3.1.2.tar.gz),因为标准的Linux wheel在Alpine Linux上无效。

为什么?大多数Linux发行版使用标准C库的GNU版本(glibc),Python以及几乎所有的C程序都需要它。但是Alpine Linux使用musl,这些二进制wheel是针对glibc编译的,因此Alpine禁用了Linux wheel支持。

现在,大多数Python包都包含了PyPI上的二进制wheel,这大大缩短了安装时间。但是,如果你使用的是Alpine Linux,那么你就需要编译你所使用的每个Python包中的所有C代码。

这也意味着你需要自己找出每个系统库依赖项。在这种情况下,为了找出依赖项,我做了一些研究,最后得到下面这个经过更新的Dockerfile:

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的构建速度要慢得多,镜像要大得多,而且我不得不做了很多研究。

你不能解决这些问题吗?

构建时间

为了缩短构建时间,Alpine Edge(最终将成为下一个稳定版本)会包含matplotlib和panda。而且安装系统包非常快。但是,到2020年1月为止,当前的稳定版本还不包括这些流行的包。

然而,即使它们可用了,系统包也几乎总是滞后于PyPI上的包,Alpine也不太可能打包PyPI上的所有东西。据我所知,在实践中,大多数Python团队并没有将系统包用于Python依赖项,而是依赖于PyPI或Conda Forge。

镜像大小

一些读者指出,你可以删除最初安装的包,或者添加不缓存包下载的选项,或者使用多阶段构建。一位读者尝试生成了一个470MB的镜像

是的,你可以得到一个与基于slim的镜像大致相当的镜像,但是Alpine Linux的全部动机是更小的镜像和更快的构建。如果工作做够了,你可能会得到一个更小的镜像,但是你仍然要忍受长达1500秒的构建时间,当你使用python:3.8-slim镜像时,构建时间只有30秒。

但是等等,还有!

Alpine Linux会导致意料之外的运行时Bug

虽然理论上,Alpine使用的musl C库与其他Linux发行版使用的glibc基本兼容,但在实践中,这种差异可能会导致问题。当问题确实发生时,可能会很奇怪且出乎意料。

下面是一些例子:

  1. Alpine线程的默认堆栈大小更小,这可能导致Python崩溃
  2. Alpine的一位用户发现,由于musl分配内存的方式与glibc不同,他们的Python应用程序要慢很多
  3. 在使用WeWork工作空间的WiFi时,我曾经无法在minikube(虚拟机中的Kubernetes)上运行的Alpine镜像中查找DNS。原因是WeWork糟糕的DNS设置、Kubernetes和minikube实现DNS的方式,以及musl对这种边缘情况的处理与glibc的方式不同。musl没有错(它符合RFC),但是我不得不浪费时间找出问题所在,然后切换到基于glibc的镜像。
  4. 另一个用户发现了时间格式和解析的问题。

大多数或者说所有这些问题可能都已经得到解决,但毫无疑问,还有更多的问题有待发现。这种出人意料的破坏是又一件需要担心的事情。

不要将Alpine Linux用于Python镜像

除非你想要更长的构建时间、更大的镜像、更多的工作,以及潜在的隐藏Bug,否则你应该避免使用Alpine Linux作为基础镜像。

关于应该使用哪些镜像的建议,请参阅我的文章“选择一个好的基础镜像”。

英文原文:

Using Alpine can make Python Docker builds 50× slower

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/VODLe9FsiBkQdlcxJZZj
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券